ethagent 1.0.8 → 1.1.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/package.json +1 -2
- package/src/chat/ChatScreen.tsx +5 -4
- package/src/chat/ContinuityEditReviewView.tsx +3 -3
- package/src/chat/commands.ts +5 -5
- package/src/cli/ResetConfirmView.tsx +12 -13
- package/src/cli/main.tsx +27 -30
- package/src/cli/reset.ts +7 -8
- package/src/cli/updateNotice.ts +52 -0
- package/src/identity/continuity/envelope.ts +11 -5
- package/src/identity/continuity/storage.ts +1 -1
- package/src/identity/hub/IdentityHub.tsx +6 -7
- package/src/identity/hub/identityHubModel.ts +12 -12
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +39 -34
- package/src/identity/hub/screens/CreateFlow.tsx +4 -4
- package/src/identity/hub/screens/DetailsScreen.tsx +2 -2
- package/src/identity/hub/screens/EditProfileFlow.tsx +5 -5
- package/src/identity/hub/screens/ErrorScreen.tsx +2 -2
- package/src/identity/hub/screens/IdentitySummary.tsx +32 -12
- package/src/identity/hub/screens/MenuScreen.tsx +17 -17
- package/src/identity/hub/screens/NetworkScreen.tsx +7 -3
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +9 -7
- package/src/identity/hub/screens/RestoreFlow.tsx +2 -2
- package/src/identity/hub/screens/StorageCredentialScreen.tsx +5 -5
- package/src/identity/wallet/wallet-page/wallet.html +1095 -966
- package/src/models/ModelPicker.tsx +71 -71
- package/src/models/llamacppPreflight.ts +1 -1
- package/src/models/modelPickerOptions.ts +22 -22
- package/src/storage/factoryReset.ts +17 -20
- package/src/tools/privateContinuityEditTool.ts +1 -1
- package/src/ui/BrandSplash.tsx +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ethagent",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A privacy-first AI agent with a portable Ethereum identity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/ethagent.js",
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"ink": "^6.8.0",
|
|
48
48
|
"react": "^19.2.4",
|
|
49
49
|
"tsx": "^4.21.0",
|
|
50
|
-
"update-notifier": "^7.3.1",
|
|
51
50
|
"viem": "^2.48.4",
|
|
52
51
|
"zod": "^3.25.76"
|
|
53
52
|
},
|
package/src/chat/ChatScreen.tsx
CHANGED
|
@@ -79,6 +79,7 @@ import { EMPTY_MCP_SNAPSHOT, McpManager, type McpSnapshot } from '../mcp/manager
|
|
|
79
79
|
type ChatScreenProps = {
|
|
80
80
|
config: EthagentConfig
|
|
81
81
|
onReplaceConfig?: (next: EthagentConfig) => void
|
|
82
|
+
updateNotice?: string | null
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
type PendingPlan = {
|
|
@@ -122,7 +123,7 @@ async function ensureLocalProviderReady(config: EthagentConfig): Promise<{ ok: t
|
|
|
122
123
|
return { ok: true }
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, onReplaceConfig }) => {
|
|
126
|
+
export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, onReplaceConfig, updateNotice }) => {
|
|
126
127
|
useRegisterKeybindingContext('Chat')
|
|
127
128
|
const { exit } = useApp()
|
|
128
129
|
const [config, setConfig] = useState<EthagentConfig>(initialConfig)
|
|
@@ -1109,7 +1110,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
1109
1110
|
}
|
|
1110
1111
|
overlayRef.current = 'none'
|
|
1111
1112
|
setOverlay('none')
|
|
1112
|
-
pushNote('snapshot not
|
|
1113
|
+
pushNote('snapshot not saved yet.', 'dim')
|
|
1113
1114
|
},
|
|
1114
1115
|
[continuityEditReview, pushNote],
|
|
1115
1116
|
)
|
|
@@ -1118,7 +1119,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
1118
1119
|
setContinuityEditReview(null)
|
|
1119
1120
|
overlayRef.current = 'none'
|
|
1120
1121
|
setOverlay('none')
|
|
1121
|
-
pushNote('snapshot not
|
|
1122
|
+
pushNote('snapshot not saved yet.', 'dim')
|
|
1122
1123
|
}, [pushNote])
|
|
1123
1124
|
|
|
1124
1125
|
const handleCopyDone = useCallback(
|
|
@@ -1402,7 +1403,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
1402
1403
|
</Text>
|
|
1403
1404
|
</Box>
|
|
1404
1405
|
)
|
|
1405
|
-
const header = <BrandSplash contextLine={contextLine} tipLine={tipLine} />
|
|
1406
|
+
const header = <BrandSplash contextLine={contextLine} tipLine={tipLine} updateNotice={updateNotice ?? null} />
|
|
1406
1407
|
return (
|
|
1407
1408
|
<ConversationStack
|
|
1408
1409
|
header={header}
|
|
@@ -19,7 +19,7 @@ export const ContinuityEditReviewView: React.FC<{
|
|
|
19
19
|
}> = ({ review, onSelect, onCancel }) => (
|
|
20
20
|
<Surface
|
|
21
21
|
title="Private Continuity Updated"
|
|
22
|
-
subtitle="Review the file, then
|
|
22
|
+
subtitle="Review the file, then save an encrypted snapshot."
|
|
23
23
|
footer="enter select · esc later"
|
|
24
24
|
>
|
|
25
25
|
<Box flexDirection="column">
|
|
@@ -37,8 +37,8 @@ export const ContinuityEditReviewView: React.FC<{
|
|
|
37
37
|
<Select<ContinuityEditReviewAction>
|
|
38
38
|
options={[
|
|
39
39
|
{ value: 'open', label: `open ${review.file}`, hint: 'review the edited private file now' },
|
|
40
|
-
{ value: 'save-publish', label: '
|
|
41
|
-
{ value: 'later', label: 'later', hint: 'keep the local draft
|
|
40
|
+
{ value: 'save-publish', label: 'save snapshot now', hint: 'go directly to wallet approval' },
|
|
41
|
+
{ value: 'later', label: 'later', hint: 'keep the local draft unsaved' },
|
|
42
42
|
]}
|
|
43
43
|
onSubmit={onSelect}
|
|
44
44
|
onCancel={onCancel}
|
package/src/chat/commands.ts
CHANGED
|
@@ -154,7 +154,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
154
154
|
if (installed.length === 0) {
|
|
155
155
|
return {
|
|
156
156
|
kind: 'note',
|
|
157
|
-
text: '
|
|
157
|
+
text: 'No local model files downloaded. Open Alt+P and choose "Add Local Model File".',
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
const lines = installed.map(m => {
|
|
@@ -190,7 +190,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
190
190
|
return {
|
|
191
191
|
kind: 'note',
|
|
192
192
|
variant: 'error',
|
|
193
|
-
text: `'${name}' is not downloaded.
|
|
193
|
+
text: `'${name}' is not downloaded. Open Alt+P and choose "View Full Catalog" or "Add Local Model File".`,
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
} else {
|
|
@@ -364,7 +364,7 @@ async function runHuggingFace(args: string, ctx: SlashContext): Promise<SlashRes
|
|
|
364
364
|
return {
|
|
365
365
|
kind: 'note',
|
|
366
366
|
variant: 'dim',
|
|
367
|
-
text: '
|
|
367
|
+
text: 'No local model files downloaded. Press Alt+P and choose "Add Local Model File".',
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
370
|
const lines = installed.map(model => {
|
|
@@ -382,8 +382,8 @@ async function runHuggingFace(args: string, ctx: SlashContext): Promise<SlashRes
|
|
|
382
382
|
kind: 'note',
|
|
383
383
|
variant: 'dim',
|
|
384
384
|
text: link
|
|
385
|
-
? `
|
|
386
|
-
: '
|
|
385
|
+
? `Alt+P opened. Choose "Add Local Model File" and paste: ${link}`
|
|
386
|
+
: 'Alt+P opened. Choose "Add Local Model File" and paste the model URL or repo ID.',
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
@@ -16,27 +16,26 @@ export const ResetConfirmView: React.FC<{
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<Surface title="
|
|
19
|
+
<Surface title="Reset Local Data?" subtitle="Deletes this machine's ethagent data. Models and onchain records stay." footer="enter select · esc cancel">
|
|
20
20
|
<Box flexDirection="column">
|
|
21
|
-
<Section title="
|
|
21
|
+
<Section title="Deletes" lines={[
|
|
22
|
+
'Identity files, sessions, history, credentials',
|
|
22
23
|
localDataLine(plan.deletePaths.length),
|
|
23
|
-
'identity metadata, markdown vaults, sessions, prompt history',
|
|
24
|
-
'rewind history, permissions, credentials',
|
|
25
24
|
]} />
|
|
26
|
-
<Section title="
|
|
27
|
-
'
|
|
28
|
-
...(plan.preservedPaths.length > 0 ? [`${plan.preservedPaths.length} local model path${plan.preservedPaths.length === 1 ? '' : 's'}`] : ['
|
|
25
|
+
<Section title="Keeps" lines={[
|
|
26
|
+
'Local GGUF models and llama.cpp runners',
|
|
27
|
+
...(plan.preservedPaths.length > 0 ? [`${plan.preservedPaths.length} local model path${plan.preservedPaths.length === 1 ? '' : 's'}`] : ['No local model assets found']),
|
|
29
28
|
]} />
|
|
30
|
-
<Section title="
|
|
31
|
-
'onchain
|
|
32
|
-
'IPFS
|
|
29
|
+
<Section title="Not Touched" lines={[
|
|
30
|
+
'ERC-8004 tokens and onchain records',
|
|
31
|
+
'IPFS snapshots and public metadata',
|
|
33
32
|
]} />
|
|
34
33
|
</Box>
|
|
35
34
|
<Box marginTop={1}>
|
|
36
35
|
<Select<'confirm' | 'cancel'>
|
|
37
36
|
options={[
|
|
38
|
-
{ value: 'confirm', label: '
|
|
39
|
-
{ value: 'cancel', label: '
|
|
37
|
+
{ value: 'confirm', label: 'Reset Local Data', hint: 'Delete local ethagent data now' },
|
|
38
|
+
{ value: 'cancel', label: 'Cancel', hint: 'Leave local data unchanged' },
|
|
40
39
|
]}
|
|
41
40
|
onSubmit={choice => finish(choice === 'confirm')}
|
|
42
41
|
onCancel={() => finish(false)}
|
|
@@ -56,6 +55,6 @@ const Section: React.FC<{ title: string; lines: string[] }> = ({ title, lines })
|
|
|
56
55
|
)
|
|
57
56
|
|
|
58
57
|
function localDataLine(count: number): string {
|
|
59
|
-
if (count === 0) return '
|
|
58
|
+
if (count === 0) return 'No local ethagent data found'
|
|
60
59
|
return `${count} local path${count === 1 ? '' : 's'} under ~/.ethagent`
|
|
61
60
|
}
|
package/src/cli/main.tsx
CHANGED
|
@@ -12,29 +12,10 @@ import { AppInputProvider, useAppInput } from '../app/input/AppInputProvider.js'
|
|
|
12
12
|
import { loadConfig, type EthagentConfig } from '../storage/config.js'
|
|
13
13
|
import { runResetCommand } from './reset.js'
|
|
14
14
|
import { runPreviewCommand } from './preview.js'
|
|
15
|
+
import { checkForUpdates } from './updateNotice.js'
|
|
16
|
+
import { Spinner } from '../ui/Spinner.js'
|
|
15
17
|
|
|
16
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
17
|
-
const pkgPath = path.resolve(__dirname, '..', '..', 'package.json')
|
|
18
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
19
|
-
|
|
20
|
-
async function checkForUpdates() {
|
|
21
|
-
try {
|
|
22
|
-
const res = await fetch('https://registry.npmjs.org/ethagent/latest', { signal: AbortSignal.timeout(1200) })
|
|
23
|
-
const { version: latest } = await res.json() as { version: string }
|
|
24
|
-
if (latest && latest !== pkg.version) {
|
|
25
|
-
const line1 = ` Update available: ${theme.accentPrimary}${pkg.version}${theme.text} -> ${theme.accentMint}${latest}${theme.text} `
|
|
26
|
-
const line2 = ` Run ${theme.accentPeach}npm install -g ethagent${theme.text} to update `
|
|
27
|
-
const border = theme.dim + '─'.repeat(Math.max(line1.length - 20, line2.length - 20) + 10) + theme.text
|
|
28
|
-
|
|
29
|
-
process.stdout.write(`\n${theme.dim}┌${border}┐${theme.text}\n`)
|
|
30
|
-
process.stdout.write(`${theme.dim}│${theme.text} ${line1} ${theme.dim}│${theme.text}\n`)
|
|
31
|
-
process.stdout.write(`${theme.dim}│${theme.text} ${line2} ${theme.dim}│${theme.text}\n`)
|
|
32
|
-
process.stdout.write(`${theme.dim}└${border}┘${theme.text}\n\n`)
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// Silent fail for offline/timeout
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
19
|
|
|
39
20
|
function readVersion(): string {
|
|
40
21
|
try {
|
|
@@ -70,17 +51,24 @@ type AppPhase =
|
|
|
70
51
|
| { kind: 'cancelled' }
|
|
71
52
|
| { kind: 'error'; message: string }
|
|
72
53
|
|
|
73
|
-
const
|
|
54
|
+
const MIN_STARTUP_SPINNER_MS = 480
|
|
55
|
+
|
|
56
|
+
function delay(ms: number): Promise<void> {
|
|
57
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const AppRoot: React.FC<{ setExitCode: (code: number) => void; currentVersion: string }> = ({ setExitCode, currentVersion }) => {
|
|
74
61
|
const [phase, setPhase] = useState<AppPhase>({ kind: 'loading' })
|
|
62
|
+
const [updateNotice, setUpdateNotice] = useState<string | null>(null)
|
|
75
63
|
const { exit } = useApp()
|
|
76
64
|
|
|
77
65
|
useEffect(() => {
|
|
78
66
|
if (phase.kind !== 'loading') return
|
|
79
67
|
let cancelled = false
|
|
80
|
-
loadConfig()
|
|
68
|
+
Promise.all([loadConfig(), delay(MIN_STARTUP_SPINNER_MS)])
|
|
81
69
|
.then(config => {
|
|
82
70
|
if (cancelled) return
|
|
83
|
-
setPhase(config ? { kind: 'ready', config } : { kind: 'setup' })
|
|
71
|
+
setPhase(config[0] ? { kind: 'ready', config: config[0] } : { kind: 'setup' })
|
|
84
72
|
})
|
|
85
73
|
.catch((err: unknown) => {
|
|
86
74
|
if (cancelled) return
|
|
@@ -89,6 +77,16 @@ const AppRoot: React.FC<{ setExitCode: (code: number) => void }> = ({ setExitCod
|
|
|
89
77
|
return () => { cancelled = true }
|
|
90
78
|
}, [phase])
|
|
91
79
|
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
let cancelled = false
|
|
82
|
+
void checkForUpdates(currentVersion)
|
|
83
|
+
.then(notice => {
|
|
84
|
+
if (!cancelled) setUpdateNotice(notice)
|
|
85
|
+
})
|
|
86
|
+
.catch(() => {})
|
|
87
|
+
return () => { cancelled = true }
|
|
88
|
+
}, [currentVersion])
|
|
89
|
+
|
|
92
90
|
useEffect(() => {
|
|
93
91
|
if (phase.kind === 'cancelled') {
|
|
94
92
|
setExitCode(1)
|
|
@@ -111,7 +109,7 @@ const AppRoot: React.FC<{ setExitCode: (code: number) => void }> = ({ setExitCod
|
|
|
111
109
|
if (phase.kind === 'loading') {
|
|
112
110
|
return (
|
|
113
111
|
<Box padding={1}>
|
|
114
|
-
<
|
|
112
|
+
<Spinner label="Starting ethagent..." showElapsed={false} />
|
|
115
113
|
</Box>
|
|
116
114
|
)
|
|
117
115
|
}
|
|
@@ -141,16 +139,17 @@ const AppRoot: React.FC<{ setExitCode: (code: number) => void }> = ({ setExitCod
|
|
|
141
139
|
<ChatScreen
|
|
142
140
|
config={phase.config}
|
|
143
141
|
onReplaceConfig={next => setPhase({ kind: 'ready', config: next })}
|
|
142
|
+
updateNotice={updateNotice}
|
|
144
143
|
/>
|
|
145
144
|
)
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
async function runDefault(): Promise<number> {
|
|
147
|
+
async function runDefault(currentVersion: string): Promise<number> {
|
|
149
148
|
let exitCode = 0
|
|
150
149
|
const instance = render(
|
|
151
150
|
<AppInputProvider>
|
|
152
151
|
<KeybindingProvider>
|
|
153
|
-
<AppRoot setExitCode={code => { exitCode = code }} />
|
|
152
|
+
<AppRoot setExitCode={code => { exitCode = code }} currentVersion={currentVersion} />
|
|
154
153
|
</KeybindingProvider>
|
|
155
154
|
</AppInputProvider>,
|
|
156
155
|
{
|
|
@@ -169,9 +168,7 @@ async function main(): Promise<number> {
|
|
|
169
168
|
const argv = process.argv.slice(2)
|
|
170
169
|
const [cmd, ...rest] = argv
|
|
171
170
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!cmd) return runDefault()
|
|
171
|
+
if (!cmd) return runDefault(readVersion())
|
|
175
172
|
if (cmd === '--version' || cmd === '-v') {
|
|
176
173
|
process.stdout.write(`ethagent ${readVersion()}\n`)
|
|
177
174
|
return 0
|
package/src/cli/reset.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function runResetCommand(args: string[] = [], io: ResetCommandIO =
|
|
|
37
37
|
? await readTextConfirmation(plan, io)
|
|
38
38
|
: await readInkConfirmation(plan, io)
|
|
39
39
|
if (!confirmed) {
|
|
40
|
-
write('
|
|
40
|
+
write('Reset Cancelled.\n')
|
|
41
41
|
return 1
|
|
42
42
|
}
|
|
43
43
|
} else {
|
|
@@ -46,12 +46,12 @@ export async function runResetCommand(args: string[] = [], io: ResetCommandIO =
|
|
|
46
46
|
|
|
47
47
|
const result = await runFactoryReset({ clearSecrets: io.clearSecrets })
|
|
48
48
|
write([
|
|
49
|
-
'
|
|
50
|
-
`
|
|
51
|
-
`
|
|
49
|
+
'Reset Complete.',
|
|
50
|
+
`Deleted ${result.deletedPaths.length} local path${result.deletedPaths.length === 1 ? '' : 's'}.`,
|
|
51
|
+
`Cleared ${result.clearedSecretAccounts.length} secret account${result.clearedSecretAccounts.length === 1 ? '' : 's'}.`,
|
|
52
52
|
result.preservedPaths.length > 0
|
|
53
|
-
? `
|
|
54
|
-
: 'no local model assets
|
|
53
|
+
? `Kept ${result.preservedPaths.length} local model path${result.preservedPaths.length === 1 ? '' : 's'}.`
|
|
54
|
+
: 'Kept no local model assets.',
|
|
55
55
|
'',
|
|
56
56
|
].join('\n'))
|
|
57
57
|
return 0
|
|
@@ -90,7 +90,6 @@ async function readInkConfirmation(plan: FactoryResetPlan, io: ResetCommandIO):
|
|
|
90
90
|
|
|
91
91
|
async function readConfirmation(io: ResetCommandIO): Promise<string> {
|
|
92
92
|
if (io.readConfirmation) {
|
|
93
|
-
;(io.write ?? (text => { processStdout.write(text) }))('type confirm to wipe local ethagent data: ')
|
|
94
93
|
return io.readConfirmation()
|
|
95
94
|
}
|
|
96
95
|
|
|
@@ -99,7 +98,7 @@ async function readConfirmation(io: ResetCommandIO): Promise<string> {
|
|
|
99
98
|
output: io.output ?? processStdout,
|
|
100
99
|
})
|
|
101
100
|
try {
|
|
102
|
-
return await rl.question('
|
|
101
|
+
return await rl.question('Confirm reset: ')
|
|
103
102
|
} finally {
|
|
104
103
|
rl.close()
|
|
105
104
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type RegistryResponse = {
|
|
2
|
+
ok?: boolean
|
|
3
|
+
json: () => Promise<unknown>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type RegistryFetch = (url: string, init: { signal: AbortSignal }) => Promise<RegistryResponse>
|
|
7
|
+
|
|
8
|
+
const REGISTRY_LATEST_URL = 'https://registry.npmjs.org/ethagent/latest'
|
|
9
|
+
|
|
10
|
+
export function compareVersions(left: string, right: string): number {
|
|
11
|
+
const a = parseVersion(left)
|
|
12
|
+
const b = parseVersion(right)
|
|
13
|
+
if (!a || !b) return 0
|
|
14
|
+
for (let i = 0; i < 3; i++) {
|
|
15
|
+
if (a[i] > b[i]) return 1
|
|
16
|
+
if (a[i] < b[i]) return -1
|
|
17
|
+
}
|
|
18
|
+
return 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isNewerVersion(candidate: string, current: string): boolean {
|
|
22
|
+
return compareVersions(candidate, current) > 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatUpdateNotice(currentVersion: string, latestVersion: string): string | null {
|
|
26
|
+
if (!isNewerVersion(latestVersion, currentVersion)) return null
|
|
27
|
+
return `update available: ethagent ${currentVersion} -> ${latestVersion}; run npm i -g ethagent`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function checkForUpdates(
|
|
31
|
+
currentVersion: string,
|
|
32
|
+
options: { fetchImpl?: RegistryFetch; timeoutMs?: number } = {},
|
|
33
|
+
): Promise<string | null> {
|
|
34
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
35
|
+
const timeoutMs = options.timeoutMs ?? 1200
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetchImpl(REGISTRY_LATEST_URL, { signal: AbortSignal.timeout(timeoutMs) })
|
|
38
|
+
if (res.ok === false) return null
|
|
39
|
+
const body = await res.json()
|
|
40
|
+
if (!body || typeof body !== 'object') return null
|
|
41
|
+
const latest = (body as { version?: unknown }).version
|
|
42
|
+
return typeof latest === 'string' ? formatUpdateNotice(currentVersion, latest) : null
|
|
43
|
+
} catch {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseVersion(value: string): [number, number, number] | null {
|
|
49
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(value.trim())
|
|
50
|
+
if (!match) return null
|
|
51
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
52
|
+
}
|
|
@@ -79,15 +79,21 @@ export class ContinuitySnapshotOwnerMismatchError extends Error {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export const CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES = [
|
|
83
|
+
'ethagent: save or restore identity files',
|
|
84
|
+
'Action: encrypt or decrypt local identity files',
|
|
85
|
+
'Private: SOUL.md, MEMORY.md',
|
|
86
|
+
'Public: skills.json, public profile',
|
|
87
|
+
'Safety: no transaction, spending, or approvals',
|
|
88
|
+
'Version: 1',
|
|
89
|
+
] as const
|
|
90
|
+
|
|
82
91
|
export function createContinuitySnapshotChallenge(ownerAddress: string): string {
|
|
83
92
|
const checksum = toChecksumAddress(ownerAddress)
|
|
84
93
|
return [
|
|
85
|
-
|
|
94
|
+
CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES[0],
|
|
86
95
|
`Owner: ${checksum}`,
|
|
87
|
-
|
|
88
|
-
'Scope: read and restore private agent continuity only',
|
|
89
|
-
'Safety: this signature does not send a transaction, spend funds, or grant token approval',
|
|
90
|
-
'Version: 1',
|
|
96
|
+
...CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES.slice(1),
|
|
91
97
|
].join('\n')
|
|
92
98
|
}
|
|
93
99
|
|
|
@@ -287,7 +287,7 @@ export function defaultContinuityFiles(identity: EthagentIdentity, now = new Dat
|
|
|
287
287
|
'## Private Instructions',
|
|
288
288
|
'',
|
|
289
289
|
'- Keep owner-specific standing instructions in this file.',
|
|
290
|
-
'- Do not
|
|
290
|
+
'- Do not share this file directly; save it via the Identity Hub encrypted snapshot.',
|
|
291
291
|
'- Public capabilities belong in skills.json.',
|
|
292
292
|
'',
|
|
293
293
|
'## Boundaries',
|
|
@@ -196,7 +196,8 @@ export const IdentityHub: React.FC<IdentityHubProps> = ({ mode, config, initialA
|
|
|
196
196
|
|
|
197
197
|
useEffect(() => {
|
|
198
198
|
let cancelled = false
|
|
199
|
-
if (!identity
|
|
199
|
+
if (!identity) return
|
|
200
|
+
if (step.kind !== 'menu' && step.kind !== 'continuity-private' && step.kind !== 'continuity-public') return
|
|
200
201
|
|
|
201
202
|
const checkStatus = async () => {
|
|
202
203
|
try {
|
|
@@ -591,7 +592,7 @@ export const IdentityHub: React.FC<IdentityHubProps> = ({ mode, config, initialA
|
|
|
591
592
|
return (
|
|
592
593
|
<WalletApprovalScreen
|
|
593
594
|
title="Refetch Latest Snapshot"
|
|
594
|
-
subtitle="Wallet approval decrypts the latest
|
|
595
|
+
subtitle="Wallet approval decrypts the latest saved snapshot and overwrites local SOUL.md, MEMORY.md, and skills.json."
|
|
595
596
|
walletSession={walletSession}
|
|
596
597
|
label={restoreProgress?.label ?? 'fetching latest snapshot from chain...'}
|
|
597
598
|
onCancel={() => setStep(step.back)}
|
|
@@ -604,13 +605,12 @@ export const IdentityHub: React.FC<IdentityHubProps> = ({ mode, config, initialA
|
|
|
604
605
|
<PrivateContinuityScreen
|
|
605
606
|
identity={identity}
|
|
606
607
|
config={config}
|
|
608
|
+
workingStatus={workingStatus}
|
|
607
609
|
ready={continuityReady}
|
|
608
610
|
notice={step.notice}
|
|
609
|
-
canBackup={canRebackup}
|
|
610
611
|
footer={footer}
|
|
611
612
|
onOpenSoul={() => { void openContinuityFile('soul') }}
|
|
612
613
|
onOpenMemory={() => { void openContinuityFile('memory') }}
|
|
613
|
-
onBackup={() => setStep({ kind: 'rebackup-confirm', back: { kind: 'continuity-private' } })}
|
|
614
614
|
onBack={back}
|
|
615
615
|
/>
|
|
616
616
|
)
|
|
@@ -621,13 +621,12 @@ export const IdentityHub: React.FC<IdentityHubProps> = ({ mode, config, initialA
|
|
|
621
621
|
<PublicSkillsScreen
|
|
622
622
|
identity={identity}
|
|
623
623
|
config={config}
|
|
624
|
+
workingStatus={workingStatus}
|
|
624
625
|
ready={continuityReady}
|
|
625
626
|
notice={step.notice}
|
|
626
|
-
canPublish={canRebackup}
|
|
627
627
|
footer={footer}
|
|
628
628
|
onEditProfile={() => openPublicProfileEdit({ kind: 'continuity-public' })}
|
|
629
629
|
onOpenSkills={() => { void openContinuityFile('skills') }}
|
|
630
|
-
onPublish={() => setStep({ kind: 'rebackup-confirm', back: { kind: 'continuity-public' } })}
|
|
631
630
|
onBack={back}
|
|
632
631
|
/>
|
|
633
632
|
)
|
|
@@ -784,7 +783,7 @@ export const IdentityHub: React.FC<IdentityHubProps> = ({ mode, config, initialA
|
|
|
784
783
|
|
|
785
784
|
async function readPublishedPublicSkills(identity: EthagentIdentity): Promise<string> {
|
|
786
785
|
const cid = identity.publicSkills?.cid
|
|
787
|
-
if (!cid) throw new Error('no
|
|
786
|
+
if (!cid) throw new Error('no saved public skills CID')
|
|
788
787
|
return new TextDecoder().decode(await catFromIpfs(
|
|
789
788
|
identity.backup?.ipfsApiUrl ?? DEFAULT_IPFS_API_URL,
|
|
790
789
|
cid,
|
|
@@ -93,7 +93,7 @@ export function tokenLabel(candidate: Erc8004AgentCandidate): string {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export function tokenCandidateLabel(candidate: Erc8004AgentCandidate): string {
|
|
96
|
-
return candidate.name?.trim() || `
|
|
96
|
+
return candidate.name?.trim() || `Agent Token #${candidate.agentId.toString()}`
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
export function tokenCandidateSelectLabel(
|
|
@@ -200,8 +200,8 @@ export function identitySummaryRows(
|
|
|
200
200
|
const tokenValue = identity?.agentId ? `#${identity.agentId}` : 'not created'
|
|
201
201
|
const chain = chainSummaryRow(config, identity)
|
|
202
202
|
const stateValue = backup?.cid ? shortCid(backup.cid) : 'not saved yet'
|
|
203
|
-
const skillsValue = identity?.publicSkills?.cid ? shortCid(identity.publicSkills.cid) : 'not
|
|
204
|
-
const cardValue = identity?.publicSkills?.agentCardCid ? shortCid(identity.publicSkills.agentCardCid) : 'not
|
|
203
|
+
const skillsValue = identity?.publicSkills?.cid ? shortCid(identity.publicSkills.cid) : 'not saved'
|
|
204
|
+
const cardValue = identity?.publicSkills?.agentCardCid ? shortCid(identity.publicSkills.agentCardCid) : 'not saved'
|
|
205
205
|
const imageValue = typeof identity?.state?.imageUrl === 'string' && identity.state.imageUrl.trim() ? 'attached' : 'not attached'
|
|
206
206
|
return [
|
|
207
207
|
{ label: 'owner', value: ownerValue, tone: identity ? 'ok' : 'dim' },
|
|
@@ -232,8 +232,8 @@ export function identityDetailSections(
|
|
|
232
232
|
const chain = chainSummaryRow(config, identity)
|
|
233
233
|
const stateCid = backup?.cid ?? 'not saved yet'
|
|
234
234
|
const registrationCid = identity?.metadataCid ?? 'not saved yet'
|
|
235
|
-
const publicSkillsCid = identity?.publicSkills?.cid ?? 'not
|
|
236
|
-
const agentCardCid = identity?.publicSkills?.agentCardCid ?? 'not
|
|
235
|
+
const publicSkillsCid = identity?.publicSkills?.cid ?? 'not saved'
|
|
236
|
+
const agentCardCid = identity?.publicSkills?.agentCardCid ?? 'not saved'
|
|
237
237
|
|
|
238
238
|
return [
|
|
239
239
|
{
|
|
@@ -273,14 +273,14 @@ export type CopyableField = {
|
|
|
273
273
|
export function copyableIdentityFields(identity?: EthagentIdentity): CopyableField[] {
|
|
274
274
|
if (!identity) return []
|
|
275
275
|
const fields: CopyableField[] = []
|
|
276
|
-
if (identity.backup?.cid) fields.push({ label: '
|
|
277
|
-
if (identity.publicSkills?.cid) fields.push({ label: '
|
|
278
|
-
if (identity.publicSkills?.agentCardCid) fields.push({ label: '
|
|
279
|
-
if (identity.metadataCid) fields.push({ label: '
|
|
280
|
-
if (identity.agentUri) fields.push({ label: '
|
|
276
|
+
if (identity.backup?.cid) fields.push({ label: 'State CID', value: identity.backup.cid })
|
|
277
|
+
if (identity.publicSkills?.cid) fields.push({ label: 'Skills CID', value: identity.publicSkills.cid })
|
|
278
|
+
if (identity.publicSkills?.agentCardCid) fields.push({ label: 'Agent Card CID', value: identity.publicSkills.agentCardCid })
|
|
279
|
+
if (identity.metadataCid) fields.push({ label: 'Registration CID', value: identity.metadataCid })
|
|
280
|
+
if (identity.agentUri) fields.push({ label: 'Agent URI', value: identity.agentUri })
|
|
281
281
|
const owner = identity.ownerAddress ?? identity.address
|
|
282
|
-
if (owner) fields.push({ label: '
|
|
283
|
-
if (identity.agentId) fields.push({ label: '
|
|
282
|
+
if (owner) fields.push({ label: 'Owner Address', value: owner })
|
|
283
|
+
if (identity.agentId) fields.push({ label: 'Token ID', value: identity.agentId })
|
|
284
284
|
return fields
|
|
285
285
|
}
|
|
286
286
|
|