ethagent 2.3.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 (110) hide show
  1. package/README.md +18 -4
  2. package/package.json +2 -1
  3. package/src/app/FirstRun.tsx +157 -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 +12 -1
  8. package/src/chat/ChatScreen.tsx +17 -5
  9. package/src/chat/ConversationStack.tsx +25 -19
  10. package/src/chat/MessageList.tsx +194 -53
  11. package/src/chat/chatSessionState.ts +4 -1
  12. package/src/chat/chatTurnOrchestrator.ts +65 -2
  13. package/src/chat/input/ChatInput.tsx +28 -2
  14. package/src/chat/input/imageRefs.ts +30 -0
  15. package/src/chat/input/textCursor.ts +13 -3
  16. package/src/chat/transcript/TranscriptView.tsx +7 -5
  17. package/src/chat/transcript/transcriptViewport.ts +88 -17
  18. package/src/chat/views/PermissionPrompt.tsx +26 -26
  19. package/src/chat/views/PermissionsView.tsx +18 -12
  20. package/src/chat/views/ResumeView.tsx +16 -7
  21. package/src/chat/views/RewindView.tsx +3 -1
  22. package/src/cli/ResetConfirmView.tsx +24 -9
  23. package/src/identity/continuity/editor.ts +27 -2
  24. package/src/identity/continuity/envelope.ts +125 -0
  25. package/src/identity/continuity/publicSkills.ts +37 -1
  26. package/src/identity/continuity/skills/frontmatter.ts +183 -0
  27. package/src/identity/continuity/skills/loadSkills.ts +609 -0
  28. package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
  29. package/src/identity/continuity/skills/scaffold.ts +52 -0
  30. package/src/identity/continuity/skills/types.ts +30 -0
  31. package/src/identity/continuity/storage/defaults.ts +28 -47
  32. package/src/identity/continuity/storage/files.ts +1 -0
  33. package/src/identity/continuity/storage/paths.ts +1 -0
  34. package/src/identity/continuity/storage/scaffold.ts +25 -23
  35. package/src/identity/continuity/storage/status.ts +34 -5
  36. package/src/identity/continuity/storage/types.ts +3 -2
  37. package/src/identity/continuity/storage.ts +3 -0
  38. package/src/identity/hub/OperationalRoutes.tsx +105 -3
  39. package/src/identity/hub/Routes.tsx +5 -3
  40. package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
  41. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
  42. package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
  43. package/src/identity/hub/continuity/effects.ts +36 -5
  44. package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
  45. package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
  46. package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
  47. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
  48. package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
  49. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
  50. package/src/identity/hub/continuity/snapshot.ts +3 -0
  51. package/src/identity/hub/continuity/state.ts +3 -2
  52. package/src/identity/hub/continuity/vault.ts +42 -10
  53. package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
  54. package/src/identity/hub/identityHubReducer.ts +21 -0
  55. package/src/identity/hub/profile/effects.ts +16 -3
  56. package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
  57. package/src/identity/hub/restore/apply.ts +12 -1
  58. package/src/identity/hub/restore/recovery.ts +11 -1
  59. package/src/identity/hub/restore/resolve.ts +1 -1
  60. package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
  61. package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
  62. package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
  63. package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
  64. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
  65. package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
  66. package/src/identity/hub/shared/effects/sync.ts +16 -3
  67. package/src/identity/hub/shared/model/copy.ts +2 -4
  68. package/src/identity/hub/transfer/effects.ts +15 -2
  69. package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
  70. package/src/identity/hub/useIdentityHubController.ts +5 -1
  71. package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
  72. package/src/mcp/manager.ts +1 -1
  73. package/src/models/ModelPicker.tsx +211 -74
  74. package/src/models/huggingface.ts +180 -2
  75. package/src/models/llamacpp.ts +261 -17
  76. package/src/models/llamacppPreflight.ts +16 -12
  77. package/src/models/modelPickerOptions.ts +57 -38
  78. package/src/providers/anthropic.ts +36 -5
  79. package/src/providers/contracts.ts +10 -1
  80. package/src/providers/gemini.ts +29 -3
  81. package/src/providers/openai-chat.ts +131 -11
  82. package/src/providers/openai-responses-format.ts +29 -8
  83. package/src/providers/openai-responses.ts +41 -11
  84. package/src/providers/registry.ts +1 -0
  85. package/src/runtime/toolExecution.ts +4 -3
  86. package/src/runtime/turn.ts +61 -30
  87. package/src/storage/config.ts +1 -0
  88. package/src/storage/sessions.ts +14 -2
  89. package/src/tools/changeDirectoryTool.ts +1 -1
  90. package/src/tools/contracts.ts +10 -0
  91. package/src/tools/deleteFileTool.ts +1 -1
  92. package/src/tools/editTool.ts +1 -1
  93. package/src/tools/listDirectoryTool.ts +1 -1
  94. package/src/tools/listSkillFilesTool.ts +77 -0
  95. package/src/tools/listSkillsTool.ts +68 -0
  96. package/src/tools/mcpResourceTools.ts +2 -2
  97. package/src/tools/privateContinuityReadTool.ts +1 -1
  98. package/src/tools/readSkillTool.ts +107 -0
  99. package/src/tools/readTool.ts +1 -1
  100. package/src/tools/registry.ts +6 -0
  101. package/src/tools/writeFileTool.ts +22 -2
  102. package/src/ui/Spinner.tsx +15 -3
  103. package/src/ui/theme.ts +2 -0
  104. package/src/utils/images.ts +140 -0
  105. package/src/utils/messages.ts +2 -0
  106. package/src/identity/continuity/localBackup.ts +0 -249
  107. package/src/identity/continuity/zipWriter.ts +0 -95
  108. package/src/identity/hub/continuity/index.ts +0 -7
  109. package/src/identity/hub/ens/index.ts +0 -11
  110. package/src/identity/hub/restore/index.ts +0 -22
