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
@@ -0,0 +1,30 @@
1
+ export type SkillVisibility = 'private' | 'public' | 'discoverable'
2
+
3
+ export type SkillFrontmatter = {
4
+ name?: string
5
+ description?: string
6
+ whenToUse?: string
7
+ version?: string
8
+ argumentHint?: string
9
+ tags?: string[]
10
+ visibility?: SkillVisibility
11
+ }
12
+
13
+ export type SkillIndexEntry = {
14
+ name: string
15
+ displayName?: string
16
+ description: string
17
+ whenToUse?: string
18
+ version?: string
19
+ argumentHint?: string
20
+ tags?: string[]
21
+ visibility: SkillVisibility
22
+ relativePath: string
23
+ absolutePath: string
24
+ }
25
+
26
+ export type Skill = SkillIndexEntry & {
27
+ body: string
28
+ }
29
+
30
+ export type ContinuitySkillsTree = Record<string, string>
@@ -16,9 +16,8 @@ export function continuityAgentSnapshot(identity: EthagentIdentity): ContinuityA
16
16
  }
17
17
  }
18
18
 
19
- export function defaultContinuityFiles(identity: EthagentIdentity, now = new Date()): ContinuityFiles {
19
+ export function defaultContinuityFiles(identity: EthagentIdentity, _now = new Date()): ContinuityFiles {
20
20
  const owner = identity.ownerAddress ?? identity.address
21
- const created = now.toISOString().slice(0, 10)
22
21
  const identityBlock = renderPrivateIdentityBlock({
23
22
  owner,
24
23
  token: identity.agentId ? `#${identity.agentId}` : 'pending registration',
@@ -33,40 +32,29 @@ export function defaultContinuityFiles(identity: EthagentIdentity, now = new Dat
33
32
  '',
34
33
  '## Persona',
35
34
  '',
36
- '- Describe the private agent persona, voice, and collaboration style.',
37
- '- Keep standing behavior that should survive model switches and device restores.',
38
- '- Prefer stable guidance over session-specific preferences.',
35
+ 'The standing voice, collaboration style, and behavior this agent keeps across sessions and models.',
39
36
  '',
40
- '## Operating Principles',
37
+ '- Voice: <e.g., direct, concise, no filler>',
38
+ '- Tone: <e.g., warm but professional>',
39
+ '- Communication style: <e.g., bullet lists for technical work, prose for narrative>',
41
40
  '',
42
- '- Record durable values, decision preferences, and owner-approved working principles.',
43
- '- Keep implementation-specific facts in MEMORY.md unless they define behavior.',
41
+ '## Principles',
44
42
  '',
45
- '## Private Instructions',
43
+ 'Owner-approved working principles. Decisions that should be made the same way every time.',
46
44
  '',
47
- '- Keep owner-specific standing instructions in this file.',
48
- '- Do not share this file directly; save it via the Identity Hub encrypted snapshot.',
49
- '- Public capabilities belong in skills.json.',
45
+ '- <principle: e.g., always confirm before destructive operations>',
46
+ '- <principle: e.g., bias toward editing existing code over creating new files>',
47
+ '- <principle: e.g., never hide errors, always surface them clearly>',
50
48
  '',
51
49
  '## Boundaries',
52
50
  '',
53
- '- Record private behavioral limits and owner-approved constraints here.',
54
- '- Do not store seed phrases, private keys, raw wallet signatures, or API keys.',
55
- '- Do not place public delegation claims here; keep them in skills.json.',
51
+ 'What this agent must never do, regardless of how it is asked.',
56
52
  '',
57
- '## Maintenance Rules',
53
+ '- <boundary: e.g., never bypass authentication or authorization checks>',
54
+ '- <boundary: e.g., never commit secrets to source control>',
55
+ '- Never store seed phrases, private keys, raw wallet signatures, or API keys.',
58
56
  '',
59
- '- Keep the generated Agent Identity block intact; edit owner-authored sections below it.',
60
- '- Do not duplicate the mutable public agent name here; it lives in the token URI and Agent Card.',
61
- '- Move factual project memory to MEMORY.md when it is not persona or instruction material.',
62
- '- Revise or remove stale guidance instead of accumulating contradictions.',
63
- '',
64
- '## Change Notes',
65
- '',
66
- '- Add dated notes when the persona or long-lived private guidance changes.',
67
- '',
68
- `Created: ${created}`,
69
- ].join('\n') + '\n',
57
+ ].join('\n'),
70
58
  'MEMORY.md': [
71
59
  '# MEMORY.md',
72
60
  '',
@@ -74,35 +62,28 @@ export function defaultContinuityFiles(identity: EthagentIdentity, now = new Dat
74
62
  '',
75
63
  '## Durable User Preferences',
76
64
  '',
77
- '- Add long-lived owner preferences that should survive across sessions and model switches.',
78
- '',
79
- '## Project Context',
80
- '',
81
- '- Add stable project facts, repo conventions, and active workstreams.',
65
+ 'Long-lived owner preferences that survive sessions and model switches. Capture how the user works.',
82
66
  '',
83
- '## Decisions and Rationale',
67
+ '- Communication: <e.g., prefers concise summaries over walls of text>',
68
+ '- Tooling: <e.g., VS Code, zsh, TypeScript over JavaScript>',
69
+ '- Workflow: <e.g., reviews PRs in the morning, deploys on Fridays>',
84
70
  '',
85
- '- Record important decisions and why they were made.',
86
- '',
87
- '## Facts to Revalidate',
71
+ '## Project Context',
88
72
  '',
89
- '- Add time-sensitive facts that should be checked before reuse, with dates or source context when available.',
73
+ 'Stable facts about active projects, repos, and conventions. Add dates for time-sensitive notes.',
90
74
  '',
91
- '## Maintenance Rules',
75
+ '### <project-name>',
92
76
  '',
93
- '- Prefer stable facts, preferences, and decisions over chat transcripts.',
94
- '- Do not duplicate the mutable public agent name here; it lives in the token URI and Agent Card.',
95
- '- Add dates or source context when a note may become stale or environment-specific.',
96
- '- Remove or rewrite stale memory instead of accumulating contradictions.',
77
+ '- Repository: <url or local path>',
78
+ '- Stack: <languages, frameworks, key libraries>',
79
+ '- Conventions: <e.g., conventional commits, semver, branch naming>',
80
+ '- Active workstream: <YYYY-MM-DD what you are focused on>',
97
81
  '',
98
82
  '## Boundaries',
99
83
  '',
100
- '- Do not store seed phrases, private keys, raw wallet signatures, or API keys.',
101
- '- Do not store secrets unless the user explicitly asks and the risk is clear.',
102
- '- Keep public capabilities in skills.json.',
84
+ 'Never store seed phrases, private keys, raw wallet signatures, or API keys.',
103
85
  '',
104
- `Created: ${created}`,
105
- ].join('\n') + '\n',
86
+ ].join('\n'),
106
87
  }
107
88
  }
108
89
 
@@ -9,6 +9,7 @@ import type { ContinuityVaultRef } from './types.js'
9
9
  export async function ensureContinuityVault(identity: EthagentIdentity): Promise<ContinuityVaultRef> {
10
10
  const ref = continuityVaultRef(identity)
11
11
  await fs.mkdir(ref.dir, { recursive: true, mode: 0o700 })
12
+ await fs.mkdir(ref.skillsDir, { recursive: true, mode: 0o700 })
12
13
  return ref
13
14
  }
14
15
 
@@ -9,6 +9,7 @@ export function continuityVaultRef(identity: Pick<EthagentIdentity, 'chainId' |
9
9
  soulPath: path.join(dir, 'SOUL.md'),
10
10
  memoryPath: path.join(dir, 'MEMORY.md'),
11
11
  publicSkillsPath: path.join(dir, 'skills.json'),
12
+ skillsDir: path.join(dir, 'skills'),
12
13
  }
13
14
  }
