ethagent 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +7 -4
  2. package/package.json +2 -1
  3. package/src/app/FirstRun.tsx +155 -15
  4. package/src/app/FirstRunTimeline.tsx +4 -0
  5. package/src/app/input/AppInputProvider.tsx +19 -0
  6. package/src/app/input/appInputParser.ts +19 -4
  7. package/src/chat/ChatBottomPane.tsx +3 -1
  8. package/src/chat/ChatScreen.tsx +7 -1
  9. package/src/chat/ConversationStack.tsx +25 -19
  10. package/src/chat/MessageList.tsx +194 -53
  11. package/src/chat/chatSessionState.ts +1 -1
  12. package/src/chat/chatTurnOrchestrator.ts +59 -0
  13. package/src/chat/input/ChatInput.tsx +3 -0
  14. package/src/chat/input/textCursor.ts +13 -3
  15. package/src/chat/transcript/TranscriptView.tsx +7 -5
  16. package/src/chat/transcript/transcriptViewport.ts +88 -17
  17. package/src/chat/views/PermissionPrompt.tsx +26 -26
  18. package/src/chat/views/PermissionsView.tsx +18 -12
  19. package/src/chat/views/RewindView.tsx +3 -1
  20. package/src/cli/ResetConfirmView.tsx +24 -9
  21. package/src/identity/continuity/editor.ts +27 -2
  22. package/src/identity/continuity/envelope.ts +125 -0
  23. package/src/identity/continuity/publicSkills.ts +37 -1
  24. package/src/identity/continuity/skills/frontmatter.ts +183 -0
  25. package/src/identity/continuity/skills/loadSkills.ts +609 -0
  26. package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
  27. package/src/identity/continuity/skills/scaffold.ts +52 -0
  28. package/src/identity/continuity/skills/types.ts +30 -0
  29. package/src/identity/continuity/storage/defaults.ts +28 -47
  30. package/src/identity/continuity/storage/files.ts +1 -0
  31. package/src/identity/continuity/storage/paths.ts +1 -0
  32. package/src/identity/continuity/storage/scaffold.ts +25 -23
  33. package/src/identity/continuity/storage/status.ts +34 -5
  34. package/src/identity/continuity/storage/types.ts +3 -2
  35. package/src/identity/continuity/storage.ts +3 -0
  36. package/src/identity/hub/OperationalRoutes.tsx +105 -3
  37. package/src/identity/hub/Routes.tsx +5 -3
  38. package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
  39. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
  40. package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
  41. package/src/identity/hub/continuity/effects.ts +36 -5
  42. package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
  43. package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
  44. package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
  45. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
  46. package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
  47. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
  48. package/src/identity/hub/continuity/snapshot.ts +3 -0
  49. package/src/identity/hub/continuity/state.ts +3 -2
  50. package/src/identity/hub/continuity/vault.ts +42 -10
  51. package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
  52. package/src/identity/hub/identityHubReducer.ts +21 -0
  53. package/src/identity/hub/profile/effects.ts +16 -3
  54. package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
  55. package/src/identity/hub/restore/apply.ts +12 -1
  56. package/src/identity/hub/restore/recovery.ts +11 -1
  57. package/src/identity/hub/restore/resolve.ts +1 -1
  58. package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
  59. package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
  60. package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
  61. package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
  62. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
  63. package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
  64. package/src/identity/hub/shared/effects/sync.ts +16 -3
  65. package/src/identity/hub/shared/model/copy.ts +2 -4
  66. package/src/identity/hub/transfer/effects.ts +15 -2
  67. package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
  68. package/src/identity/hub/useIdentityHubController.ts +5 -1
  69. package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
  70. package/src/mcp/manager.ts +1 -1
  71. package/src/models/ModelPicker.tsx +89 -84
  72. package/src/models/llamacpp.ts +160 -11
  73. package/src/models/llamacppPreflight.ts +1 -16
  74. package/src/models/modelPickerOptions.ts +43 -37
  75. package/src/providers/contracts.ts +1 -0
  76. package/src/providers/openai-chat.ts +50 -9
  77. package/src/providers/openai-responses.ts +19 -4
  78. package/src/runtime/toolExecution.ts +4 -3
  79. package/src/runtime/turn.ts +61 -30
  80. package/src/tools/changeDirectoryTool.ts +1 -1
  81. package/src/tools/contracts.ts +10 -0
  82. package/src/tools/deleteFileTool.ts +1 -1
  83. package/src/tools/editTool.ts +1 -1
  84. package/src/tools/listDirectoryTool.ts +1 -1
  85. package/src/tools/listSkillFilesTool.ts +77 -0
  86. package/src/tools/listSkillsTool.ts +68 -0
  87. package/src/tools/mcpResourceTools.ts +2 -2
  88. package/src/tools/privateContinuityReadTool.ts +1 -1
  89. package/src/tools/readSkillTool.ts +107 -0
  90. package/src/tools/readTool.ts +1 -1
  91. package/src/tools/registry.ts +6 -0
  92. package/src/tools/writeFileTool.ts +22 -2
  93. package/src/ui/Spinner.tsx +1 -1
  94. package/src/identity/continuity/localBackup.ts +0 -249
  95. package/src/identity/continuity/zipWriter.ts +0 -95
  96. package/src/identity/hub/continuity/index.ts +0 -7
  97. package/src/identity/hub/ens/index.ts +0 -11
  98. package/src/identity/hub/restore/index.ts +0 -22