@@ -6,7 +6,13 @@ import {
6
6
  restoreContinuitySnapshotEnvelope,
7
7
  transferSnapshotMetadataFromEnvelope,
8
8
  } from '../../continuity/envelope.js'
9
- import { ensureIdentityMarkdownScaffold, localContinuitySnapshotContentHashes, writeContinuityFiles } from '../../continuity/storage.js'
9
+ import {
10
+ ensureIdentityMarkdownScaffold,
11
+ localContinuitySnapshotContentHashes,
12
+ restoreSkillsTree,
13
+ writeContinuityFiles,
14
+ } from '../../continuity/storage.js'
15
+ import { syncPublicSkillsManifest } from '../../continuity/skills/publicSkillsSync.js'
10
16
  import { recordPublishedContinuitySnapshot, updatePublishedContinuitySnapshotContentHashes } from '../../continuity/snapshots.js'
11
17
  import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../../storage/ipfs.js'
12
18
  import {
@@ -123,9 +129,13 @@ export async function runRecoveryRefetch(
123
129
  } : {}),
124
130
  }
125
131
  await writeContinuityFiles(nextIdentity, payload.files)
132
+ if (payload.skills) {
133
+ await restoreSkillsTree(nextIdentity, payload.skills)
134
+ }
126
135
  callbacks.onRestoreProgress?.({ phase: 'finishing', label: 'finalizing refreshed identity...' })
127
136
  const publicSkillsRestored = await restorePublishedPublicSkills(nextIdentity, apiUrl, candidate.publicDiscovery?.skillsCid)
128
137
  await ensureIdentityMarkdownScaffold(nextIdentity)
138
+ await syncPublicSkillsManifest(nextIdentity).catch(() => null)
129
139
  await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'Refetched Latest Snapshot From Chain' }).catch(() => null)
