ethagent 2.4.0 → 3.0.1

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 (103) 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 +134 -9
  23. package/src/identity/continuity/publicSkills.ts +54 -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 +79 -5
  37. package/src/identity/hub/Routes.tsx +5 -3
  38. package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +7 -73
  39. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +6 -6
  40. package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -2
  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/NewSkillScreen.tsx +57 -0
  44. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
  45. package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +151 -0
  46. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +181 -0
  47. package/src/identity/hub/continuity/snapshot.ts +3 -0
  48. package/src/identity/hub/continuity/state.ts +9 -8
  49. package/src/identity/hub/continuity/vault.ts +42 -10
  50. package/src/identity/hub/create/CreateFlow.tsx +1 -1
  51. package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
  52. package/src/identity/hub/custody/routes.tsx +1 -1
  53. package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +0 -1
  54. package/src/identity/hub/ens/EnsEditMaintenanceScreens.tsx +0 -1
  55. package/src/identity/hub/identityHubReducer.ts +15 -0
  56. package/src/identity/hub/profile/EditProfileFlow.tsx +5 -5
  57. package/src/identity/hub/profile/effects.ts +16 -3
  58. package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
  59. package/src/identity/hub/restore/apply.ts +12 -1
  60. package/src/identity/hub/restore/recovery.ts +14 -4
  61. package/src/identity/hub/restore/resolve.ts +1 -1
  62. package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
  63. package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
  64. package/src/identity/hub/shared/components/IdentitySummary.tsx +118 -54
  65. package/src/identity/hub/shared/components/MenuScreen.tsx +21 -18
  66. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +4 -4
  67. package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
  68. package/src/identity/hub/shared/effects/sync.ts +16 -3
  69. package/src/identity/hub/shared/model/copy.ts +2 -4
  70. package/src/identity/hub/transfer/effects.ts +15 -2
  71. package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
  72. package/src/identity/hub/useIdentityHubController.ts +5 -1
  73. package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
  74. package/src/identity/wallet/page/copy.ts +43 -43
  75. package/src/mcp/manager.ts +1 -1
  76. package/src/models/ModelPicker.tsx +89 -84
  77. package/src/models/llamacpp.ts +160 -11
  78. package/src/models/llamacppPreflight.ts +1 -16
  79. package/src/models/modelPickerOptions.ts +45 -37
  80. package/src/providers/contracts.ts +1 -0
  81. package/src/providers/openai-chat.ts +50 -9
  82. package/src/providers/openai-responses.ts +19 -4
  83. package/src/runtime/toolExecution.ts +4 -3
  84. package/src/runtime/turn.ts +61 -30
  85. package/src/tools/changeDirectoryTool.ts +1 -1
  86. package/src/tools/contracts.ts +10 -0
  87. package/src/tools/deleteFileTool.ts +1 -1
  88. package/src/tools/editTool.ts +1 -1
  89. package/src/tools/listDirectoryTool.ts +1 -1
  90. package/src/tools/listSkillFilesTool.ts +77 -0
  91. package/src/tools/listSkillsTool.ts +68 -0
  92. package/src/tools/mcpResourceTools.ts +2 -2
  93. package/src/tools/privateContinuityReadTool.ts +1 -1
  94. package/src/tools/readSkillTool.ts +107 -0
  95. package/src/tools/readTool.ts +1 -1
  96. package/src/tools/registry.ts +6 -0
  97. package/src/tools/writeFileTool.ts +22 -2
  98. package/src/ui/Spinner.tsx +1 -1
  99. package/src/identity/continuity/localBackup.ts +0 -249
  100. package/src/identity/continuity/zipWriter.ts +0 -95
  101. package/src/identity/hub/continuity/index.ts +0 -7
  102. package/src/identity/hub/ens/index.ts +0 -11
  103. package/src/identity/hub/restore/index.ts +0 -22
@@ -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 onchain' } : {}),
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
  },
@@ -2,15 +2,23 @@ import { useEffect, useState } from 'react'
2
2
  import type { EthagentIdentity } from '../../storage/config.js'