@@ -0,0 +1,213 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { Surface } from '../../../../ui/Surface.js'
4
+ import { Select, type SelectOption } from '../../../../ui/Select.js'
5
+ import { theme } from '../../../../ui/theme.js'
6
+ import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
7
+ import {
8
+ listSkillsTree,
9
+ type SkillsTreeView,
10
+ } from '../../../continuity/skills/loadSkills.js'
11
+ import type { SkillIndexEntry } from '../../../continuity/skills/types.js'
12
+ import { IdentitySummary } from '../../shared/components/IdentitySummary.js'
13
+ import type { ContinuityWorkingTreeStatus } from '../../../continuity/storage.js'
14
+
15
+ type SkillsTreeAction =
16
+ | { kind: 'skill'; relativePath: string }
17
+ | { kind: 'new' }
18
+ | { kind: 'delete' }
19
+ | { kind: 'visibility' }
20
+ | { kind: 'view-manifest' }
21
+ | { kind: 'open-folder' }
22
+ | { kind: 'noop' }
23
+ | { kind: 'back' }
24
+
25
+ interface SkillsTreeScreenProps {
26
+ identity?: EthagentIdentity
27
+ config?: EthagentConfig
28
+ workingStatus?: ContinuityWorkingTreeStatus | null
29
+ notice?: string
30
+ editorOpened?: boolean
31
+ footer: React.ReactNode
32
+ onOpenSkill: (relativePath: string) => void
33
+ onNewSkill: () => void
34
+ onDelete: () => void
35
+ onVisibility: () => void
36
+ onViewPublicManifest: () => void
37
+ onOpenFolder: () => void
38
+ onBack: () => void
39
+ }
40
+
41
+ export const SkillsTreeScreen: React.FC<SkillsTreeScreenProps> = ({
42
+ identity,
43
+ config,
44
+ workingStatus,
45
+ notice,
46
+ editorOpened,
47
+ footer,
48
+ onOpenSkill,
49
+ onNewSkill,
50
+ onDelete,
51
+ onVisibility,
52
+ onViewPublicManifest,
53
+ onOpenFolder,
54
+ onBack,
55
+ }) => {
56
+ const [tree, setTree] = useState<SkillsTreeView | null>(null)
57
+ const [error, setError] = useState<string | null>(null)
58
+
59
+ useEffect(() => {
60
+ let cancelled = false
61
+ if (!identity) {
62
+ setTree({ skills: [], supportingCounts: {} })
63
+ return () => { cancelled = true }
64
+ }
65
+ const refresh = (): Promise<void> => listSkillsTree(identity)
66
+ .then(view => {
67
+ if (cancelled) return
68
+ setTree(view)
69
+ setError(null)
70
+ })
71
+ .catch(err => {
72
+ if (cancelled) return
73
+ setTree({ skills: [], supportingCounts: {} })
74
+ setError(String((err as Error).message ?? err))
75
+ })
76
+ void refresh()
77
+ if (!editorOpened) return () => { cancelled = true }
78
+ const interval = setInterval(() => { void refresh() }, 1500)
79
+ return () => {
80
+ cancelled = true
81
+ clearInterval(interval)
82
+ }
83
+ }, [identity, editorOpened])
84
+
85
+ const subtitle = notice ?? 'Open a skill, create one, or remove one.'
86
+ const isLoading = tree === null
87
+ const skills = tree?.skills ?? []
88
+ const supportingCounts = tree?.supportingCounts ?? {}
89
+ const hasAny = skills.length > 0
90
+
91
+ const options = buildOptions(skills, supportingCounts, isLoading, hasAny)
92
+
93
+ return (
94
+ <Surface title="Skills" subtitle={subtitle} footer={footer}>
95
+ <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} />
96
+ {error && (
97
+ <Box marginTop={1}>
98
+ <Text color={theme.accentError}>{error}</Text>
99
+ </Box>
100
+ )}
101
+ {editorOpened && (
102
+ <Box marginTop={1}>
103
+ <Text color={theme.accentPeriwinkle}>Save with ctrl+s in your editor</Text>
104
+ </Box>
105
+ )}
106
+ <Box marginTop={1}>
107
+ <Select<SkillsTreeAction>
108
+ options={options}
109
+ hintLayout="inline"
110
+ onSubmit={choice => {
111
+ if (choice.kind === 'skill') return onOpenSkill(choice.relativePath)
112
+ if (choice.kind === 'new') return onNewSkill()
113
+ if (choice.kind === 'delete') return onDelete()
114
+ if (choice.kind === 'visibility') return onVisibility()
115
+ if (choice.kind === 'view-manifest') return onViewPublicManifest()
116
+ if (choice.kind === 'open-folder') return onOpenFolder()
117
+ if (choice.kind === 'back') return onBack()
118
+ }}
119
+ onCancel={onBack}
120
+ />
121
+ </Box>
122
+ </Surface>
123
+ )
124
+ }
125
+
126
+ function buildOptions(
127
+ entries: SkillIndexEntry[],
128
+ supportingCounts: Record<string, number>,
129
+ isLoading: boolean,
130
+ hasAny: boolean,
131
+ ): Array<SelectOption<SkillsTreeAction>> {
132
+ const rows: Array<SelectOption<SkillsTreeAction>> = []
133
+ const noopValue: SkillsTreeAction = { kind: 'noop' }
134
+
135
+ if (isLoading) {
136
+ rows.push({
137
+ value: noopValue,
138
+ role: 'notice',
139
+ label: 'Loading...',
140
+ labelColor: theme.dim,
141
+ indent: 0,
142
+ })
143
+ } else if (!hasAny) {
144
+ rows.push({ value: noopValue, role: 'section', label: 'Catalog' })
145
+ rows.push({ value: noopValue, role: 'notice', label: 'skills/', labelColor: theme.dim, indent: 3 })
146
+ rows.push({ value: noopValue, role: 'notice', label: '└── <skill>/', labelColor: theme.dim, indent: 3 })
147
+ rows.push({ value: noopValue, role: 'notice', label: ' └── SKILL.md', labelColor: theme.dim, indent: 3 })
148
+ } else {
149
+ rows.push({ value: noopValue, role: 'section', label: 'Catalog' })
150
+ rows.push({ value: noopValue, role: 'notice', label: 'skills/', labelColor: theme.dim, indent: 3 })
151
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name))
152
+ for (let i = 0; i < sorted.length; i++) {
153
+ const skill = sorted[i]
154
+ if (!skill) continue
155
+ const isLast = i === sorted.length - 1
156
+ const branch = isLast ? '└── ' : '├── '
157
+ const supportCount = supportingCounts[skill.name] ?? 0
158
+ const meta = [capitalize(skill.visibility)]
159
+ if (supportCount > 0) meta.push(`${supportCount + 1} files`)
160
+ rows.push({
161
+ value: { kind: 'skill', relativePath: skill.relativePath },
162
+ label: `${branch}${skill.name}/SKILL.md`,
163
+ hint: meta.join(' · '),
164
+ indent: 3,
165
+ })
166
+ }
167
+ }
168
+
169
+ rows.push({ value: noopValue, role: 'notice', label: '' })
170
+ rows.push({ value: noopValue, role: 'section', label: 'Manage' })
171
+ rows.push({
172
+ value: { kind: 'new' },
173
+ label: 'New Skill',
174
+ hint: 'Scaffold a new skill folder with SKILL.md',
175
+ })
176
+ if (hasAny) {
177
+ rows.push({
178
+ value: { kind: 'delete' },
179
+ label: 'Delete Skill',
180
+ hint: 'Remove a skill folder and all its supporting files',
181
+ })
182
+ rows.push({
183
+ value: { kind: 'visibility' },
184
+ label: 'Change Visibility',
185
+ hint: 'Toggle public, discoverable, or private',
186
+ })
187
+ }
188
+ rows.push({ value: noopValue, role: 'section', label: 'Inspect' })
189
+ rows.push({
190
+ value: { kind: 'view-manifest' },
191
+ label: 'Edit skills.json',
192
+ hint: 'Open skills.json in your editor',
193
+ })
194
+ rows.push({
195
+ value: { kind: 'open-folder' },
196
+ label: 'Open Skills Folder',
197
+ hint: 'Reveal skills/ in your file manager',
198
+ })
199
+ rows.push({ value: noopValue, role: 'section', label: 'Navigation' })
200
+ rows.push({
201
+ value: { kind: 'back' },
202
+ label: 'Back',
203
+ hint: 'Return to Identity Hub menu',
204
+ role: 'utility',
205
+ })
206
+
207
+ return rows
208
+ }
209
+
210
+ function capitalize(value: string): string {
211
+ if (!value) return value
212
+ return value.charAt(0).toUpperCase() + value.slice(1)
213
+ }
@@ -12,6 +12,7 @@ import {
12
12
  continuityAgentSnapshot,
13
13
  defaultContinuityFiles,
14
14
  } from '../../continuity/storage.js'