130
140
  if (publicSkillsRestored) {
131
141
  const contentHashes = await localContinuitySnapshotContentHashes(nextIdentity)
@@ -29,7 +29,7 @@ export async function resolveAgentEnsToCandidate(
29
29
  return { ok: false, message: `Could not reach Ethereum mainnet to resolve ${trimmed}: ${err instanceof Error ? err.message : String(err)}` }
30
30
  }
31
31
  const tokenValue = records[AGENT_RECORD_KEYS.token]
32
- if (!tokenValue) return { ok: false, message: `${trimmed} has no org.ethagent.token record. Use the full agent subdomain (e.g. agent.${trimmed}).` }
32
+ if (!tokenValue) return { ok: false, message: `${trimmed} has no org.ethagent.token record.` }
33
33
  const tokenRef = parseAgentTokenReference(tokenValue)
34
34
  if (!tokenRef) return { ok: false, message: `${trimmed}'s org.ethagent.token record is not a valid eip155 reference.` }
35
35
  if (tokenRef.chainId !== registry.chainId) {
@@ -1,11 +1,9 @@
1
1
  import { useEffect } from 'react'
2
2
  import type { EthagentConfig } from '../../../storage/config.js'
3
- import {
4
- runRestoreAuthorize,
5
- runRestoreConnectWallet,
6
- runRestoreDiscover,
7
- runRestoreFetch,
8
- } from './index.js'
3
+ import { runRestoreAuthorize } from './apply.js'
4
+ import { runRestoreConnectWallet } from './auth.js'
5
+ import { runRestoreDiscover } from './discover.js'
6
+ import { runRestoreFetch } from './fetch.js'
9
7
  import type { EffectCallbacks } from '../shared/effects/types.js'
10
8
  import type { Step } from '../identityHubReducer.js'
11
9
 
@@ -6,12 +6,14 @@ import { Select, type SelectOption } from '../../../../ui/Select.js'
6
6
  import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
7
7
  import { copyableIdentityFields, identityValuesCopyHint } from '../model/copy.js'
8
8
  import { IdentitySummary } from './IdentitySummary.js'
9
+ import type { ContinuityWorkingTreeStatus } from '../../../continuity/storage.js'
9
10
 
10
11
  type CopyAction = `copy:${string}` | 'back'
11
12
 
12
13
  type DetailsScreenProps = {
13
14
  identity?: EthagentIdentity
14
15
  config?: EthagentConfig
16
+ workingStatus?: ContinuityWorkingTreeStatus | null
15
17
  copyNotice?: string | null
16
18
  unlinked?: boolean
17
19
  onchainOwner?: string
@@ -23,6 +25,7 @@ type DetailsScreenProps = {
23
25
  export const DetailsScreen: React.FC<DetailsScreenProps> = ({
24
26
  identity,
25
27
  config,
28
+ workingStatus,
26
29
  copyNotice,
27
30
  unlinked,
28
31
  onchainOwner,
@@ -45,7 +48,7 @@ export const DetailsScreen: React.FC<DetailsScreenProps> = ({
45
48
 
46
49
  return (
47
50
  <Surface title="Token Values" subtitle={unlinked ? 'Token no longer linked to this wallet, values retained for reference.' : `${identityValuesCopyHint(identity)}.`} footer={footer}>
48
- <IdentitySummary identity={identity} config={config} onchainOwner={onchainOwner} />
51
+ <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} onchainOwner={onchainOwner} />
49
52
  {copyNotice ? <Text color={theme.accentPeriwinkle} bold>{copyNotice}</Text> : null}
50
53
  <Box marginTop={1}>
51
54
  <Select<CopyAction>
@@ -67,59 +67,67 @@ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, conf
67
67
  <Text color={identity.agentId ? theme.text : theme.dim} bold={Boolean(identity.agentId)}>{tokenLine}</Text>
68
68
  </>
69
69
  )}
70
- <Text>
71
- <Text color={theme.dim}>{'ENS'.padEnd(12)}</Text>
72
- {ensStatus.kind === 'linked'
73
- ? <Text color={theme.accentPeriwinkle}>{ensStatus.name}</Text>
74
- : ensStatus.kind === 'issue'
75
- ? <Text color={theme.accentError}>{ensStatus.name} ({ensValidationReasonText(ensStatus.reason)})</Text>
76
- : <Text color={theme.dim}>Not Linked</Text>}
77
- </Text>
78
- {tokenLinked ? (
79
- <Text>
80
- <Text color={theme.dim}>{'Custody'.padEnd(12)}</Text>
81
- <Text color={custodyMode ? theme.text : theme.dim}>{displayCustodyMode(custodyMode)}</Text>
82
- </Text>
83
- ) : null}
84
- {ownerAddress ? (
85
- <Text>
86
- <Text color={theme.dim}>{'Owner'.padEnd(12)}</Text>
87
- <Text color={theme.text}>{shortAddress(ownerAddress)}</Text>
88
- </Text>
89
- ) : null}
70
+ <SummaryRow
71
+ left={{
72
+ label: 'ENS',
73
+ value: ensStatus.kind === 'linked'
74
+ ? <Text color={theme.accentPeriwinkle}>{ensStatus.name}</Text>
75
+ : ensStatus.kind === 'issue'
76
+ ? <Text color={theme.accentError}>{ensStatus.name} ({ensValidationReasonText(ensStatus.reason)})</Text>
77
+ : <Text color={theme.dim}>Not Linked</Text>,
78
+ }}
79
+ right={tokenLinked
80
+ ? {
81
+ label: 'Custody',
82
+ value: <Text color={custodyMode ? theme.text : theme.dim}>{displayCustodyMode(custodyMode)}</Text>,
83
+ }
84
+ : undefined}
85
+ />
90
86
  {(() => {
91
- if (custodyMode !== 'advanced') return null
92
- const vaultAddress = readIdentityStateString(identity.state, 'operatorVaultAddress')
93
- if (!vaultAddress) return null
87
+ const vaultAddress = custodyMode === 'advanced'
88
+ ? readIdentityStateString(identity.state, 'operatorVaultAddress')
89
+ : undefined
90
+ const pairedOperatorsValue = custodyMode === 'advanced' && tokenLinked
91
+ ? approvedOperatorCount > 1
92
+ ? <Text color={theme.text}>{`${approvedOperatorCount} authorized`}</Text>
93
+ : activeOperator
94
+ ? <Text color={theme.text}>{shortAddress(activeOperator)}</Text>
95
+ : <Text color={theme.dim}>None Authorized</Text>
96
+ : null
97
+ const lastSavedCell = {
98
+ label: 'Last Saved',
99
+ value: <Text color={lastBackup === 'never' ? theme.dim : theme.text}>{displayValue(lastBackup)}</Text>,
100
+ }
101
+ const pendingCell = {
102
+ label: 'Pending',
103
+ value: <Text color={theme.dim}>local ahead of chain, owner rotates pointer</Text>,
104
+ }
94
105
  return (
95
- <Text>
96
- <Text color={theme.dim}>{'Vault'.padEnd(12)}</Text>
97
- <Text color={theme.text}>{shortAddress(vaultAddress)}</Text>
98
- </Text>
106
+ <>
107
+ {ownerAddress ? (
108
+ <SummaryRow
109
+ left={{
110
+ label: 'Owner',
111
+ value: <Text color={theme.text}>{shortAddress(ownerAddress)}</Text>,
112
+ }}
113
+ {...(pairedOperatorsValue
114
+ ? { right: { label: 'Operators', value: pairedOperatorsValue } }
115
+ : {})}
116
+ />
117
+ ) : null}
118
+ {vaultAddress ? (
119
+ <SummaryRow
120
+ left={{ label: 'Vault', value: <Text color={theme.text}>{shortAddress(vaultAddress)}</Text> }}
121
+ right={hasPendingPublish(identity) ? pendingCell : lastSavedCell}
122
+ />
123
+ ) : (
124
+ hasPendingPublish(identity)
125
+ ? <SummaryRow left={lastSavedCell} right={pendingCell} />
126
+ : <SummaryRow left={lastSavedCell} />
127
+ )}
128
+ </>
99
129
  )
100
130
  })()}
101
- {tokenLinked && custodyMode === 'advanced' ? (
102
- <Text>
103
- <Text color={theme.dim}>{'Operators'.padEnd(12)}</Text>
104
- {approvedOperatorCount > 1 ? (
105
- <Text color={theme.text}>{`${approvedOperatorCount} authorized${activeOperator ? ` (active ${shortAddress(activeOperator)})` : ''}`}</Text>
106
- ) : activeOperator ? (
107
- <Text color={theme.text}>{shortAddress(activeOperator)}</Text>
108
- ) : (
109
- <Text color={theme.dim}>None Authorized</Text>
110
- )}
111
- </Text>
112
- ) : null}
113
- <Text>
114
- <Text color={theme.dim}>{'Last Saved'.padEnd(12)}</Text>
115
- <Text color={lastBackup === 'never' ? theme.dim : theme.text}>{displayValue(lastBackup)}</Text>
116
- </Text>
117
- {hasPendingPublish(identity) ? (
118
- <Text>
119
- <Text color={theme.dim}>{'Pending'.padEnd(12)}</Text>
120
- <Text color={theme.dim}>local snapshot ahead of chain, owner wallet rotates the pointer</Text>
121
- </Text>
122
- ) : null}
123
131
  {transferSnapshot ? (
124
132
  <Box marginTop={1}>
125
133
  <TransferSnapshotStatus status={transferSnapshot} />
@@ -134,6 +142,39 @@ export const IdentitySummary: React.FC<IdentitySummaryProps> = ({ identity, conf
134
142
  )
135
143
  }
136
144
 
145
+ type SummaryCell = { label: string; value: React.ReactNode }
146
+
147
+ const LEFT_LABEL_WIDTH = 12
148
+ const LEFT_VALUE_WIDTH = 30
149
+ const RIGHT_CELL_WIDTH = 36
150
+
151
+ const SummaryRow: React.FC<{ left: SummaryCell; right?: SummaryCell }> = ({ left, right }) => {
152
+ if (!right) {
153
+ return (
154
+ <Text>
155
+ <Text color={theme.dim}>{left.label.padEnd(LEFT_LABEL_WIDTH)}</Text>
156
+ {left.value}
157
+ </Text>
158
+ )
159
+ }
160
+ return (
161
+ <Box flexDirection="row">
162
+ <Box width={LEFT_LABEL_WIDTH + LEFT_VALUE_WIDTH}>
163
+ <Text>
164
+ <Text color={theme.dim}>{left.label.padEnd(LEFT_LABEL_WIDTH)}</Text>
165
+ {left.value}
166
+ </Text>
167
+ </Box>
168
+ <Box width={RIGHT_CELL_WIDTH}>
169
+ <Text>
170
+ <Text color={theme.dim}>{right.label.padEnd(LEFT_LABEL_WIDTH)}</Text>
171
+ {right.value}
172
+ </Text>
173
+ </Box>
174
+ </Box>
175
+ )
176
+ }
177
+
137
178
  const TransferSnapshotStatus: React.FC<{ status: NonNullable<TransferSnapshotView> }> = ({ status }) => {
138
179
  const receiverLabel = status.receiverHandle && status.receiverHandle !== status.receiver
139
180
  ? `${shortAddress(status.receiver)} (${status.receiverHandle})`
@@ -163,10 +204,13 @@ const TransferSnapshotStatus: React.FC<{ status: NonNullable<TransferSnapshotVie
163
204
  const LocalChangeStatusLine: React.FC<{ status: LocalChangeStatusView }> = ({ status }) => {
164
205
  if (status.hasLocalChanges) {
165
206
  return (
166
- <Text color={theme.accentError} bold>
167
- Local changes detected
168
- {status.files.length > 0 ? `: ${status.files.join(', ')}` : ''}
169
- </Text>
207
+ <Box flexDirection="column">
208
+ <Text color={theme.accentError} bold>
209
+ Local changes detected
210
+ {status.files.length > 0 ? `: ${status.files.join(', ')}` : ''}
211
+ </Text>
212
+ <Text color={theme.dim}>Save Snapshot Now to publish.</Text>
213
+ </Box>
170
214
  )
171
215
  }
172
216
 
@@ -28,6 +28,7 @@ type MenuScreenProps = {
28
28
  onEnsName: () => void
29
29
  onWalletSetup: () => void
30
30
  onContinuity: () => void
31
+ onSkillsTree: () => void
31
32
  onIdentityValues: () => void
32
33
  onPrepareTransfer: () => void
33
34
  onStorage: () => void
@@ -40,6 +41,7 @@ type Action =
40
41
  | 'ens-name'
41
42
  | 'wallet-setup'
42
43
  | 'continuity'
44
+ | 'skills-tree'
43
45
  | 'backup'
44
46
  | 'refetch'
45
47
  | 'identity-values'
@@ -66,6 +68,7 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
66
68
  onEnsName,
67
69
  onWalletSetup,
68
70
  onContinuity,
71
+ onSkillsTree,
69
72
  onIdentityValues,
70
73
  onPrepareTransfer,
71
74
  onStorage,
@@ -91,47 +94,46 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
91
94
  : null)
92
95
 
93
96
  const walletSetupBaseHint = custodyMode === 'advanced'
94
- ? 'Advanced. Owner wallet, Vault, authorized operator wallets'
95
- : 'Simple. Switch to Advanced to delegate URI rotation through a dedicated Vault'
97
+ ? 'Owner wallet, vault, operators'
98
+ : 'Simple mode, switch for vault delegation'
96
99
 
97
100
  const walletSetupLabel = flags?.custodyAsterisk ? 'Custody Mode *' : 'Custody Mode'
98
101
  const walletSetupHint = flags?.custodyModeReason ?? flags?.custodyHint ?? walletSetupBaseHint
99
102
 
100
- const saveSnapshotLabel = flags?.saveSnapshotAsterisk ? 'Save Snapshot Now *' : 'Save Snapshot Now'
101
- const saveSnapshotHint = flags?.saveSnapshotHint ?? 'Encrypt and publish latest snapshot'
103
+ const saveSnapshotLabel = flags?.saveSnapshotAsterisk ? 'Save Snapshot *' : 'Save Snapshot'
104
+ const saveSnapshotHint = flags?.saveSnapshotHint ?? 'Publish encrypted snapshot'
102
105
 
103
- const ensNameHint = flags?.ensNameReason ?? 'Public name or subdomain for this agent'
106
+ const ensNameHint = flags?.ensNameReason ?? 'Public name or subdomain'
104
107
 
105
- const prepareTransferHint = flags?.prepareTransferReason ?? 'Create transfer snapshot and handoff slots'
108
+ const prepareTransferHint = flags?.prepareTransferReason ?? 'Hand off this agent'
106
109
 
107
110
  const tokenValuesHint = flags?.tokenValuesUnlinkedNote ?? identityValuesCopyHint(identity)
108
111
 
109
112
  const options: Array<SelectOption<Action>> = identity
110
113
  ? [
111
114
  { value: 'public-profile', role: 'section', label: 'Public Identity' },
112
- { value: 'public-profile', label: 'Public Profile', hint: 'Name, description, icon, and Agent Card' },
115
+ { value: 'public-profile', label: 'Public Profile', hint: 'Identity card and profile fields' },
113
116
  { value: 'ens-name', label: 'ENS Name', hint: ensNameHint, disabled: flags?.ensNameDisabled ?? false },
114
117
  { value: 'continuity', role: 'section', label: 'Continuity' },
115
- { value: 'continuity', label: 'Soul, Memory, Skills', hint: 'SOUL.md, MEMORY.md, skills.json on this device' },
118
+ { value: 'continuity', label: 'Soul & Memory', hint: 'Edit SOUL.md and MEMORY.md' },
119
+ { value: 'skills-tree', label: 'Skills', hint: 'Browse and edit SKILL.md files' },
116
120
  { value: 'backup', label: saveSnapshotLabel, hint: saveSnapshotHint, disabled: !canRebackup || (flags?.saveSnapshotDisabled ?? false) },
117
- { value: 'refetch', label: 'Refetch Latest', hint: 'Restore local files from latest saved snapshot', disabled: !canRefetch || (flags?.refetchLatestDisabled ?? false) },
121
+ { value: 'refetch', label: 'Refetch Snapshot', hint: 'Restore latest snapshot', disabled: !canRefetch || (flags?.refetchLatestDisabled ?? false) },
118
122
  { value: 'wallet-setup', role: 'section', label: 'Custody' },
119
123
  { value: 'wallet-setup', label: walletSetupLabel, hint: walletSetupHint, disabled: !identity.agentId || (flags?.custodyModeDisabled ?? false) },
120
124
  { value: 'prepare-transfer', label: 'Prepare Transfer', hint: prepareTransferHint, disabled: flags?.prepareTransferDisabled ?? false },
121
125
  { value: 'identity-values', role: 'section', label: 'Token' },
122
126
  { value: 'identity-values', label: 'Token Values', hint: tokenValuesHint },
123
- { value: 'load', label: 'Load Agent', hint: 'Refresh this agent from chain, or load a different one' },
124
- { value: 'create', label: 'New Agent', hint: 'Mint another token and make it active' },
125
- { value: 'storage', role: 'section', label: 'Storage' },
126
- { value: 'storage', label: 'IPFS Storage', hint: 'Publishing credentials for encrypted snapshots' },
127
+ { value: 'load', label: 'Load Agent', hint: 'Refresh or load another agent' },
128
+ { value: 'create', label: 'New Agent', hint: 'Mint another agent' },
129
+ { value: 'storage', label: 'IPFS Storage', hint: 'Publishing credentials' },
127
130
  { value: 'cancel', role: 'section', label: 'Exit' },
128
- { value: 'cancel', label: 'Close Identity Hub', hint: 'Return to chat without changing identity', role: 'utility' },
131
+ { value: 'cancel', label: 'Close Identity Hub', hint: 'Return to chat', role: 'utility' },
129
132
  ]
130
133
  : [
131
134
  { value: 'create', role: 'section', label: 'Setup' },
132
135
  { value: 'create', label: 'Create New Agent', hint: 'Mint a wallet-owned token for this machine' },
133
136
  { value: 'load', label: 'Load Existing Agent', hint: 'Find a token owned by this wallet or linked to it' },
134
- { value: mode === 'first-run' ? 'skip' : 'cancel', role: 'section', label: 'Exit' },
135
137
  ...(mode === 'first-run'
136
138
  ? [
137
139
  { value: 'skip' as Action, label: 'Skip For Now', hint: 'Continue now, use /identity later', role: 'utility' as const },
@@ -170,6 +172,7 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
170
172
  if (choice === 'ens-name') return onEnsName()
171
173
  if (choice === 'wallet-setup') return onWalletSetup()
172
174
  if (choice === 'continuity') return onContinuity()
175
+ if (choice === 'skills-tree') return onSkillsTree()
173
176
  if (choice === 'backup') return onBackupNow()
174
177
  if (choice === 'refetch') return onRefetchLatest()
175
178
  if (choice === 'identity-values') return onIdentityValues()
@@ -43,7 +43,7 @@ export const UnlinkedIdentityScreen: React.FC<UnlinkedIdentityScreenProps> = ({
43
43
  <Surface
44
44
  title="No Linked Agent"
45
45
  subtitle="The agent token recorded locally is not currently owned by your wallet."
46
- footer={<Text color={theme.dim}>enter selects, esc back</Text>}
46
+ footer={<Text color={theme.dim}>enter selects · esc back</Text>}
47
47
  >
48
48
  <Box flexDirection="column">
49
49
  {transferSnapshot ? (
@@ -25,9 +25,9 @@ export function menuFlagsFromReconciliation(r: AgentReconciliation, perspective:
25
25
 
26
26
  let prepareTransferReason: string | undefined
27
27
  if (isOperator) {
28
- prepareTransferReason = 'Operators cannot transfer the token.'
28
+ prepareTransferReason = 'Owner-only action'
29
29
  } else if (!unlinked && (r.custody === 'advanced' || r.custody === 'mid-flow-uri-pending')) {
30
- prepareTransferReason = 'Token is in the vault. Withdraw it first in Custody Mode.'
30
+ prepareTransferReason = 'Withdraw from vault first'
31
31
  }
32
32
 
33
33
  const custodyAsterisk = r.custody === 'mid-flow-uri-pending' || r.vault === 'missing'
@@ -35,17 +35,13 @@ export function menuFlagsFromReconciliation(r: AgentReconciliation, perspective:
35
35
  if (isOperator) {
36
36
  custodyHint = undefined
37
37
  } else if (r.custody === 'mid-flow-uri-pending') {
38
- custodyHint = 'Advanced setup pending. Open to finish.'
38
+ custodyHint = 'Setup pending, open to finish'
39
39
  } else if (r.vault === 'missing') {
40
- custodyHint = 'Vault contract not found. Open to redeploy.'
40
+ custodyHint = 'Vault missing, open to redeploy'
41
41
  }
42
42
 
43
- const custodyModeReason = isOperator
44
- ? 'Operators cannot change custody, withdraw the token, or manage operators.'
45
- : undefined
46
- const ensNameReason = isOperator
47
- ? 'Operators cannot change the ENS subdomain or its records.'
48
- : undefined
43
+ const custodyModeReason = isOperator ? 'Owner-only action' : undefined
44
+ const ensNameReason = isOperator ? 'Owner-only action' : undefined
49
45
 
50
46
  return {
51
47
  prepareTransferDisabled: unlinked || inVault || isOperator,
@@ -56,11 +52,11 @@ export function menuFlagsFromReconciliation(r: AgentReconciliation, perspective:
56
52
  ...(ensNameReason ? { ensNameReason } : {}),
57
53
  saveSnapshotDisabled: unlinked,
58
54
  refetchLatestDisabled: unlinked,
59
- ...(unlinked ? { tokenValuesUnlinkedNote: 'Token no longer linked to this wallet, values retained for reference' } : {}),
55
+ ...(unlinked ? { tokenValuesUnlinkedNote: 'Unlinked, retained for reference' } : {}),
60
56
 
61
57
  custodyAsterisk: custodyAsterisk && !isOperator,
62
58
  ...(custodyHint ? { custodyHint } : {}),
63
59
  saveSnapshotAsterisk: r.agentUri === 'local-newer',
64
- ...(r.agentUri === 'local-newer' ? { saveSnapshotHint: 'Local state newer than chain. Save to publish the latest agentURI.' } : {}),
60
+ ...(r.agentUri === 'local-newer' ? { saveSnapshotHint: 'Local newer than chain' } : {}),
65
61
  }
66
62
  }
@@ -16,7 +16,11 @@ import {
16
16
  } from '../reconciliation/index.js'
17
17
  import { normalizeApprovedOperatorWallets } from '../operatorWallets.js'
18
18
  import { readOwnerAddressField } from '../../../identityCompat.js'
19
- import { localContinuitySnapshotContentHashes } from '../../../continuity/storage.js'
19
+ import {
20
+ continuitySnapshotContentHashesFromSources,
21
+ localContinuitySnapshotContentHashes,
22
+ } from '../../../continuity/storage.js'
23
+ import type { ContinuityFiles, ContinuitySkillsTree } from '../../../continuity/envelope.js'
20
24
  import { updatePublishedContinuitySnapshotContentHashes } from '../../../continuity/snapshots.js'
21
25
  import type { EffectCallbacks } from './types.js'
22
26
  import { awaitConfirmedReceipt } from './receipts.js'
@@ -141,9 +145,18 @@ export async function syncVaultMetadataOperatorsAfterOwnerSave(args: {
141
145
  }
142
146
  }
143
147
 
144
- export async function markCurrentContinuityFilesPublished(identity: EthagentIdentity): Promise<void> {
148
+ export async function markCurrentContinuityFilesPublished(
149
+ identity: EthagentIdentity,
150
+ publishedSources?: {
151
+ privateFiles: ContinuityFiles
152
+ publicSkills: string
153
+ skills: ContinuitySkillsTree
154
+ },
155
+ ): Promise<void> {
145
156
  const cid = identity.backup?.cid
146
157
  if (!cid) return
147
- const contentHashes = await localContinuitySnapshotContentHashes(identity)
158
+ const contentHashes = publishedSources
159
+ ? continuitySnapshotContentHashesFromSources(publishedSources)
160
+ : await localContinuitySnapshotContentHashes(identity)
148
161
  await updatePublishedContinuitySnapshotContentHashes(identity, cid, contentHashes).catch(() => null)
149
162
  }
@@ -28,8 +28,6 @@ export function copyableIdentityFields(identity?: EthagentIdentity, config?: Eth
28
28
  return fields
29
29
  }
30
30
 
31
- export function identityValuesCopyHint(identity?: EthagentIdentity): string {
32
- return readIdentityStateString(identity?.state, 'ensName')
33
- ? 'Copy token, ENS, and token URI pointers'
34
- : 'Copy token and token URI pointers'
31
+ export function identityValuesCopyHint(_identity?: EthagentIdentity): string {
32
+ return 'Copy token and pointers'
35
33
  }
@@ -9,15 +9,21 @@ import {
9
9
  import {
10
10
  continuityAgentSnapshot,
11
11
  continuityVaultStatus,
12
+ prepareSyncedSkillsTree,
12
13
  readContinuityFiles,
13
14
  readPublicSkillsFile,
14
15
  writePublicSkillsFile,
15
16
  } from '../../continuity/storage.js'
16
17
  import {
18
+ appendPublicSkillEntries,
17
19
  createAgentCard,
18
20
  defaultPublicSkillsProfile,
19
21
  serializeAgentCard,
20
22
  } from '../../continuity/publicSkills.js'
23
+ import {
24
+ derivePublicSkillEntries,
25
+ syncPublicSkillsManifest,
26
+ } from '../../continuity/skills/publicSkillsSync.js'
21
27
  import { recordPublishedContinuitySnapshot } from '../../continuity/snapshots.js'
22
28
  import { addToIpfs, DEFAULT_IPFS_API_URL, isPinataUploadUrl } from '../../storage/ipfs.js'
23
29
  import {
@@ -161,16 +167,22 @@ export async function runTokenTransferSigning(
161
167
  }
162
168
  const nextIdentityForFiles: EthagentIdentity = { ...step.identity, state }
163
169
  const continuityFiles = await readContinuityFiles(nextIdentityForFiles)
164
- const publicSkillsJson = await readPublicSkillsFile(nextIdentityForFiles)
170
+ const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
165
171
  const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
166
172
  assertVerifiedPin(publicSkillsPin)
173
+ const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
174
+ const augmentedPublicProfile = appendPublicSkillEntries(
175
+ defaultPublicSkillsProfile(nextIdentityForFiles),
176
+ publicSkillEntries,
177
+ )
167
178
  const agentCardPin = await addToIpfs(
168
179
  DEFAULT_IPFS_API_URL,
169
- serializeAgentCard(createAgentCard(defaultPublicSkillsProfile(nextIdentityForFiles))),
180
+ serializeAgentCard(createAgentCard(augmentedPublicProfile)),
170
181
  fetch,
171
182
  { pinataJwt: step.pinataJwt },
172
183
  )
173
184
  assertVerifiedPin(agentCardPin)
185
+ const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
174
186
  const envelope = createTransferContinuitySnapshotEnvelope({
175
187
  ownerAddress,
176
188
  ownerWalletSignature: senderSignature.signature,
@@ -181,6 +193,7 @@ export async function runTokenTransferSigning(
181
193
  payload: {
182
194
  agent: continuityAgentSnapshot(nextIdentityForFiles),
183
195
  files: continuityFiles,
196
+ ...(Object.keys(skillsTree).length > 0 ? { skills: skillsTree } : {}),
184
197
  transcript: [],
185
198
  state,
186
199
  },