3
3
  import { catFromIpfs, DEFAULT_IPFS_API_URL } from '../storage/ipfs.js'
4
4
  import {
5
- continuityVaultRef,
6
5
  continuityVaultStatus,
7
6
  continuityWorkingTreeStatus,
8
7
  ensurePublicSkillsFile,
9
8
  type ContinuityWorkingTreeStatus,
10
9
  } from '../continuity/storage.js'
11
- import { openFileInEditor } from '../continuity/editor.js'
12
- import { exportLocalBackup } from '../continuity/localBackup.js'
10
+ import { openFileInEditor, openInFileManager } from '../continuity/editor.js'
13
11
  import { listPublishedContinuitySnapshots } from '../continuity/snapshots.js'
12
+ import {
13
+ createSkillFile,
14
+ deleteSkillEntry,
15
+ invalidateSkillsCache,
16
+ readSkillByRelativePath,
17
+ setSkillVisibility as setSkillVisibilityStorage,
18
+ } from '../continuity/skills/loadSkills.js'
19
+ import type { SkillVisibility } from '../continuity/skills/types.js'
20
+ import { syncPublicSkillsManifest } from '../continuity/skills/publicSkillsSync.js'
21
+ import { continuityVaultRef } from '../continuity/storage.js'
14
22
  import type { Step } from './identityHubReducer.js'
15
23
 