14
15
 
@@ -1,6 +1,14 @@
1
1
  import { atomicWriteText } from '../../../storage/atomicWrite.js'
2
2
  import type { EthagentIdentity } from '../../../storage/config.js'
3
- import type { ContinuityFiles } from '../envelope.js'
3
+ import type { ContinuityFiles, ContinuitySkillsTree } from '../envelope.js'
4
+ import {
5
+ loadSkillsTree,
6
+ materializeSkillsTree,
7
+ } from '../skills/loadSkills.js'
8
+ import {
9
+ renderPublicSkillsJsonForIdentity,
10
+ syncPublicSkillsManifest,
11
+ } from '../skills/publicSkillsSync.js'
4
12
  import { defaultContinuityFiles, defaultPublicSkillsJson } from './defaults.js'
5
13
  import {
6
14
  ensureContinuityFiles,
@@ -20,9 +28,10 @@ export async function ensureIdentityMarkdownScaffold(
20
28
  ): Promise<IdentityMarkdownScaffold> {
21
29
  const privateFiles = await ensureContinuityFiles(identity)
22
30
  const publicSkills = await ensurePublicSkillsFile(identity, { fallback: options.publicSkillsFallback })
31
+ const syncedPublic = await syncPublicSkillsManifest(identity).catch(() => publicSkills)
23
32
  return {
24
33
  ...privateFiles,
25
- 'skills.json': publicSkills,
34
+ 'skills.json': syncedPublic,
26
35
  }
27
36
  }
28
37
 
@@ -47,9 +56,8 @@ export async function syncIdentityMarkdownScaffold(identity: EthagentIdentity):
47
56
  export async function prepareSyncedIdentityMarkdownScaffold(identity: EthagentIdentity): Promise<IdentityMarkdownScaffold> {
48
57
  await ensureIdentityMarkdownScaffold(identity)
49
58
  const privateFiles = await readContinuityFiles(identity)
50
- const publicSkills = await readPublicSkillsFile(identity)
51
59
  const privateDefaults = defaultContinuityFiles(identity)
52
- const publicDefault = defaultPublicSkillsJson(identity)
60
+ const publicDefault = await renderPublicSkillsJsonForIdentity(identity)
53
61
  return {
54
62
  'SOUL.md': syncGeneratedMarkdown(privateFiles['SOUL.md'], privateDefaults['SOUL.md'], [
55
63
  { marker: 'identity' },
@@ -57,34 +65,28 @@ export async function prepareSyncedIdentityMarkdownScaffold(identity: EthagentId
57
65
  'MEMORY.md': syncGeneratedMarkdown(privateFiles['MEMORY.md'], privateDefaults['MEMORY.md'], [
58
66
  { marker: 'identity' },
59
67
  ]),
60
- 'skills.json': syncSkillsJson(publicSkills, publicDefault),
68
+ 'skills.json': publicDefault,
61
69
  }
62
70
  }
63
71
 
64
72
  export async function prepareSyncedPublicSkillsJson(identity: EthagentIdentity): Promise<string> {
65
73
  await ensurePublicSkillsFile(identity)
66
- const publicSkills = await readPublicSkillsFile(identity)
67
- return syncSkillsJson(publicSkills, defaultPublicSkillsJson(identity))
74
+ return renderPublicSkillsJsonForIdentity(identity)
68
75
  }
69
76
 
70
- function syncSkillsJson(existing: string, fresh: string): string {
71
- try {
72
- const existingParsed = JSON.parse(existing)
73
- const freshParsed = JSON.parse(fresh)
74
- const merged = {
75
- ...existingParsed,
76
- ...freshParsed,
77
- skills: existingParsed.skills || freshParsed.skills,
78
- inputModes: existingParsed.inputModes || freshParsed.inputModes,
79
- outputModes: existingParsed.outputModes || freshParsed.outputModes,
80
- }
81
- if (!Object.hasOwn(freshParsed, 'imageUrl')) delete merged.imageUrl
82
- return `${JSON.stringify(merged, null, 2)}\n`
83
- } catch {
84
- return fresh
85
- }
77
+ export async function prepareSyncedSkillsTree(identity: EthagentIdentity): Promise<ContinuitySkillsTree> {
78
+ await ensureContinuityVault(identity)
79
+ return loadSkillsTree(identity)
86
80
  }
87
81
 
82
+ export async function restoreSkillsTree(
83
+ identity: EthagentIdentity,
84
+ tree: ContinuitySkillsTree | undefined,
85
+ ): Promise<void> {
86
+ await materializeSkillsTree(identity, tree)
87
+ }
88
+
89
+
88
90
  export async function ensurePublicSkillsFile(
89
91
  identity: EthagentIdentity,
90
92
  options: { fallback?: string | (() => Promise<string>) } = {},
@@ -1,6 +1,8 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import type { EthagentIdentity } from '../../../storage/config.js'
3
- import type { ContinuityFiles } from '../envelope.js'
3
+ import type { ContinuityFiles, ContinuitySkillsTree } from '../envelope.js'
4
+ import { loadSkillsTree } from '../skills/loadSkills.js'
5
+ import { syncPublicSkillsManifest } from '../skills/publicSkillsSync.js'
4
6
  import { continuityVaultRef } from './paths.js'
5
7
  import { exists, readContinuityFiles, statIfExists } from './files.js'
6
8
  import { readPublicSkillsFile } from './scaffold.js'
@@ -52,28 +54,55 @@ export async function localContinuitySnapshotContentHashes(
52
54
  identity: EthagentIdentity,
53
55
  ): Promise<ContinuitySnapshotContentHashes> {
54
56
  const privateFiles = await readContinuityFiles(identity)
57
+ await syncPublicSkillsManifest(identity).catch(() => undefined)
55
58
  const publicSkills = await readPublicSkillsFile(identity)
56
- return continuitySnapshotContentHashes(privateFiles, publicSkills)
59
+ const skills = await loadSkillsTree(identity).catch(() => ({} as ContinuitySkillsTree))
60
+ return continuitySnapshotContentHashes(privateFiles, publicSkills, skills)
61
+ }
62
+
63
+ export function continuitySnapshotContentHashesFromSources(args: {
64
+ privateFiles: ContinuityFiles
65
+ publicSkills: string
66
+ skills: ContinuitySkillsTree
67
+ }): ContinuitySnapshotContentHashes {
68
+ return continuitySnapshotContentHashes(args.privateFiles, args.publicSkills, args.skills)
57
69
  }
58
70
 
59
71
  function continuitySnapshotContentHashes(
60
72
  privateFiles: ContinuityFiles,
61
73
  publicSkills: string,
74
+ skills: ContinuitySkillsTree,
62
75
  ): ContinuitySnapshotContentHashes {
76
+ const skillsHash = hashSkillsTree(skills)
63
77
  return {
64
78
  'SOUL.md': hashContinuitySnapshotContent(privateFiles['SOUL.md']),
65
79
  'MEMORY.md': hashContinuitySnapshotContent(privateFiles['MEMORY.md']),
66
80
  'skills.json': hashContinuitySnapshotContent(publicSkills),
81
+ ...(skillsHash ? { 'private-skills': skillsHash } : {}),
82
+ }
83
+ }
84
+
85
+ export function hashSkillsTree(tree: ContinuitySkillsTree): string {
86
+ const keys = Object.keys(tree).sort()
87
+ if (keys.length === 0) return ''
88
+ const h = createHash('sha256')
89
+ for (const key of keys) {
90
+ h.update(key, 'utf8')
91
+ h.update('\0')
92
+ h.update(normalizeSnapshotContent(tree[key] ?? ''), 'utf8')
93
+ h.update('\0')
67
94
  }
95
+ return h.digest('hex')
68
96
  }
69
97
 
70
98
  function equalContinuitySnapshotHashes(
71
99
  a: ContinuitySnapshotContentHashes,
72
100
  b: ContinuitySnapshotContentHashes,
73
101
  ): boolean {
74
- return a['SOUL.md'] === b['SOUL.md']
75
- && a['MEMORY.md'] === b['MEMORY.md']
76
- && a['skills.json'] === b['skills.json']
102
+ if (a['SOUL.md'] !== b['SOUL.md']) return false
103
+ if (a['MEMORY.md'] !== b['MEMORY.md']) return false
104
+ if (a['skills.json'] !== b['skills.json']) return false
105
+ return a['private-skills'] === b['private-skills']
77
106
  }
78
107
 
79
108
  function hashContinuitySnapshotContent(value: string): string {
@@ -8,14 +8,15 @@ export type ContinuityVaultRef = {
8
8
  soulPath: string
9
9
  memoryPath: string
10
10
  publicSkillsPath: string
11
+ skillsDir: string
11
12
  }
12
13
 
13
14
  export type IdentityMarkdownScaffold = ContinuityFiles & {
14
15
  'skills.json': string
15
16
  }
16
17
 
17
- export type ContinuitySnapshotFile = PrivateContinuityFile | 'skills.json'
18
- export type ContinuitySnapshotContentHashes = Record<ContinuitySnapshotFile, string>
18
+ export type ContinuitySnapshotFile = PrivateContinuityFile | 'skills.json' | 'private-skills'
19
+ export type ContinuitySnapshotContentHashes = Partial<Record<ContinuitySnapshotFile, string>> & Record<PrivateContinuityFile | 'skills.json', string>
19
20
  export type ContinuityPublishState = 'not-restored' | 'not-published' | 'verify-needed' | 'local-changes' | 'published'
20
21
  export type ContinuityWorkingTreeStatus = {
21
22
  ready: boolean
@@ -19,8 +19,10 @@ export {
19
19
  ensureIdentityMarkdownScaffold,
20
20
  ensurePublicSkillsFile,
21
21
  prepareSyncedIdentityMarkdownScaffold,
22
+ prepareSyncedSkillsTree,
22
23
  prepareSyncedPublicSkillsJson,
23
24
  readPublicSkillsFile,
25
+ restoreSkillsTree,
24
26
  syncIdentityMarkdownScaffold,
25
27
  writeIdentityMarkdownScaffold,
26
28
  writePublicSkillsFile,
@@ -28,5 +30,6 @@ export {
28
30
  export {
29
31
  continuityVaultStatus,
30
32
  continuityWorkingTreeStatus,
33
+ continuitySnapshotContentHashesFromSources,
31
34
  localContinuitySnapshotContentHashes,
32
35
  } from './storage/status.js'
@@ -17,6 +17,15 @@ import {
17
17
  PrivateContinuityScreen,
18
18
  PublicProfileScreen,
19
19
  } from './continuity/ContinuityDashboardScreen.js'
20
+ import { SkillsTreeScreen } from './continuity/skills/SkillsTreeScreen.js'
21
+ import { NewSkillScreen } from './continuity/skills/NewSkillScreen.js'
22
+ import { NewSkillVisibilityScreen } from './continuity/skills/NewSkillVisibilityScreen.js'
23
+ import { DeleteSkillScreen } from './continuity/skills/DeleteSkillScreen.js'
24
+ import { DeleteSkillConfirmScreen } from './continuity/skills/DeleteSkillConfirmScreen.js'
25
+ import {
26
+ SkillVisibilityListScreen,
27
+ SkillVisibilityPickScreen,
28
+ } from './continuity/skills/SkillVisibilityScreen.js'
20
29
  import { RecoveryConfirmScreen } from './continuity/RecoveryConfirmScreen.js'
21
30
  import { SavePromptScreen } from './continuity/SavePromptScreen.js'
22
31
  import { ErrorScreen } from './shared/components/ErrorScreen.js'
@@ -66,7 +75,11 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
66
75
  openTokenTransferFlow,
67
76
  openPublicProfileEdit,
68
77
  openContinuityFile,
69
- exportLocalBackupZip,
78
+ openSkillFile,
79
+ openSkillsFolder,
80
+ createSkill,
81
+ deleteSkill,
82
+ setSkillVisibility,
70
83
  } = controller
71
84
 
72
85
  if (step.kind === 'rebackup-confirm') {
@@ -143,13 +156,102 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
143
156
  editorOpened={step.editorOpened}
144
157
  onOpenSoul={() => { void openContinuityFile('soul') }}
145
158
  onOpenMemory={() => { void openContinuityFile('memory') }}
146
- onOpenSkills={() => { void openContinuityFile('skills') }}
147
- onExportBackup={() => { void exportLocalBackupZip() }}
148
159
  onBack={back}
149
160
  />
150
161
  )
151
162
  }
152
163
 
164
+ if (step.kind === 'continuity-skills-tree') {
165
+ return (
166
+ <SkillsTreeScreen
167
+ identity={identity}
168
+ config={config}
169
+ workingStatus={workingStatus}
170
+ notice={step.notice}
171
+ editorOpened={step.editorOpened}
172
+ footer={footer}
173
+ onOpenSkill={relativePath => { void openSkillFile(relativePath) }}
174
+ onNewSkill={() => setStep({ kind: 'continuity-skill-new' })}
175
+ onDelete={() => setStep({ kind: 'continuity-skill-delete' })}
176
+ onVisibility={() => setStep({ kind: 'continuity-skill-visibility' })}
177
+ onViewPublicManifest={() => { void openContinuityFile('skills') }}
178
+ onOpenFolder={() => { void openSkillsFolder() }}
179
+ onBack={back}
180
+ />
181
+ )
182
+ }
183
+
184
+ if (step.kind === 'continuity-skill-visibility') {
185
+ return (
186
+ <SkillVisibilityListScreen
187
+ identity={identity}
188
+ notice={step.notice}
189
+ footer={footer}
190
+ onPick={relativePath => setStep({ kind: 'continuity-skill-visibility-pick', relativePath })}
191
+ onCancel={back}
192
+ />
193
+ )
194
+ }
195
+
196
+ if (step.kind === 'continuity-skill-visibility-pick') {
197
+ return (
198
+ <SkillVisibilityPickScreen
199
+ identity={identity}
200
+ relativePath={step.relativePath}
201
+ footer={footer}
202
+ onSelect={visibility => { void setSkillVisibility(step.relativePath, visibility) }}
203
+ onCancel={back}
204
+ />
205
+ )
206
+ }
207
+
208
+ if (step.kind === 'continuity-skill-new') {
209
+ return (
210
+ <NewSkillScreen
211
+ error={step.error}
212
+ footer={footer}
213
+ onSubmit={name => setStep({ kind: 'continuity-skill-new-visibility', name })}
214
+ onCancel={back}
215
+ />
216
+ )
217
+ }
218
+
219
+ if (step.kind === 'continuity-skill-new-visibility') {
220
+ return (
221
+ <NewSkillVisibilityScreen
222
+ name={step.name}
223
+ {...(step.error ? { error: step.error } : {})}
224
+ footer={footer}
225
+ onSelect={visibility => { void createSkill(step.name, visibility) }}
226
+ onCancel={back}
227
+ />
228
+ )
229
+ }
230
+
231
+ if (step.kind === 'continuity-skill-delete') {
232
+ return (
233
+ <DeleteSkillScreen
234
+ identity={identity}
235
+ notice={step.notice}
236
+ footer={footer}
237
+ onPick={target => setStep({ kind: 'continuity-skill-delete-confirm', target })}
238
+ onCancel={back}
239
+ />
240
+ )
241
+ }
242
+
243
+ if (step.kind === 'continuity-skill-delete-confirm') {
244
+ return (
245
+ <DeleteSkillConfirmScreen
246
+ identity={identity}
247
+ target={step.target}
248
+ footer={footer}
249
+ onConfirm={() => { void deleteSkill(step.target.relativePath) }}
250
+ onCancel={back}
251
+ />
252
+ )
253
+ }
254
+
153
255
  if (step.kind === 'continuity-public') {
154
256
  return (
155
257
  <PublicProfileScreen
@@ -9,11 +9,11 @@ import { copyToClipboard } from '../../utils/clipboard.js'
9
9
  import { DEFAULT_IPFS_API_URL } from '../storage/ipfs.js'
10
10
  import { chainIdForNetwork, erc8004ConfigForSupportedChain } from '../registry/erc8004.js'
11
11
  import { shortAddress } from './shared/model/format.js'
12
+ import { canRestoreCandidate } from './restore/discover.js'
12
13
  import {
13
- canRestoreCandidate,
14
14
  resolveAgentEnsToCandidate,
15
15
  resolveAgentTokenIdToCandidate,
16
- } from './restore/index.js'
16
+ } from './restore/resolve.js'
17
17
  import {
18
18
  runRegistrySubmit,
19
19
  runStorageSubmit,
@@ -85,7 +85,7 @@ export const IdentityHubRoutes: React.FC<{ controller: IdentityHubController }>
85
85
  { value: 'ens', role: 'section', label: 'Set Up Now' },
86
86
  { value: 'ens', label: 'Set Up ENS Name', hint: 'Walks you through Root, Name, Review, and Apply' },
87
87
  { value: 'skip', role: 'section', label: 'Skip' },
88
- { value: 'skip', label: 'Skip For Now', hint: 'Continue to model setup. ENS can be added from Identity Hub later.', role: 'utility' },
88
+ { value: 'skip', label: 'Skip For Now', hint: 'Continue to model setup; add ENS later', role: 'utility' },
89
89
  ]}
90
90
  hintLayout="inline"
91
91
  onSubmit={choice => {
@@ -142,6 +142,7 @@ export const IdentityHubRoutes: React.FC<{ controller: IdentityHubController }>
142
142
  setStep({ kind: 'custody-model', identity, registry: reg, returnTo: { kind: 'menu' } })
143
143
  }}
144
144
  onContinuity={() => setStep({ kind: 'continuity-private' })}
145
+ onSkillsTree={() => setStep({ kind: 'continuity-skills-tree' })}
145
146
  onIdentityValues={() => setStep({ kind: 'details' })}
146
147
  onPrepareTransfer={openTokenTransferFlow}
147
148
  onStorage={() => setStep({ kind: 'storage-credential' })}
@@ -329,6 +330,7 @@ export const IdentityHubRoutes: React.FC<{ controller: IdentityHubController }>
329
330
  <DetailsScreen
330
331
  identity={identity}
331
332
  config={config}
333
+ workingStatus={workingStatus}
332
334
  copyNotice={copyNotice}
333
335
  unlinked={reconciliation?.token === 'unlinked'}
334
336
  {...(reconciliation?.onChainOwner ? { onchainOwner: reconciliation.onChainOwner } : {})}
@@ -10,7 +10,7 @@ import { changedContinuitySnapshotFiles } from './state.js'
10
10
  import { readIdentityStateString } from '../custody/state.js'
11
11
  import { shortCid } from '../shared/model/format.js'
12
12
 
13
- type PrivateAction = 'soul' | 'memory' | 'skills' | 'export-backup' | 'back'
13
+ type PrivateAction = 'soul' | 'memory' | 'back'
14
14
  type PublicAction = 'edit' | 'back'
15
15
 
16
16
  interface CommonProps {
@@ -36,7 +36,7 @@ const SaveFromHubHint: React.FC<{ workingStatus?: ContinuityWorkingTreeStatus |
36
36
  Unsaved changes
37
37
  {files.length > 0 ? `: ${files.join(', ')}` : ''}
38
38
  </Text>
39
- <Text color={theme.dim}>Use Save Snapshot Now in the Identity Hub menu, or relaunch ethagent.</Text>
39
+ <Text color={theme.dim}>Save Snapshot Now to publish.</Text>
40
40
  </Box>
41
41
  )
42
42
  }
@@ -44,8 +44,6 @@ const SaveFromHubHint: React.FC<{ workingStatus?: ContinuityWorkingTreeStatus |
44
44
  export const PrivateContinuityScreen: React.FC<CommonProps & {
45
45
  onOpenSoul: () => void
46
46
  onOpenMemory: () => void
47
- onOpenSkills: () => void
48
- onExportBackup: () => void
49
47
  }> = ({
50
48
  identity,
51
49
  config,
@@ -56,13 +54,10 @@ export const PrivateContinuityScreen: React.FC<CommonProps & {
56
54
  footer,
57
55
  onOpenSoul,
58
56
  onOpenMemory,
59
- onOpenSkills,
60
- onExportBackup,
61
57
  onBack,
62
58
  }) => (
63
- <Surface title="Soul, Memory, and Skills" subtitle={notice ?? privateSubtitle(ready)} footer={footer}>
59
+ <Surface title="Soul & Memory" subtitle={notice ?? privateSubtitle(ready)} footer={footer}>
64
60
  <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges />
65
- <PrivateRows identity={identity} ready={ready} />
66
61
  <SaveFromHubHint workingStatus={workingStatus} />
67
62
  {editorOpened && (
68
63
  <Box marginTop={1}>
@@ -72,13 +67,9 @@ export const PrivateContinuityScreen: React.FC<CommonProps & {
72
67
  <Box marginTop={1}>
73
68
  <Select<PrivateAction>
74
69
  options={[
75
- { value: 'soul', role: 'section', label: 'Private Continuity' },
70
+ { value: 'soul', role: 'section', label: 'Files' },
76
71
  { value: 'soul', label: 'Edit Soul', hint: 'Voice, behavior, and operating preferences', disabled: !ready },
77
72
  { value: 'memory', label: 'Edit Memory', hint: 'Durable owner memory for this agent', disabled: !ready },
78
- { value: 'skills', role: 'section', label: 'Public Capabilities' },
79
- { value: 'skills', label: 'Edit skills.json', hint: 'Machine-readable capabilities for discovery', disabled: !ready },
80
- { value: 'export-backup', role: 'section', label: 'Local Backup' },
81
- { value: 'export-backup', label: 'Save Local Backup', hint: 'Bundle SOUL.md, MEMORY.md, skills.json into a zip you can keep anywhere', disabled: !ready },
82
73
  { value: 'back', role: 'section', label: 'Navigation' },
83
74
  { value: 'back', label: 'Back', hint: 'Return to Identity Hub menu', role: 'utility' },
84
75
  ]}
@@ -86,8 +77,6 @@ export const PrivateContinuityScreen: React.FC<CommonProps & {
86
77
  onSubmit={choice => {
87
78
  if (choice === 'soul') return onOpenSoul()
88
79
  if (choice === 'memory') return onOpenMemory()
89
- if (choice === 'skills') return onOpenSkills()
90
- if (choice === 'export-backup') return onExportBackup()
91
80
  return onBack()
92
81
  }}
93
82
  onCancel={onBack}
@@ -102,7 +91,6 @@ export const PublicProfileScreen: React.FC<CommonProps & {
102
91
  return (
103
92
  <Surface title="Public Profile" subtitle={notice ?? 'Manage the public name, description, icon, and Agent Card.'} footer={footer}>
104
93
  <IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges />
105
- <PublicProfileRows identity={identity} />
106
94
  <SaveFromHubHint workingStatus={workingStatus} />
107
95
  {editorOpened && (
108
96
  <Box marginTop={1}>
@@ -129,42 +117,8 @@ export const PublicProfileScreen: React.FC<CommonProps & {
129
117
  )
130
118
  }
131
119
 
132
- const PrivateRows: React.FC<{ identity?: EthagentIdentity; ready: boolean }> = ({ identity, ready }) => (
133
- <Box flexDirection="column" marginTop={1}>
134
- <Text>
135
- <Text color={theme.dim}>{'Private'.padEnd(13)}</Text>
136
- <Text color={ready ? theme.text : theme.dim}>{ready ? 'Local Files Ready' : 'Missing Local Working Files'}</Text>
137
- </Text>
138
- <Text>
139
- <Text color={theme.dim}>{'Snapshot'.padEnd(13)}</Text>
140
- <Text color={identity?.backup?.cid ? theme.text : theme.dim}>{identity?.backup?.cid ? shortCid(identity.backup.cid) : 'Not Saved Yet'}</Text>
141
- </Text>
142
- <Text>
143
- <Text color={theme.dim}>{'Skills'.padEnd(13)}</Text>
144
- <Text color={identity?.publicSkills?.cid ? theme.text : theme.dim}>{identity?.publicSkills?.cid ? shortCid(identity.publicSkills.cid) : 'Not Saved'}</Text>
145
- </Text>
146
- </Box>
147
- )
148
-
149
- const PublicProfileRows: React.FC<{ identity?: EthagentIdentity }> = ({ identity }) => (
150
- <Box flexDirection="column" marginTop={1}>
151
- <Text>
152
- <Text color={theme.dim}>{publicProfileLabel('Agent Card')}</Text>
153
- <Text color={identity?.publicSkills?.agentCardCid ? theme.text : theme.dim}>{identity?.publicSkills?.agentCardCid ? shortCid(identity.publicSkills.agentCardCid) : 'Not Saved'}</Text>
154
- </Text>
155
- <Text>
156
- <Text color={theme.dim}>{publicProfileLabel('Agent Icon')}</Text>
157
- <Text color={readIdentityStateString(identity?.state, 'imageUrl') ? theme.text : theme.dim}>{readIdentityStateString(identity?.state, 'imageUrl') ? 'Attached' : 'Not Attached'}</Text>
158
- </Text>
159
- </Box>
160
- )
161
-
162
- function publicProfileLabel(label: string): string {
163
- return label.padEnd(16)
164
- }
165
-
166
120
  function privateSubtitle(ready: boolean): string {
167
121
  return ready
168
- ? 'Edit local continuity files. Public skills are saved with the same snapshot.'
122
+ ? 'Edit the durable persona and memory files for this identity.'
169
123
  : 'Use "Refetch Latest Snapshot" from the Identity Hub menu to recover files.'
170
124
  }
@@ -66,7 +66,7 @@ export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mo
66
66
  label: isPublish ? 'Yes, Save Snapshot Now' : 'Yes, Refetch From Chain',
67
67
  hint: isPublish ? 'Sign and save the encrypted snapshot' : 'Wallet decrypts and overwrites local files',
68
68
  },
69
- { value: 'back', role: 'section', label: 'Cancel' },
69
+ { value: 'back', role: 'section', label: 'Navigation' },
70
70
  {
71
71
  value: 'back',
72
72
  label: 'No, Go Back',