ethagent 3.0.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.
- package/package.json +1 -1
- package/src/identity/continuity/envelope.ts +9 -9
- package/src/identity/continuity/publicSkills.ts +17 -0
- package/src/identity/hub/OperationalRoutes.tsx +11 -39
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +3 -23
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +5 -5
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -3
- package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +151 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +1 -33
- package/src/identity/hub/continuity/state.ts +9 -9
- package/src/identity/hub/create/CreateFlow.tsx +1 -1
- package/src/identity/hub/custody/routes.tsx +1 -1
- package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +0 -1
- package/src/identity/hub/ens/EnsEditMaintenanceScreens.tsx +0 -1
- package/src/identity/hub/identityHubReducer.ts +3 -9
- package/src/identity/hub/profile/EditProfileFlow.tsx +5 -5
- package/src/identity/hub/restore/recovery.ts +3 -3
- package/src/identity/hub/shared/components/IdentitySummary.tsx +22 -2
- package/src/identity/hub/shared/components/MenuScreen.tsx +4 -4
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +3 -3
- package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +1 -1
- package/src/identity/hub/useIdentityHubContinuity.ts +3 -3
- package/src/identity/wallet/page/copy.ts +43 -43
- package/src/models/modelPickerOptions.ts +2 -0
- package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +0 -123
- package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +0 -171
package/package.json
CHANGED
|
@@ -224,10 +224,10 @@ export class ContinuitySnapshotRestoreSlotMissingError extends Error {
|
|
|
224
224
|
const CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES = [
|
|
225
225
|
'Save or Restore Identity Files',
|
|
226
226
|
'Action: encrypt or decrypt local identity files',
|
|
227
|
-
'Private: SOUL.md, MEMORY.md',
|
|
227
|
+
'Private: SOUL.md, MEMORY.md, skills',
|
|
228
228
|
'Public: public skills and profile',
|
|
229
229
|
'Safety: no transaction, spending, or approvals',
|
|
230
|
-
'Version:
|
|
230
|
+
'Version: 2',
|
|
231
231
|
] as const
|
|
232
232
|
|
|
233
233
|
export function createContinuitySnapshotChallenge(ownerAddress: string): string {
|
|
@@ -265,10 +265,10 @@ export function createTransferContinuitySnapshotChallenge(args: {
|
|
|
265
265
|
`Sender Owner: ${ownerAddress}`,
|
|
266
266
|
`Receiver Owner: ${targetAddress}`,
|
|
267
267
|
'Action: encrypt or decrypt local identity files for this token transfer',
|
|
268
|
-
'Private: SOUL.md, MEMORY.md',
|
|
268
|
+
'Private: SOUL.md, MEMORY.md, skills',
|
|
269
269
|
'Public: public skills and profile',
|
|
270
270
|
'Safety: no transaction, spending, or approvals',
|
|
271
|
-
'Version:
|
|
271
|
+
'Version: 2',
|
|
272
272
|
].join('\n')
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -290,7 +290,7 @@ const WALLET_CHALLENGE_V2_COPY: Record<WalletChallengePurpose, { title: string;
|
|
|
290
290
|
'create-agent': { title: 'Create Agent Snapshot Key', action: 'Action: encrypt the new agent snapshot for owner restore' },
|
|
291
291
|
'update-snapshot': { title: 'Save Snapshot Encryption Key', action: 'Action: encrypt the updated agent snapshot' },
|
|
292
292
|
'update-ens-snapshot': { title: 'Update ENS in Agent Snapshot', action: 'Action: encrypt the snapshot with the new ENS name. No onchain ENS records change.' },
|
|
293
|
-
'clear-ens-snapshot': { title: '
|
|
293
|
+
'clear-ens-snapshot': { title: 'Unlink ENS from Agent', action: 'Action: encrypt the snapshot with no ENS name. No onchain ENS records change.' },
|
|
294
294
|
'update-profile-snapshot': { title: 'Update Public Profile Snapshot Key', action: 'Action: encrypt the snapshot with the updated profile' },
|
|
295
295
|
'update-operators-snapshot': { title: 'Update Operator Wallets Snapshot Key', action: 'Action: encrypt the snapshot with the updated operator list' },
|
|
296
296
|
'refetch-snapshot': { title: 'Refetch Latest Snapshot', action: 'Action: decrypt the latest published snapshot' },
|
|
@@ -322,9 +322,9 @@ export function createWalletRestoreAccessChallenge(args: {
|
|
|
322
322
|
`Wallet: ${walletAddress}`,
|
|
323
323
|
`Access Epoch: ${args.accessEpoch ?? 1}`,
|
|
324
324
|
copy.action,
|
|
325
|
-
'Private: SOUL.md, MEMORY.md',
|
|
325
|
+
'Private: SOUL.md, MEMORY.md, skills',
|
|
326
326
|
'Safety: no transaction, spending, or approvals',
|
|
327
|
-
'Version:
|
|
327
|
+
'Version: 3',
|
|
328
328
|
].join('\n')
|
|
329
329
|
}
|
|
330
330
|
return [
|
|
@@ -336,9 +336,9 @@ export function createWalletRestoreAccessChallenge(args: {
|
|
|
336
336
|
`Wallet: ${walletAddress}`,
|
|
337
337
|
`Access Epoch: ${args.accessEpoch ?? 1}`,
|
|
338
338
|
'Action: create a restore key for encrypted identity snapshots',
|
|
339
|
-
'Private: SOUL.md, MEMORY.md',
|
|
339
|
+
'Private: SOUL.md, MEMORY.md, skills',
|
|
340
340
|
'Safety: no transaction, spending, or approvals',
|
|
341
|
-
'Version:
|
|
341
|
+
'Version: 2',
|
|
342
342
|
].join('\n')
|
|
343
343
|
}
|
|
344
344
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { EthagentIdentity } from '../../storage/config.js'
|
|
2
2
|
import type { SkillIndexEntry } from './skills/types.js'
|
|
3
|
+
import { identityOwnerAddress } from '../hub/custody/state.js'
|
|
4
|
+
import { toChecksumAddress } from '../crypto/eth.js'
|
|
3
5
|
|
|
4
6
|
type PublicSkill = {
|
|
5
7
|
id: string
|
|
@@ -13,6 +15,7 @@ type PublicSkillsProfile = {
|
|
|
13
15
|
name: string
|
|
14
16
|
description: string
|
|
15
17
|
version: string
|
|
18
|
+
agentWallet: string
|
|
16
19
|
imageUrl?: string
|
|
17
20
|
skills: PublicSkill[]
|
|
18
21
|
}
|
|
@@ -30,6 +33,8 @@ type AgentCard = {
|
|
|
30
33
|
streaming: boolean
|
|
31
34
|
pushNotifications: boolean
|
|
32
35
|
}
|
|
36
|
+
producer: { name: string; url: string }
|
|
37
|
+
agent_wallet: string
|
|
33
38
|
skills: Array<{
|
|
34
39
|
id: string
|
|
35
40
|
name: string
|
|
@@ -39,6 +44,11 @@ type AgentCard = {
|
|
|
39
44
|
}>
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
const ETHAGENT_PRODUCER = {
|
|
48
|
+
name: 'ethagent',
|
|
49
|
+
url: 'https://github.com/baairon/ethagent',
|
|
50
|
+
} as const
|
|
51
|
+
|
|
42
52
|
export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSkillsProfile {
|
|
43
53
|
const state = identity.state ?? {}
|
|
44
54
|
const name = typeof state.name === 'string' && state.name.trim()
|
|
@@ -50,10 +60,13 @@ export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSk
|
|
|
50
60
|
const imageUrl = typeof state.imageUrl === 'string' && state.imageUrl.trim()
|
|
51
61
|
? state.imageUrl.trim()
|
|
52
62
|
: undefined
|
|
63
|
+
const ownerAddress = identityOwnerAddress(identity)
|
|
64
|
+
const agentWallet = ownerAddress ? toChecksumAddress(ownerAddress) : ''
|
|
53
65
|
return {
|
|
54
66
|
name,
|
|
55
67
|
description,
|
|
56
68
|
version: '1.0.0',
|
|
69
|
+
agentWallet,
|
|
57
70
|
...(imageUrl ? { imageUrl } : {}),
|
|
58
71
|
skills: [
|
|
59
72
|
{
|
|
@@ -121,6 +134,8 @@ export function renderPublicSkillsJson(profile: PublicSkillsProfile): string {
|
|
|
121
134
|
const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
|
|
122
135
|
const summary = {
|
|
123
136
|
schema: 'ethagent.public-skills.v1',
|
|
137
|
+
producer: ETHAGENT_PRODUCER,
|
|
138
|
+
agent_wallet: profile.agentWallet,
|
|
124
139
|
visibility: 'public',
|
|
125
140
|
name: profile.name,
|
|
126
141
|
description: profile.description,
|
|
@@ -185,6 +200,8 @@ export function createAgentCard(profile: PublicSkillsProfile, url?: string): Age
|
|
|
185
200
|
streaming: true,
|
|
186
201
|
pushNotifications: false,
|
|
187
202
|
},
|
|
203
|
+
producer: ETHAGENT_PRODUCER,
|
|
204
|
+
agent_wallet: profile.agentWallet,
|
|
188
205
|
skills: profile.skills.map(skill => ({
|
|
189
206
|
id: skill.id,
|
|
190
207
|
name: skill.name,
|
|
@@ -20,12 +20,8 @@ import {
|
|
|
20
20
|
import { SkillsTreeScreen } from './continuity/skills/SkillsTreeScreen.js'
|
|
21
21
|
import { NewSkillScreen } from './continuity/skills/NewSkillScreen.js'
|
|
22
22
|
import { NewSkillVisibilityScreen } from './continuity/skills/NewSkillVisibilityScreen.js'
|
|
23
|
-
import {
|
|
23
|
+
import { SkillActionsScreen } from './continuity/skills/SkillActionsScreen.js'
|
|
24
24
|
import { DeleteSkillConfirmScreen } from './continuity/skills/DeleteSkillConfirmScreen.js'
|
|
25
|
-
import {
|
|
26
|
-
SkillVisibilityListScreen,
|
|
27
|
-
SkillVisibilityPickScreen,
|
|
28
|
-
} from './continuity/skills/SkillVisibilityScreen.js'
|
|
29
25
|
import { RecoveryConfirmScreen } from './continuity/RecoveryConfirmScreen.js'
|
|
30
26
|
import { SavePromptScreen } from './continuity/SavePromptScreen.js'
|
|
31
27
|
import { ErrorScreen } from './shared/components/ErrorScreen.js'
|
|
@@ -136,9 +132,9 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
|
|
|
136
132
|
return (
|
|
137
133
|
<WalletApprovalScreen
|
|
138
134
|
title="Refetch Latest Snapshot"
|
|
139
|
-
subtitle="Wallet signature decrypts the latest saved snapshot and restores SOUL.md, MEMORY.md, and skills.
|
|
135
|
+
subtitle="Wallet signature decrypts the latest saved snapshot and restores SOUL.md, MEMORY.md, and skills."
|
|
140
136
|
walletSession={walletSession}
|
|
141
|
-
label={restoreProgress?.label ?? 'fetching latest snapshot from
|
|
137
|
+
label={restoreProgress?.label ?? 'fetching latest snapshot from onchain...'}
|
|
142
138
|
onCancel={() => setStep(step.back)}
|
|
143
139
|
/>
|
|
144
140
|
)
|
|
@@ -170,37 +166,25 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
|
|
|
170
166
|
notice={step.notice}
|
|
171
167
|
editorOpened={step.editorOpened}
|
|
172
168
|
footer={footer}
|
|
173
|
-
onOpenSkill={relativePath => {
|
|
169
|
+
onOpenSkill={relativePath => setStep({ kind: 'continuity-skill-actions', relativePath })}
|
|
174
170
|
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
171
|
onOpenFolder={() => { void openSkillsFolder() }}
|
|
179
172
|
onBack={back}
|
|
180
173
|
/>
|
|
181
174
|
)
|
|
182
175
|
}
|
|
183
176
|
|
|
184
|
-
if (step.kind === 'continuity-skill-
|
|
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') {
|
|
177
|
+
if (step.kind === 'continuity-skill-actions') {
|
|
197
178
|
return (
|
|
198
|
-
<
|
|
179
|
+
<SkillActionsScreen
|
|
199
180
|
identity={identity}
|
|
200
181
|
relativePath={step.relativePath}
|
|
182
|
+
{...(step.notice ? { notice: step.notice } : {})}
|
|
201
183
|
footer={footer}
|
|
202
|
-
|
|
203
|
-
|
|
184
|
+
onOpenSkill={relativePath => { void openSkillFile(relativePath) }}
|
|
185
|
+
onSetVisibility={(relativePath, visibility) => { void setSkillVisibility(relativePath, visibility) }}
|
|
186
|
+
onDelete={relativePath => setStep({ kind: 'continuity-skill-delete-confirm', target: { kind: 'skill', relativePath } })}
|
|
187
|
+
onBack={back}
|
|
204
188
|
/>
|
|
205
189
|
)
|
|
206
190
|
}
|
|
@@ -228,18 +212,6 @@ export const IdentityHubOperationalRoutes: React.FC<IdentityHubOperationalRoutes
|
|
|
228
212
|
)
|
|
229
213
|
}
|
|
230
214
|
|
|
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
215
|
if (step.kind === 'continuity-skill-delete-confirm') {
|
|
244
216
|
return (
|
|
245
217
|
<DeleteSkillConfirmScreen
|
|
@@ -6,7 +6,6 @@ import { theme } from '../../../ui/theme.js'
|
|
|
6
6
|
import type { EthagentConfig, EthagentIdentity } from '../../../storage/config.js'
|
|
7
7
|
import type { ContinuityWorkingTreeStatus } from '../../continuity/storage.js'
|
|
8
8
|
import { IdentitySummary } from '../shared/components/IdentitySummary.js'
|
|
9
|
-
import { changedContinuitySnapshotFiles } from './state.js'
|
|
10
9
|
import { readIdentityStateString } from '../custody/state.js'
|
|
11
10
|
import { shortCid } from '../shared/model/format.js'
|
|
12
11
|
|
|
@@ -24,23 +23,6 @@ interface CommonProps {
|
|
|
24
23
|
onBack: () => void
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const SaveFromHubHint: React.FC<{ workingStatus?: ContinuityWorkingTreeStatus | null }> = ({ workingStatus }) => {
|
|
28
|
-
const needsBackup = workingStatus?.publishState === 'local-changes'
|
|
29
|
-
|| workingStatus?.publishState === 'not-published'
|
|
30
|
-
|| workingStatus?.publishState === 'verify-needed'
|
|
31
|
-
if (!needsBackup) return null
|
|
32
|
-
const files = changedContinuitySnapshotFiles(workingStatus)
|
|
33
|
-
return (
|
|
34
|
-
<Box marginTop={1} flexDirection="column">
|
|
35
|
-
<Text color={theme.accentError} bold>
|
|
36
|
-
Unsaved changes
|
|
37
|
-
{files.length > 0 ? `: ${files.join(', ')}` : ''}
|
|
38
|
-
</Text>
|
|
39
|
-
<Text color={theme.dim}>Save Snapshot Now to publish.</Text>
|
|
40
|
-
</Box>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
26
|
export const PrivateContinuityScreen: React.FC<CommonProps & {
|
|
45
27
|
onOpenSoul: () => void
|
|
46
28
|
onOpenMemory: () => void
|
|
@@ -57,8 +39,7 @@ export const PrivateContinuityScreen: React.FC<CommonProps & {
|
|
|
57
39
|
onBack,
|
|
58
40
|
}) => (
|
|
59
41
|
<Surface title="Soul & Memory" subtitle={notice ?? privateSubtitle(ready)} footer={footer}>
|
|
60
|
-
<IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges />
|
|
61
|
-
<SaveFromHubHint workingStatus={workingStatus} />
|
|
42
|
+
<IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges compact />
|
|
62
43
|
{editorOpened && (
|
|
63
44
|
<Box marginTop={1}>
|
|
64
45
|
<Text color={theme.accentPeriwinkle}>Save with ctrl+s in your editor</Text>
|
|
@@ -90,8 +71,7 @@ export const PublicProfileScreen: React.FC<CommonProps & {
|
|
|
90
71
|
}> = ({ identity, config, workingStatus, notice, editorOpened, footer, onEditProfile, onBack }) => {
|
|
91
72
|
return (
|
|
92
73
|
<Surface title="Public Profile" subtitle={notice ?? 'Manage the public name, description, icon, and Agent Card.'} footer={footer}>
|
|
93
|
-
<IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges />
|
|
94
|
-
<SaveFromHubHint workingStatus={workingStatus} />
|
|
74
|
+
<IdentitySummary identity={identity} config={config} workingStatus={workingStatus} hideLocalChanges compact />
|
|
95
75
|
{editorOpened && (
|
|
96
76
|
<Box marginTop={1}>
|
|
97
77
|
<Text color={theme.accentPeriwinkle}>Save with ctrl+s in your editor</Text>
|
|
@@ -101,7 +81,7 @@ export const PublicProfileScreen: React.FC<CommonProps & {
|
|
|
101
81
|
<Select<PublicAction>
|
|
102
82
|
options={[
|
|
103
83
|
{ value: 'edit', role: 'section', label: 'Profile' },
|
|
104
|
-
{ value: 'edit', label: 'Edit
|
|
84
|
+
{ value: 'edit', label: 'Edit Profile', hint: 'Name, description, icon' },
|
|
105
85
|
{ value: 'back', role: 'section', label: 'Navigation' },
|
|
106
86
|
{ value: 'back', label: 'Back', hint: 'Return to Identity Hub menu', role: 'utility' },
|
|
107
87
|
]}
|
|
@@ -20,15 +20,15 @@ interface RecoveryConfirmScreenProps {
|
|
|
20
20
|
|
|
21
21
|
export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mode, workingStatus, pendingPublish, footer, onConfirm, onBack }) => {
|
|
22
22
|
const isPublish = mode === 'publish'
|
|
23
|
-
const title = isPublish ? 'Save Snapshot?' : 'Refetch Latest From
|
|
23
|
+
const title = isPublish ? 'Save Snapshot?' : 'Refetch Latest From Onchain?'
|
|
24
24
|
const subtitle = isPublish
|
|
25
|
-
? 'Saves SOUL.md, MEMORY.md, skills
|
|
25
|
+
? 'Saves SOUL.md, MEMORY.md, skills, and profile changes.'
|
|
26
26
|
: 'This overwrites local files with the onchain version.'
|
|
27
27
|
|
|
28
28
|
const headlineColor = theme.accentPeriwinkle
|
|
29
29
|
const headline = isPublish
|
|
30
30
|
? 'Saving updates the onchain pointer for this agent.'
|
|
31
|
-
: 'Refetching replaces SOUL.md, MEMORY.md, and skills
|
|
31
|
+
: 'Refetching replaces SOUL.md, MEMORY.md, and skills with what is onchain.'
|
|
32
32
|
const detail = isPublish
|
|
33
33
|
? 'Your local continuity files and profile edits become the saved state. The previous snapshot pointer is overwritten.'
|
|
34
34
|
: 'Unsaved local edits will be lost. Use this when local files are missing or out of sync with the latest saved snapshot.'
|
|
@@ -47,7 +47,7 @@ export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mo
|
|
|
47
47
|
)}
|
|
48
48
|
{!isPublish && pendingPublish ? (
|
|
49
49
|
<Box marginTop={1} flexDirection="column">
|
|
50
|
-
<Text color={theme.accentError} bold>Local snapshot is ahead of
|
|
50
|
+
<Text color={theme.accentError} bold>Local snapshot is ahead of onchain.</Text>
|
|
51
51
|
<Text color={theme.textSubtle}>Local edits have not yet been rotated to the onchain pointer. Refetching discards them and reverts to the last published snapshot.</Text>
|
|
52
52
|
</Box>
|
|
53
53
|
) : null}
|
|
@@ -63,7 +63,7 @@ export const RecoveryConfirmScreen: React.FC<RecoveryConfirmScreenProps> = ({ mo
|
|
|
63
63
|
{ value: 'confirm', role: 'section', label: isPublish ? 'Save' : 'Refetch' },
|
|
64
64
|
{
|
|
65
65
|
value: 'confirm',
|
|
66
|
-
label: isPublish ? 'Yes, Save Snapshot Now' : 'Yes, Refetch From
|
|
66
|
+
label: isPublish ? 'Yes, Save Snapshot Now' : 'Yes, Refetch From Onchain',
|
|
67
67
|
hint: isPublish ? 'Sign and save the encrypted snapshot' : 'Wallet decrypts and overwrites local files',
|
|
68
68
|
},
|
|
69
69
|
{ value: 'back', role: 'section', label: 'Navigation' },
|
|
@@ -35,10 +35,8 @@ export const SavePromptScreen: React.FC<SavePromptScreenProps> = ({ workingStatu
|
|
|
35
35
|
<Box marginTop={1}>
|
|
36
36
|
<Select<SavePromptAction>
|
|
37
37
|
options={[
|
|
38
|
-
{ value: 'save-now', role: 'section', label: 'Save' },
|
|
39
38
|
{ value: 'save-now', label: 'Save now', hint: 'Sign once and save the encrypted snapshot' },
|
|
40
|
-
{ value: 'later',
|
|
41
|
-
{ value: 'later', label: 'Not now', hint: 'Ask again on the next ethagent launch', role: 'utility' },
|
|
39
|
+
{ value: 'later', label: 'Not now', hint: 'Ask again on the next launch', role: 'utility' },
|
|
42
40
|
]}
|
|
43
41
|
hintLayout="inline"
|
|
44
42
|
onSubmit={onSelect}
|
|
@@ -0,0 +1,151 @@
|
|
|
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 { EthagentIdentity } from '../../../../storage/config.js'
|
|
7
|
+
import { listSkills, listSkillFiles } from '../../../continuity/skills/loadSkills.js'
|
|
8
|
+
import type { SkillIndexEntry, SkillVisibility } from '../../../continuity/skills/types.js'
|
|
9
|
+
|
|
10
|
+
type SkillAction =
|
|
11
|
+
| { kind: 'open' }
|
|
12
|
+
| { kind: 'set-visibility'; visibility: SkillVisibility }
|
|
13
|
+
| { kind: 'delete' }
|
|
14
|
+
| { kind: 'back' }
|
|
15
|
+
| { kind: 'noop' }
|
|
16
|
+
|
|
17
|
+
interface SkillActionsScreenProps {
|
|
18
|
+
identity?: EthagentIdentity
|
|
19
|
+
relativePath: string
|
|
20
|
+
notice?: string
|
|
21
|
+
footer: React.ReactNode
|
|
22
|
+
onOpenSkill: (relativePath: string) => void
|
|
23
|
+
onSetVisibility: (relativePath: string, visibility: SkillVisibility) => void
|
|
24
|
+
onDelete: (relativePath: string) => void
|
|
25
|
+
onBack: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SkillActionsScreen: React.FC<SkillActionsScreenProps> = ({
|
|
29
|
+
identity,
|
|
30
|
+
relativePath,
|
|
31
|
+
notice,
|
|
32
|
+
footer,
|
|
33
|
+
onOpenSkill,
|
|
34
|
+
onSetVisibility,
|
|
35
|
+
onDelete,
|
|
36
|
+
onBack,
|
|
37
|
+
}) => {
|
|
38
|
+
const [entry, setEntry] = useState<SkillIndexEntry | null>(null)
|
|
39
|
+
const [supportingCount, setSupportingCount] = useState<number | null>(null)
|
|
40
|
+
const skillName = relativePath.split('/')[0] ?? ''
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let cancelled = false
|
|
44
|
+
if (!identity) return () => { cancelled = true }
|
|
45
|
+
listSkills(identity)
|
|
46
|
+
.then(list => {
|
|
47
|
+
if (cancelled) return
|
|
48
|
+
const match = list.find(item => item.relativePath === relativePath)
|
|
49
|
+
setEntry(match ?? null)
|
|
50
|
+
})
|
|
51
|
+
.catch(() => { if (!cancelled) setEntry(null) })
|
|
52
|
+
listSkillFiles(identity, skillName)
|
|
53
|
+
.then(files => {
|
|
54
|
+
if (cancelled) return
|
|
55
|
+
setSupportingCount(files.filter(f => f.relativePath !== 'SKILL.md').length)
|
|
56
|
+
})
|
|
57
|
+
.catch(() => { if (!cancelled) setSupportingCount(null) })
|
|
58
|
+
return () => { cancelled = true }
|
|
59
|
+
}, [identity, relativePath, skillName, notice])
|
|
60
|
+
|
|
61
|
+
const displayName = entry?.displayName ?? entry?.name ?? skillName
|
|
62
|
+
const visibility = entry?.visibility
|
|
63
|
+
const subtitle = notice ?? 'Open, change visibility, or delete this skill.'
|
|
64
|
+
|
|
65
|
+
const options: Array<SelectOption<SkillAction>> = []
|
|
66
|
+
const noop: SkillAction = { kind: 'noop' }
|
|
67
|
+
|
|
68
|
+
options.push({ value: noop, role: 'section', label: 'Open' })
|
|
69
|
+
options.push({
|
|
70
|
+
value: { kind: 'open' },
|
|
71
|
+
label: 'Open SKILL.md',
|
|
72
|
+
hint: 'View or edit this skill',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
options.push({ value: noop, role: 'section', label: 'Visibility' })
|
|
76
|
+
options.push(visibilityOption('private', visibility))
|
|
77
|
+
options.push(visibilityOption('discoverable', visibility))
|
|
78
|
+
options.push(visibilityOption('public', visibility))
|
|
79
|
+
|
|
80
|
+
options.push({ value: noop, role: 'section', label: 'Manage' })
|
|
81
|
+
options.push({
|
|
82
|
+
value: { kind: 'delete' },
|
|
83
|
+
label: 'Delete',
|
|
84
|
+
hint: 'Remove this skill folder and its supporting files',
|
|
85
|
+
})
|
|
86
|
+
options.push({
|
|
87
|
+
value: { kind: 'back' },
|
|
88
|
+
label: 'Back',
|
|
89
|
+
hint: 'Return to Skills',
|
|
90
|
+
role: 'utility',
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const leafMeta = formatLeafMeta(visibility, supportingCount)
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Surface title={`Skill · ${displayName}`} subtitle={subtitle} footer={footer}>
|
|
97
|
+
<Box flexDirection="column" marginTop={1}>
|
|
98
|
+
<Text color={theme.dim}>skills/</Text>
|
|
99
|
+
<Text>
|
|
100
|
+
<Text color={theme.dim}>└── </Text>
|
|
101
|
+
<Text color={theme.accentPeriwinkle} bold>{`${skillName}/SKILL.md`}</Text>
|
|
102
|
+
{leafMeta ? <Text color={theme.dim}>{` ${leafMeta}`}</Text> : null}
|
|
103
|
+
</Text>
|
|
104
|
+
</Box>
|
|
105
|
+
<Box marginTop={1}>
|
|
106
|
+
<Select<SkillAction>
|
|
107
|
+
options={options}
|
|
108
|
+
hintLayout="inline"
|
|
109
|
+
onSubmit={choice => {
|
|
110
|
+
if (choice.kind === 'open') return onOpenSkill(relativePath)
|
|
111
|
+
if (choice.kind === 'set-visibility') return onSetVisibility(relativePath, choice.visibility)
|
|
112
|
+
if (choice.kind === 'delete') return onDelete(relativePath)
|
|
113
|
+
if (choice.kind === 'back') return onBack()
|
|
114
|
+
}}
|
|
115
|
+
onCancel={onBack}
|
|
116
|
+
/>
|
|
117
|
+
</Box>
|
|
118
|
+
</Surface>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatLeafMeta(visibility: SkillVisibility | undefined, supportingCount: number | null): string {
|
|
123
|
+
if (!visibility) return ''
|
|
124
|
+
const fileLabel = supportingCount === null
|
|
125
|
+
? null
|
|
126
|
+
: supportingCount === 0 ? '1 file' : `${supportingCount + 1} files`
|
|
127
|
+
return fileLabel ? `${capitalize(visibility)} · ${fileLabel}` : capitalize(visibility)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function visibilityOption(level: SkillVisibility, current?: SkillVisibility): SelectOption<SkillAction> {
|
|
131
|
+
const isCurrent = current === level
|
|
132
|
+
const hint = visibilityHint(level) + (isCurrent ? ' · current' : '')
|
|
133
|
+
const base: SelectOption<SkillAction> = {
|
|
134
|
+
value: { kind: 'set-visibility', visibility: level },
|
|
135
|
+
label: `Set ${capitalize(level)}`,
|
|
136
|
+
hint,
|
|
137
|
+
}
|
|
138
|
+
if (isCurrent) base.labelColor = theme.accentPeriwinkle
|
|
139
|
+
return base
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function visibilityHint(level: SkillVisibility): string {
|
|
143
|
+
if (level === 'private') return 'Local-only. Not in skills.json.'
|
|
144
|
+
if (level === 'discoverable') return 'Default. Indexed with description.'
|
|
145
|
+
return 'Indexed with description and Agent Card link.'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function capitalize(value: string): string {
|
|
149
|
+
if (!value) return value
|
|
150
|
+
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
151
|
+
}
|
|
@@ -15,9 +15,6 @@ import type { ContinuityWorkingTreeStatus } from '../../../continuity/storage.js
|
|
|
15
15
|
type SkillsTreeAction =
|
|
16
16
|
| { kind: 'skill'; relativePath: string }
|
|
17
17
|
| { kind: 'new' }
|
|
18
|
-
| { kind: 'delete' }
|
|
19
|
-
| { kind: 'visibility' }
|
|
20
|
-
| { kind: 'view-manifest' }
|
|
21
18
|
| { kind: 'open-folder' }
|
|
22
19
|
| { kind: 'noop' }
|
|
23
20
|
| { kind: 'back' }
|
|
@@ -31,9 +28,6 @@ interface SkillsTreeScreenProps {
|
|
|
31
28
|
footer: React.ReactNode
|
|
32
29
|
onOpenSkill: (relativePath: string) => void
|
|
33
30
|
onNewSkill: () => void
|
|
34
|
-
onDelete: () => void
|
|
35
|
-
onVisibility: () => void
|
|
36
|
-
onViewPublicManifest: () => void
|
|
37
31
|
onOpenFolder: () => void
|
|
38
32
|
onBack: () => void
|
|
39
33
|
}
|
|
@@ -47,9 +41,6 @@ export const SkillsTreeScreen: React.FC<SkillsTreeScreenProps> = ({
|
|
|
47
41
|
footer,
|
|
48
42
|
onOpenSkill,
|
|
49
43
|
onNewSkill,
|
|
50
|
-
onDelete,
|
|
51
|
-
onVisibility,
|
|
52
|
-
onViewPublicManifest,
|
|
53
44
|
onOpenFolder,
|
|
54
45
|
onBack,
|
|
55
46
|
}) => {
|
|
@@ -92,7 +83,7 @@ export const SkillsTreeScreen: React.FC<SkillsTreeScreenProps> = ({
|
|
|
92
83
|
|
|
93
84
|
return (
|
|
94
85
|
<Surface title="Skills" subtitle={subtitle} footer={footer}>
|
|
95
|
-
<IdentitySummary identity={identity} config={config} workingStatus={workingStatus} />
|
|
86
|
+
<IdentitySummary identity={identity} config={config} workingStatus={workingStatus} compact />
|
|
96
87
|
{error && (
|
|
97
88
|
<Box marginTop={1}>
|
|
98
89
|
<Text color={theme.accentError}>{error}</Text>
|
|
@@ -110,9 +101,6 @@ export const SkillsTreeScreen: React.FC<SkillsTreeScreenProps> = ({
|
|
|
110
101
|
onSubmit={choice => {
|
|
111
102
|
if (choice.kind === 'skill') return onOpenSkill(choice.relativePath)
|
|
112
103
|
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
104
|
if (choice.kind === 'open-folder') return onOpenFolder()
|
|
117
105
|
if (choice.kind === 'back') return onBack()
|
|
118
106
|
}}
|
|
@@ -166,37 +154,17 @@ function buildOptions(
|
|
|
166
154
|
}
|
|
167
155
|
}
|
|
168
156
|
|
|
169
|
-
rows.push({ value: noopValue, role: 'notice', label: '' })
|
|
170
157
|
rows.push({ value: noopValue, role: 'section', label: 'Manage' })
|
|
171
158
|
rows.push({
|
|
172
159
|
value: { kind: 'new' },
|
|
173
160
|
label: 'New Skill',
|
|
174
161
|
hint: 'Scaffold a new skill folder with SKILL.md',
|
|
175
162
|
})
|
|
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
163
|
rows.push({
|
|
195
164
|
value: { kind: 'open-folder' },
|
|
196
165
|
label: 'Open Skills Folder',
|
|
197
166
|
hint: 'Reveal skills/ in your file manager',
|
|
198
167
|
})
|
|
199
|
-
rows.push({ value: noopValue, role: 'section', label: 'Navigation' })
|
|
200
168
|
rows.push({
|
|
201
169
|
value: { kind: 'back' },
|
|
202
170
|
label: 'Back',
|
|
@@ -20,15 +20,15 @@ export function changedContinuitySnapshotFiles(
|
|
|
20
20
|
workingStatus?: ContinuityWorkingTreeStatus | null,
|
|
21
21
|
): string[] {
|
|
22
22
|
if (!workingStatus?.localContentHashes || !workingStatus.publishedContentHashes) return []
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
23
|
+
const local = workingStatus.localContentHashes
|
|
24
|
+
const published = workingStatus.publishedContentHashes
|
|
25
|
+
const changed = (file: keyof typeof local): boolean =>
|
|
26
|
+
(local[file] ?? '') !== (published[file] ?? '')
|
|
27
|
+
const result: string[] = []
|
|
28
|
+
if (changed('SOUL.md')) result.push('SOUL.md')
|
|
29
|
+
if (changed('MEMORY.md')) result.push('MEMORY.md')
|
|
30
|
+
if (changed('skills.json') || changed('private-skills')) result.push('Skills')
|
|
31
|
+
return result
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function localChangeStatusView(
|
|
@@ -143,7 +143,7 @@ export const CreateFlow: React.FC<CreateFlowProps> = ({
|
|
|
143
143
|
<BusyScreen
|
|
144
144
|
title="Getting Ready"
|
|
145
145
|
subtitle={indicator}
|
|
146
|
-
label="checking IPFS storage and
|
|
146
|
+
label="checking IPFS storage and onchain..."
|
|
147
147
|
onCancel={onBack}
|
|
148
148
|
/>
|
|
149
149
|
)
|
|
@@ -54,7 +54,7 @@ export function renderCustodyStep({
|
|
|
54
54
|
footer={<Text color={theme.dim}>esc cancel</Text>}
|
|
55
55
|
>
|
|
56
56
|
<Box marginTop={1}>
|
|
57
|
-
<Text color={theme.textSubtle}>Reading vault state from
|
|
57
|
+
<Text color={theme.textSubtle}>Reading vault state from onchain...</Text>
|
|
58
58
|
</Box>
|
|
59
59
|
</Surface>
|
|
60
60
|
)
|