15
+ import type { ContinuitySkillsTree } from '../../continuity/envelope.js'
15
16
  import type { Erc8004RegistryConfig, EthagentOperatorsPointer } from '../../registry/erc8004.js'
16
17
  import { readOwnerAddressField } from '../../identityCompat.js'
17
18
  import { readCustodyMode } from '../custody/state.js'
@@ -206,6 +207,7 @@ export function createContinuityEnvelopeForSave(args: {
206
207
  walletSignature: string
207
208
  state: Record<string, unknown>
208
209
  files: ReturnType<typeof defaultContinuityFiles>
210
+ skills?: ContinuitySkillsTree
209
211
  walletAccess: WalletRestoreAccessContext
210
212
  challengePurpose?: WalletChallengePurpose
211
213
  }): ContinuitySnapshotEnvelope {
@@ -259,6 +261,7 @@ export function createContinuityEnvelopeForSave(args: {
259
261
  payload: {
260
262
  agent: continuityAgentSnapshot(args.identity),
261
263
  files: args.files,
264
+ ...(args.skills && Object.keys(args.skills).length > 0 ? { skills: args.skills } : {}),
262
265
  transcript: [],
263
266
  state: args.state,
264
267
  },
@@ -20,13 +20,14 @@ export function changedContinuitySnapshotFiles(
20
20
  workingStatus?: ContinuityWorkingTreeStatus | null,
21
21
  ): string[] {
22
22
  if (!workingStatus?.localContentHashes || !workingStatus.publishedContentHashes) return []
23
- const files: Array<keyof typeof workingStatus.localContentHashes> = ['SOUL.md', 'MEMORY.md', 'skills.json']
23
+ const files: Array<keyof typeof workingStatus.localContentHashes> = ['SOUL.md', 'MEMORY.md', 'skills.json', 'private-skills']
24
24
  return files
25
- .filter(file => workingStatus.localContentHashes?.[file] !== workingStatus.publishedContentHashes?.[file])
25
+ .filter(file => (workingStatus.localContentHashes?.[file] ?? '') !== (workingStatus.publishedContentHashes?.[file] ?? ''))
26
26
  .map(displayContinuitySnapshotFile)
27
27
  }
28
28
 
29
29
  function displayContinuitySnapshotFile(file: keyof NonNullable<ContinuityWorkingTreeStatus['localContentHashes']>): string {
30
+ if (file === 'private-skills') return 'skills/'
30
31
  return file
31
32
  }
32
33
 
@@ -2,6 +2,7 @@ import { getAddress, type Address, type Hex } from 'viem'
2
2
  import type { EthagentIdentity } from '../../../storage/config.js'
3
3
  import {
4
4
  prepareSyncedIdentityMarkdownScaffold,
5
+ prepareSyncedSkillsTree,
5
6
  readContinuityFiles,
6
7
  readPublicSkillsFile,
7
8
  writeIdentityMarkdownScaffold,
@@ -10,13 +11,20 @@ import {
10
11
  import {
11
12
  createWalletRestoreAccessChallenge,
12
13
  serializeContinuitySnapshotEnvelope,
14
+ type ContinuityFiles,
15
+ type ContinuitySkillsTree,
13
16
  type WalletChallengePurpose,
14
17
  } from '../../continuity/envelope.js'
15
18
  import {
19
+ appendPublicSkillEntries,
16
20
  createAgentCard,
17
21
  defaultPublicSkillsProfile,
18
22
  serializeAgentCard,
19
23
  } from '../../continuity/publicSkills.js'
24
+ import {
25
+ derivePublicSkillEntries,
26
+ syncPublicSkillsManifest,
27
+ } from '../../continuity/skills/publicSkillsSync.js'
20
28
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
21
29
  import { addToIpfs, DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
22
30
  import {
@@ -58,6 +66,11 @@ type VaultPublishPrepared = {
58
66
  nextIdentity: EthagentIdentity
59
67
  markdownScaffold?: IdentityMarkdownScaffold
60
68
  completionMessage: string
69
+ publishedSources: {
70
+ privateFiles: ContinuityFiles
71
+ publicSkills: string
72
+ skills: ContinuitySkillsTree
73
+ }
61
74
  }
62
75
 
63
76
  export async function runOperatorWalletRebackup(args: {
@@ -129,18 +142,22 @@ export async function runOperatorWalletRebackup(args: {
129
142
  const continuityFiles = markdownScaffold
130
143
  ? { 'SOUL.md': markdownScaffold['SOUL.md'], 'MEMORY.md': markdownScaffold['MEMORY.md'] }
131
144
  : await readContinuityFiles(nextIdentityForFiles)
132
- const publicSkillsJson = markdownScaffold
133
- ? markdownScaffold['skills.json']
134
- : await readPublicSkillsFile(nextIdentityForFiles)
145
+ const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
135
146
  const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
136
147
  assertVerifiedPin(publicSkillsPin)
148
+ const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
149
+ const augmentedPublicProfile = appendPublicSkillEntries(
150
+ defaultPublicSkillsProfile(nextIdentityForFiles),
151
+ publicSkillEntries,
152
+ )
137
153
  const agentCardPin = await addToIpfs(
138
154
  DEFAULT_IPFS_API_URL,
139
- serializeAgentCard(createAgentCard(defaultPublicSkillsProfile(nextIdentityForFiles))),
155
+ serializeAgentCard(createAgentCard(augmentedPublicProfile)),
140
156
  fetch,
141
157
  { pinataJwt: step.pinataJwt },
142
158
  )
143
159
  assertVerifiedPin(agentCardPin)
160
+ const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
144
161
  const envelope = createContinuityEnvelopeForSave({
145
162
  identity: nextIdentityForFiles,
146
163
  registry: step.registry,
@@ -149,6 +166,7 @@ export async function runOperatorWalletRebackup(args: {
149
166
  walletSignature: wallet.signature,
150
167
  state,
151
168
  files: continuityFiles,
169
+ skills: skillsTree,
152
170
  walletAccess,
153
171
  ...(challengePurpose ? { challengePurpose } : {}),
154
172
  })
@@ -185,7 +203,11 @@ export async function runOperatorWalletRebackup(args: {
185
203
  await writeIdentityMarkdownScaffold(nextIdentity, markdownScaffold)
186
204
  }
187
205
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'local operator-wallet snapshot' }).catch(() => null)
188
- await markCurrentContinuityFilesPublished(nextIdentity).catch(() => null)
206
+ await markCurrentContinuityFilesPublished(nextIdentity, {
207
+ privateFiles: continuityFiles,
208
+ publicSkills: publicSkillsJson,
209
+ skills: skillsTree,
210
+ }).catch(() => null)
189
211
  const completionMessage = nextEnsName !== undefined && nextEnsName !== ((step.identity.state as Record<string, unknown> | undefined)?.ensName as string | undefined)
190
212
  ? 'Snapshot saved locally. Owner wallet still needs to publish to make ENS changes discoverable.'
191
213
  : uploadedImageUri !== undefined
@@ -262,18 +284,22 @@ async function runOperatorWalletVaultPublish(args: {
262
284
  const continuityFiles = markdownScaffold
263
285
  ? { 'SOUL.md': markdownScaffold['SOUL.md'], 'MEMORY.md': markdownScaffold['MEMORY.md'] }
264
286
  : await readContinuityFiles(nextIdentityForFiles)
265
- const publicSkillsJson = markdownScaffold
266
- ? markdownScaffold['skills.json']
267
- : await readPublicSkillsFile(nextIdentityForFiles)
287
+ const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
268
288
  const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
269
289
  assertVerifiedPin(publicSkillsPin)
290
+ const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
291
+ const augmentedPublicProfile = appendPublicSkillEntries(
292
+ defaultPublicSkillsProfile(nextIdentityForFiles),
293
+ publicSkillEntries,
294
+ )
270
295
  const agentCardPin = await addToIpfs(
271
296
  DEFAULT_IPFS_API_URL,
272
- serializeAgentCard(createAgentCard(defaultPublicSkillsProfile(nextIdentityForFiles))),
297
+ serializeAgentCard(createAgentCard(augmentedPublicProfile)),
273
298
  fetch,
274
299
  { pinataJwt: step.pinataJwt },
275
300
  )
276
301
  assertVerifiedPin(agentCardPin)
302
+ const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
277
303
  const envelope = createContinuityEnvelopeForSave({
278
304
  identity: nextIdentityForFiles,
279
305
  registry: step.registry,
@@ -282,6 +308,7 @@ async function runOperatorWalletVaultPublish(args: {
282
308
  walletSignature: wallet.signature,
283
309
  state,
284
310
  files: continuityFiles,
311
+ skills: skillsTree,
285
312
  walletAccess,
286
313
  ...(challengePurpose ? { challengePurpose } : {}),
287
314
  })
@@ -354,6 +381,11 @@ async function runOperatorWalletVaultPublish(args: {
354
381
  nextIdentity,
355
382
  ...(markdownScaffold ? { markdownScaffold } : {}),
356
383
  completionMessage,
384
+ publishedSources: {
385
+ privateFiles: continuityFiles,
386
+ publicSkills: publicSkillsJson,
387
+ skills: skillsTree,
388
+ },
357
389
  },
358
390
  }
359
391
  },
@@ -373,6 +405,6 @@ async function runOperatorWalletVaultPublish(args: {
373
405
  await writeIdentityMarkdownScaffold(nextIdentity, result.prepared.markdownScaffold)
374
406
  }
375
407
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'operator-published snapshot' }).catch(() => null)
376
- await markCurrentContinuityFilesPublished(nextIdentity).catch(() => null)
408
+ await markCurrentContinuityFilesPublished(nextIdentity, result.prepared.publishedSources).catch(() => null)
377
409
  await callbacks.onIdentityComplete(nextIdentity, result.prepared.completionMessage, 'update')
378
410
  }
@@ -214,7 +214,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
214
214
  <Surface
215
215
  title="Switch to Advanced"
216
216
  subtitle="Move this token into its own Vault so authorized operator wallets can update this agent onchain without your signature each time."
217
- footer={footerHint('enter confirm, esc back')}
217
+ footer={footerHint('enter confirm · esc back')}
218
218
  >
219
219
  <Box flexDirection="column">
220
220
  <Row label="Token" value={tokenLabel} />
@@ -236,7 +236,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
236
236
  { value: 'confirm', label: 'Yes, Switch to Advanced', hint: `Sign with ${shortAddress(ownerAddress || tokenOwner)} to deposit this token into its Vault` },
237
237
  { value: 'transfer', role: 'section', label: 'Move Token First' },
238
238
  { value: 'transfer', label: 'Prepare Token Transfer', hint: 'Move the token to a different wallet first, with snapshot handoff' },
239
- { value: 'back', role: 'section', label: 'Cancel' },
239
+ { value: 'back', role: 'section', label: 'Navigation' },
240
240
  { value: 'back', label: 'No, Go Back', hint: 'Return without changing custody', role: 'utility' },
241
241
  ]}
242
242
  hintLayout="inline"
@@ -278,7 +278,7 @@ export const CustodyEditFlow: React.FC<CustodyEditFlowProps> = ({
278
278
  options={[
279
279
  { value: 'confirm', role: 'section', label: 'Confirm' },
280
280
  { value: 'confirm', label: 'Yes, Switch to Simple', hint: `Sign with the owner wallet to unwrap ${tokenLabel} from its Vault` },
281
- { value: 'back', role: 'section', label: 'Cancel' },
281
+ { value: 'back', role: 'section', label: 'Navigation' },
282
282
  { value: 'back', label: 'No, Go Back', hint: 'Return without changing custody', role: 'utility' },
283
283
  ]}
284
284
  hintLayout="inline"
@@ -59,6 +59,13 @@ export type Step =
59
59
  | { kind: 'public-profile-storage'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; error?: string; pinataJwt?: string; profileUpdates?: ProfileUpdates; returnTo?: Step; vaultAddress?: `0x${string}` }
60
60
  | { kind: 'continuity-private'; notice?: string; editorOpened?: boolean }
61
61
  | { kind: 'continuity-public'; notice?: string; editorOpened?: boolean }
62
+ | { kind: 'continuity-skills-tree'; notice?: string; editorOpened?: boolean }
63
+ | { kind: 'continuity-skill-new'; error?: string }
64
+ | { kind: 'continuity-skill-new-visibility'; name: string; error?: string }
65
+ | { kind: 'continuity-skill-delete'; notice?: string }
66
+ | { kind: 'continuity-skill-delete-confirm'; target: { kind: 'skill'; relativePath: string } }
67
+ | { kind: 'continuity-skill-visibility'; notice?: string }
68
+ | { kind: 'continuity-skill-visibility-pick'; relativePath: string }
62
69
  | { kind: 'rebackup-confirm'; back: Step }
63
70
  | { kind: 'recovery-refetch-confirm'; back: Step }
64
71
  | { kind: 'recovery-refetching'; identity: EthagentIdentity; registry: Erc8004RegistryConfig; back: Step }
@@ -173,6 +180,20 @@ function backStep(from: Step): Step {
173
180
  case 'continuity-private':
174
181
  case 'continuity-public':
175
182
  return { kind: 'menu' }
183
+ case 'continuity-skills-tree':
184
+ return { kind: 'menu' }
185
+ case 'continuity-skill-new':
186
+ return { kind: 'continuity-skills-tree' }
187
+ case 'continuity-skill-new-visibility':
188
+ return { kind: 'continuity-skill-new' }
189
+ case 'continuity-skill-delete':
190
+ return { kind: 'continuity-skills-tree' }
191
+ case 'continuity-skill-delete-confirm':
192
+ return { kind: 'continuity-skill-delete' }
193
+ case 'continuity-skill-visibility':
194
+ return { kind: 'continuity-skills-tree' }
195
+ case 'continuity-skill-visibility-pick':
196
+ return { kind: 'continuity-skill-visibility' }
176
197
  case 'rebackup-confirm':
177
198
  case 'recovery-refetch-confirm':
178
199
  case 'recovery-refetching':
@@ -6,15 +6,21 @@ import {
6
6
  type WalletChallengePurpose,
7
7
  } from '../../continuity/envelope.js'
8
8
  import {
9
+ prepareSyncedSkillsTree,
9
10
  prepareSyncedPublicSkillsJson,
10
11
  readContinuityFiles,
11
12
  writePublicSkillsFile,
12
13
  } from '../../continuity/storage.js'
13
14
  import {
15
+ appendPublicSkillEntries,
14
16
  createAgentCard,
15
17
  defaultPublicSkillsProfile,
16
18
  serializeAgentCard,
17
19
  } from '../../continuity/publicSkills.js'
20
+ import {
21
+ derivePublicSkillEntries,
22
+ syncPublicSkillsManifest,
23
+ } from '../../continuity/skills/publicSkillsSync.js'
18
24
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
19
25
  import { addToIpfs, DEFAULT_IPFS_API_URL, isPinataUploadUrl } from '../../storage/ipfs.js'
20
26
  import {
@@ -151,7 +157,7 @@ async function runPublicProfileSigningInner(
151
157
  includeLastBackedUpAt: false,
152
158
  })
153
159
  const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
154
- const publicSkillsJson = await prepareSyncedPublicSkillsJson(nextIdentityForFiles)
160
+ const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
155
161
  const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
156
162
  assertVerifiedPin(publicSkillsPin)
157
163
  const agentCardPin = await addToIpfs(
@@ -395,18 +401,24 @@ async function prepareOperatorProfileArtifacts(args: {
395
401
  })
396
402
  const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
397
403
 
398
- const publicSkillsJson = await prepareSyncedPublicSkillsJson(nextIdentityForFiles)
404
+ const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
399
405
  const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
400
406
  assertVerifiedPin(publicSkillsPin)
407
+ const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
408
+ const augmentedPublicProfile = appendPublicSkillEntries(
409
+ defaultPublicSkillsProfile(nextIdentityForFiles),
410
+ publicSkillEntries,
411
+ )
401
412
  const agentCardPin = await addToIpfs(
402
413
  DEFAULT_IPFS_API_URL,
403
- serializeAgentCard(createAgentCard(defaultPublicSkillsProfile(nextIdentityForFiles))),
414
+ serializeAgentCard(createAgentCard(augmentedPublicProfile)),
404
415
  fetch,
405
416
  { pinataJwt: step.pinataJwt },
406
417
  )
407
418
  assertVerifiedPin(agentCardPin)
408
419
 
409
420
  const continuityFiles = await readContinuityFiles(nextIdentityForFiles)
421
+ const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
410
422
  const envelope = createContinuityEnvelopeForSave({
411
423
  identity: nextIdentityForFiles,
412
424
  registry: step.registry,
@@ -415,6 +427,7 @@ async function prepareOperatorProfileArtifacts(args: {
415
427
  walletSignature: wallet.signature,
416
428
  state,
417
429
  files: continuityFiles,
430
+ skills: skillsTree,
418
431
  walletAccess,
419
432
  challengePurpose,
420
433
  })
@@ -1,8 +1,9 @@
1
1
  import React from 'react'
2
- import { Text } from 'ink'
2
+ import { Box, Text } from 'ink'
3
3
  import { Surface } from '../../../ui/Surface.js'
4
4
  import { Select } from '../../../ui/Select.js'
5
5
  import { TextInput } from '../../../ui/TextInput.js'
6
+ import { Spinner } from '../../../ui/Spinner.js'
6
7
  import { theme } from '../../../ui/theme.js'
7
8
  import { normalizeErc8004RegistryConfig } from '../../registry/erc8004.js'
8
9
  import {
@@ -18,7 +19,7 @@ import { WalletApprovalScreen } from '../shared/components/WalletApprovalScreen.
18
19
  import { BusyScreen } from '../shared/components/BusyScreen.js'
19
20
  import type { BrowserWalletReady } from '../../wallet/browserWallet.js'
20
21
  import type { EthagentConfig } from '../../../storage/config.js'
21
- import { restoreSignatureRequestForStep } from './index.js'
22
+ import { restoreSignatureRequestForStep } from './auth.js'
22
23
  import type { RestoreProgress } from '../shared/effects/types.js'
23
24
 
24
25
  type RestoreStep = Exclude<Extract<Step, { kind: `restore-${string}` }>, { kind: 'restore-wallet' | 'restore-network' }>
@@ -122,11 +123,24 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
122
123
  }
123
124
 
124
125
  if (step.kind === 'restore-ens-input') {
126
+ if (step.busy) {
127
+ return (
128
+ <Surface
129
+ title={isSwitch ? 'Load Agent' : 'Restore Agent'}
130
+ subtitle="Looking up the agent onchain."
131
+ footer={footerHint('esc cancels')}
132
+ >
133
+ <Box marginTop={1}>
134
+ <Spinner label="looking up ENS name onchain..." />
135
+ </Box>
136
+ </Surface>
137
+ )
138
+ }
125
139
  return (
126
140
  <Surface
127
141
  title={isSwitch ? 'Load Agent' : 'Restore Agent'}
128
142
  subtitle="Enter the agent's ENS name to decrypt with an authorized operator wallet."
129
- footer={footerHint(step.busy ? 'Looking up...' : 'enter continue · esc back')}
143
+ footer={footerHint('enter continue · esc back')}
130
144
  >
131
145
  <Text color={theme.dim}>The full agent subdomain, e.g. agent.example.eth.</Text>
132
146
  <TextInput
@@ -134,17 +148,35 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
134
148
  onSubmit={value => onEnsSubmit(value.trim())}
135
149
  onCancel={onBack}
136
150
  />
137
- {step.error ? <Text color={theme.accentError}>{step.error}</Text> : null}
151
+ {step.error ? (
152
+ <Box marginTop={1} flexDirection="column">
153
+ <Text color={theme.accentError}>Could not resolve ENS name</Text>
154
+ <Text color={theme.textSubtle}>{step.error}</Text>
155
+ </Box>
156
+ ) : null}
138
157
  </Surface>
139
158
  )
140
159
  }
141
160
 
142
161
  if (step.kind === 'restore-token-id-input') {
162
+ if (step.busy) {
163
+ return (
164
+ <Surface
165
+ title={isSwitch ? 'Load Agent' : 'Restore Agent'}
166
+ subtitle="Looking up the agent onchain."
167
+ footer={footerHint('esc cancels')}
168
+ >
169
+ <Box marginTop={1}>
170
+ <Spinner label="looking up token onchain..." />
171
+ </Box>
172
+ </Surface>
173
+ )
174
+ }
143
175
  return (
144
176
  <Surface
145
177
  title={isSwitch ? 'Load Agent' : 'Restore Agent'}
146
178
  subtitle={`Enter the ERC-8004 token ID on ${networkLabelForRegistry(step.registry)}.`}
147
- footer={footerHint(step.busy ? 'Looking up...' : 'enter continue · esc back')}
179
+ footer={footerHint('enter continue · esc back')}
148
180
  >
149
181
  <Text color={theme.dim}>The integer token ID assigned at mint.</Text>
150
182
  <TextInput
@@ -152,7 +184,12 @@ export const RestoreFlow: React.FC<RestoreFlowProps> = ({
152
184
  onSubmit={value => onTokenIdSubmit(value.trim())}
153
185
  onCancel={onBack}
154
186
  />
155
- {step.error ? <Text color={theme.accentError}>{step.error}</Text> : null}
187
+ {step.error ? (
188
+ <Box marginTop={1} flexDirection="column">
189
+ <Text color={theme.accentError}>Could not resolve token</Text>
190
+ <Text color={theme.textSubtle}>{step.error}</Text>
191
+ </Box>
192
+ ) : null}
156
193
  </Surface>
157
194
  )
158
195
  }
@@ -5,7 +5,12 @@ import {
5
5
  restoreContinuitySnapshotEnvelope,
6
6
  transferSnapshotMetadataFromEnvelope,
7
7
  } from '../../continuity/envelope.js'
8
- import { ensureIdentityMarkdownScaffold, writeContinuityFiles } from '../../continuity/storage.js'
8
+ import {
9
+ ensureIdentityMarkdownScaffold,
10
+ restoreSkillsTree,
11
+ writeContinuityFiles,
12
+ } from '../../continuity/storage.js'
13
+ import { syncPublicSkillsManifest } from '../../continuity/skills/publicSkillsSync.js'
9
14
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
10
15
  import { requestBrowserWalletSignature } from '../../wallet/browserWallet.js'
11
16
  import { setVaultAddressField } from '../../identityCompat.js'
@@ -31,6 +36,7 @@ export async function runRestoreAuthorize(
31
36
  callbacks.onRestoreProgress?.({ phase: 'decrypting', label: 'signature received · decrypting encrypted snapshot...' })
32
37
  let restored: ReturnType<typeof restoreAgentStateBackupEnvelope> | ReturnType<typeof restoreContinuitySnapshotEnvelope>
33
38
  let continuityFiles: ReturnType<typeof restoreContinuitySnapshotEnvelope>['files'] | undefined
39
+ let continuitySkills: ReturnType<typeof restoreContinuitySnapshotEnvelope>['skills']
34
40
  if (isContinuitySnapshotEnvelope(step.envelope)) {
35
41
  const payload = restoreContinuitySnapshotEnvelope({
36
42
  envelope: step.envelope,
@@ -39,6 +45,7 @@ export async function runRestoreAuthorize(
39
45
  })
40
46
  restored = payload
41
47
  continuityFiles = payload.files
48
+ continuitySkills = payload.skills
42
49
  } else {
43
50
  restored = restoreAgentStateBackupEnvelope({
44
51
  envelope: step.envelope,
@@ -104,9 +111,13 @@ export async function runRestoreAuthorize(
104
111
  if (continuityFiles) {
105
112
  await writeContinuityFiles(nextIdentity, continuityFiles)
106
113
  }
114
+ if (continuitySkills) {
115
+ await restoreSkillsTree(nextIdentity, continuitySkills)
116
+ }
107
117
  callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing restored identity...' })
108
118
  await restorePublishedPublicSkills(nextIdentity, step.apiUrl, step.candidate.publicDiscovery?.skillsCid)
109
119
  await ensureIdentityMarkdownScaffold(nextIdentity)
120
+ await syncPublicSkillsManifest(nextIdentity).catch(() => null)
110
121
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'restored from agent backup' }).catch(() => null)
111
122
  await callbacks.onIdentityComplete(nextIdentity, `ERC-8004 agent restored · #${step.candidate.agentId.toString()}`, 'restore')
112
123
  }