16
24
  type UseIdentityHubContinuityArgs = {
@@ -30,7 +38,11 @@ export function useIdentityHubContinuity({
30
38
  setContinuityReady: (ready: boolean) => void
31
39
  workingStatus: ContinuityWorkingTreeStatus | null
32
40
  openContinuityFile: (kind: 'soul' | 'memory' | 'skills') => Promise<void>
33
- exportLocalBackupZip: () => Promise<void>
41
+ openSkillFile: (relativePath: string) => Promise<void>
42
+ openSkillsFolder: () => Promise<void>
43
+ createSkill: (name: string, visibility: SkillVisibility) => Promise<void>
44
+ deleteSkill: (relativePath: string) => Promise<void>
45
+ setSkillVisibility: (relativePath: string, visibility: SkillVisibility) => Promise<void>
34
46
  } {
35
47
  const [continuityReady, setContinuityReady] = useState<boolean>(false)
36
48
  const [workingStatus, setWorkingStatus] = useState<ContinuityWorkingTreeStatus | null>(null)
@@ -55,6 +67,7 @@ export function useIdentityHubContinuity({
55
67
  step.kind !== 'menu'
56
68
  && step.kind !== 'continuity-private'
57
69
  && step.kind !== 'continuity-public'
70
+ && step.kind !== 'continuity-skills-tree'
58
71
  && step.kind !== 'save-prompt'
59
72
  && step.kind !== 'rebackup-confirm'
60
73
  ) return
@@ -78,8 +91,39 @@ export function useIdentityHubContinuity({
78
91
  }
79
92
  }, [identity, step.kind])
80
93
 
94
+ const requireReadyVault = async (): Promise<EthagentIdentity> => {
95
+ if (!identity) throw new Error('No active identity')
96
+ const status = await continuityVaultStatus(identity)
97
+ if (!status.ready) {
98
+ throw new Error('Restore local continuity files before editing the skills tree')
99
+ }
100
+ return identity
101
+ }
102
+
103
+ const mutateSkillsTree = async (args: {
104
+ backStep: Step
105
+ run: (id: EthagentIdentity) => Promise<string>
106
+ successStep?: (notice: string) => Step
107
+ }): Promise<void> => {
108
+ try {
109
+ const id = await requireReadyVault()
110
+ const notice = await args.run(id)
111
+ invalidateSkillsCache(id)
112
+ await syncPublicSkillsManifest(id)
113
+ const next = args.successStep
114
+ ? args.successStep(notice)
115
+ : { kind: 'continuity-skills-tree' as const, notice }
116
+ setStep(next)
117
+ } catch (err: unknown) {
118
+ handleStepError(err, args.backStep)
119
+ }
120
+ }
121
+
81
122
  const openContinuityFile = async (kind: 'soul' | 'memory' | 'skills'): Promise<void> => {
82
123
  if (!identity) return
124
+ const returnKind: 'continuity-private' | 'continuity-skills-tree' = kind === 'skills'
125
+ ? 'continuity-skills-tree'
126
+ : 'continuity-private'
83
127
  try {
84
128
  if (kind === 'skills') {
85
129
  await ensurePublicSkillsFile(identity, {
@@ -89,43 +133,121 @@ export function useIdentityHubContinuity({
89
133
  const ref = continuityVaultRef(identity)
90
134
  const file = kind === 'soul' ? ref.soulPath : kind === 'memory' ? ref.memoryPath : ref.publicSkillsPath
91
135
  const result = await openFileInEditor(file)
92
- const displayName = kind === 'soul' ? 'SOUL.md' : kind === 'memory' ? 'MEMORY.md' : 'skills.json'
93
- const message = result.ok
94
- ? `opened ${displayName} with ${result.method}.`
95
- : `open failed: ${result.error}`
96
- setStep({ kind: 'continuity-private', notice: message, editorOpened: result.ok })
136
+ if (result.ok) {
137
+ setStep({ kind: returnKind, editorOpened: true })
138
+ } else {
139
+ setStep({ kind: returnKind, notice: `open failed: ${result.error}`, editorOpened: false })
140
+ }
97
141
  } catch (err: unknown) {
98
- handleStepError(err, { kind: 'continuity-private' })
142
+ handleStepError(err, { kind: returnKind })
99
143
  }
100
144
  }
101
145
 
102
- const exportLocalBackupZip = async (): Promise<void> => {
146
+ const openSkillFile = async (relativePath: string): Promise<void> => {
103
147
  if (!identity) return
104
148
  try {
105
- await ensurePublicSkillsFile(identity, {
106
- fallback: () => readPublishedPublicSkills(identity),
107
- })
108
- const result = await exportLocalBackup(identity)
109
- const message = result.ok
110
- ? `Saved local backup to ${result.path}`
111
- : result.cancelled
112
- ? 'Backup cancelled'
113
- : `Backup failed: ${result.error}`
114
- setStep({ kind: 'continuity-private', notice: message })
149
+ const skill = await readSkillByRelativePath(identity, relativePath)
150
+ const result = await openFileInEditor(skill.absolutePath)
151
+ invalidateSkillsCache(identity)
152
+ try {
153
+ await syncPublicSkillsManifest(identity)
154
+ } catch (syncErr: unknown) {
155
+ const failPrefix = result.ok ? '' : `open failed: ${result.error}; `
156
+ setStep({ kind: 'continuity-skills-tree', notice: `${failPrefix}public manifest sync failed: ${(syncErr as Error).message}`, editorOpened: result.ok })
157
+ return
158
+ }
159
+ if (result.ok) {
160
+ setStep({ kind: 'continuity-skills-tree', editorOpened: true })
161
+ } else {
162
+ setStep({ kind: 'continuity-skills-tree', notice: `open failed: ${result.error}`, editorOpened: false })
163
+ }
115
164
  } catch (err: unknown) {
116
- handleStepError(err, { kind: 'continuity-private' })
165
+ handleStepError(err, { kind: 'continuity-skills-tree' })
117
166
  }
118
167
  }
119
168
 
169
+ const openSkillsFolder = async (): Promise<void> => {
170
+ if (!identity) return
171
+ try {
172
+ const ref = continuityVaultRef(identity)
173
+ const result = await openInFileManager(ref.skillsDir)
174
+ if (result.ok) {
175
+ setStep({ kind: 'continuity-skills-tree', editorOpened: true })
176
+ } else {
177
+ setStep({ kind: 'continuity-skills-tree', notice: `open failed: ${result.error}`, editorOpened: false })
178
+ }
179
+ } catch (err: unknown) {
180
+ handleStepError(err, { kind: 'continuity-skills-tree' })
181
+ }
182
+ }
183
+
184
+ const createSkill = async (name: string, visibility: SkillVisibility): Promise<void> => {
185
+ const normalizedName = sanitizeSkillSegment(name)
186
+ if (!normalizedName) {
187
+ handleStepError(
188
+ new Error('Folder name must contain only letters, numbers, dashes, underscores, or dots'),
189
+ { kind: 'continuity-skill-new' },
190
+ )
191
+ return
192
+ }
193
+ try {
194
+ const id = await requireReadyVault()
195
+ const created = await createSkillFile(id, { name: normalizedName, visibility })
196
+ invalidateSkillsCache(id)
197
+ await syncPublicSkillsManifest(id)
198
+ const result = await openFileInEditor(created.absolutePath)
199
+ if (result.ok) {
200
+ setStep({ kind: 'continuity-skills-tree', editorOpened: true })
201
+ } else {
202
+ setStep({ kind: 'continuity-skills-tree', notice: `created ${created.relativePath}; open failed: ${result.error}`, editorOpened: false })
203
+ }
204
+ } catch (err: unknown) {
205
+ handleStepError(err, { kind: 'continuity-skill-new' })
206
+ }
207
+ }
208
+
209
+ const deleteSkill = async (relativePath: string): Promise<void> => {
210
+ await mutateSkillsTree({
211
+ backStep: { kind: 'continuity-skills-tree' },
212
+ run: async id => {
213
+ await deleteSkillEntry(id, relativePath)
214
+ return `deleted ${relativePath}`
215
+ },
216
+ })
217
+ }
218
+
219
+ const setSkillVisibility = async (
220
+ relativePath: string,
221
+ visibility: SkillVisibility,
222
+ ): Promise<void> => {
223
+ await mutateSkillsTree({
224
+ backStep: { kind: 'continuity-skill-actions', relativePath },
225
+ successStep: notice => ({ kind: 'continuity-skill-actions', relativePath, notice }),
226
+ run: async id => {
227
+ await setSkillVisibilityStorage(id, relativePath, visibility)
228
+ const display = relativePath.split('/')[0] ?? relativePath
229
+ return `${display} now ${visibility}`
230
+ },
231
+ })
232
+ }
233
+
120
234
  return {
121
235
  continuityReady,
122
236
  setContinuityReady,
123
237
  workingStatus,
124
238
  openContinuityFile,
125
- exportLocalBackupZip,
239
+ openSkillFile,
240
+ openSkillsFolder,
241
+ createSkill,
242
+ deleteSkill,
243
+ setSkillVisibility,
126
244
  }
127
245
  }
128
246
 
247
+ export function sanitizeSkillSegment(value: string): string {
248
+ return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60)
249
+ }
250
+
129
251
  async function readPublishedPublicSkills(identity: EthagentIdentity): Promise<string> {
130
252
  const cid = identity.publicSkills?.cid
131
253
  if (!cid) throw new Error('No saved public skills CID')
@@ -389,7 +389,11 @@ export function useIdentityHubController({
389
389
  openTokenTransferFlow,
390
390
  openPublicProfileEdit,
391
391
  openContinuityFile: continuity.openContinuityFile,
392
- exportLocalBackupZip: continuity.exportLocalBackupZip,
392
+ openSkillFile: continuity.openSkillFile,
393
+ openSkillsFolder: continuity.openSkillsFolder,
394
+ createSkill: continuity.createSkill,
395
+ deleteSkill: continuity.deleteSkill,
396
+ setSkillVisibility: continuity.setSkillVisibility,
393
397
  }
394
398
  }
395
399
 
@@ -20,10 +20,8 @@ import {
20
20
  runEnsSetupRecordsTransaction,
21
21
  runEnsSetupRegistryTransaction,
22
22
  runUpdateEnsRecords,
23
- } from './ens/index.js'
24
- import {
25
- runRecoveryRefetch,
26
- } from './restore/index.js'
23
+ } from './ens/transactions.js'
24
+ import { runRecoveryRefetch } from './restore/recovery.js'
27
25
  import type { EffectCallbacks } from './shared/effects/types.js'
28
26
  import { useRestoreEffects } from './restore/useRestoreEffects.js'
29
27
  import {