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.
- package/README.md +7 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +155 -15
- package/src/app/FirstRunTimeline.tsx +4 -0
- package/src/app/input/AppInputProvider.tsx +19 -0
- package/src/app/input/appInputParser.ts +19 -4
- package/src/chat/ChatBottomPane.tsx +3 -1
- package/src/chat/ChatScreen.tsx +7 -1
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +1 -1
- package/src/chat/chatTurnOrchestrator.ts +59 -0
- package/src/chat/input/ChatInput.tsx +3 -0
- package/src/chat/input/textCursor.ts +13 -3
- package/src/chat/transcript/TranscriptView.tsx +7 -5
- package/src/chat/transcript/transcriptViewport.ts +88 -17
- package/src/chat/views/PermissionPrompt.tsx +26 -26
- package/src/chat/views/PermissionsView.tsx +18 -12
- package/src/chat/views/RewindView.tsx +3 -1
- package/src/cli/ResetConfirmView.tsx +24 -9
- package/src/identity/continuity/editor.ts +27 -2
- package/src/identity/continuity/envelope.ts +134 -9
- package/src/identity/continuity/publicSkills.ts +54 -1
- package/src/identity/continuity/skills/frontmatter.ts +183 -0
- package/src/identity/continuity/skills/loadSkills.ts +609 -0
- package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
- package/src/identity/continuity/skills/scaffold.ts +52 -0
- package/src/identity/continuity/skills/types.ts +30 -0
- package/src/identity/continuity/storage/defaults.ts +28 -47
- package/src/identity/continuity/storage/files.ts +1 -0
- package/src/identity/continuity/storage/paths.ts +1 -0
- package/src/identity/continuity/storage/scaffold.ts +25 -23
- package/src/identity/continuity/storage/status.ts +34 -5
- package/src/identity/continuity/storage/types.ts +3 -2
- package/src/identity/continuity/storage.ts +3 -0
- package/src/identity/hub/OperationalRoutes.tsx +79 -5
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +7 -73
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +6 -6
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -2
- package/src/identity/hub/continuity/effects.ts +36 -5
- package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
- package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
- package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +151 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +181 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +9 -8
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/create/CreateFlow.tsx +1 -1
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- 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 +15 -0
- package/src/identity/hub/profile/EditProfileFlow.tsx +5 -5
- package/src/identity/hub/profile/effects.ts +16 -3
- package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
- package/src/identity/hub/restore/apply.ts +12 -1
- package/src/identity/hub/restore/recovery.ts +14 -4
- package/src/identity/hub/restore/resolve.ts +1 -1
- package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
- package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
- package/src/identity/hub/shared/components/IdentitySummary.tsx +118 -54
- package/src/identity/hub/shared/components/MenuScreen.tsx +21 -18
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +4 -4
- package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
- package/src/identity/hub/shared/effects/sync.ts +16 -3
- package/src/identity/hub/shared/model/copy.ts +2 -4
- package/src/identity/hub/transfer/effects.ts +15 -2
- package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
- package/src/identity/hub/useIdentityHubController.ts +5 -1
- package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
- package/src/identity/wallet/page/copy.ts +43 -43
- package/src/mcp/manager.ts +1 -1
- package/src/models/ModelPicker.tsx +89 -84
- package/src/models/llamacpp.ts +160 -11
- package/src/models/llamacppPreflight.ts +1 -16
- package/src/models/modelPickerOptions.ts +45 -37
- package/src/providers/contracts.ts +1 -0
- package/src/providers/openai-chat.ts +50 -9
- package/src/providers/openai-responses.ts +19 -4
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/contracts.ts +10 -0
- package/src/tools/deleteFileTool.ts +1 -1
- package/src/tools/editTool.ts +1 -1
- package/src/tools/listDirectoryTool.ts +1 -1
- package/src/tools/listSkillFilesTool.ts +77 -0
- package/src/tools/listSkillsTool.ts +68 -0
- package/src/tools/mcpResourceTools.ts +2 -2
- package/src/tools/privateContinuityReadTool.ts +1 -1
- package/src/tools/readSkillTool.ts +107 -0
- package/src/tools/readTool.ts +1 -1
- package/src/tools/registry.ts +6 -0
- package/src/tools/writeFileTool.ts +22 -2
- package/src/ui/Spinner.tsx +1 -1
- package/src/identity/continuity/localBackup.ts +0 -249
- package/src/identity/continuity/zipWriter.ts +0 -95
- package/src/identity/hub/continuity/index.ts +0 -7
- package/src/identity/hub/ens/index.ts +0 -11
- package/src/identity/hub/restore/index.ts +0 -22
|
@@ -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,10 +63,10 @@ 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
|
-
{ value: 'back', role: 'section', label: '
|
|
69
|
+
{ value: 'back', role: 'section', label: 'Navigation' },
|
|
70
70
|
{
|
|
71
71
|
value: 'back',
|
|
72
72
|
label: 'No, Go Back',
|
|
@@ -35,9 +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', label: 'Not now', hint: 'Ask again on the next
|
|
39
|
+
{ value: 'later', label: 'Not now', hint: 'Ask again on the next launch', role: 'utility' },
|
|
41
40
|
]}
|
|
42
41
|
hintLayout="inline"
|
|
43
42
|
onSubmit={onSelect}
|
|
@@ -4,14 +4,25 @@ import type { EthagentIdentity } from '../../../storage/config.js'
|
|
|
4
4
|
import {
|
|
5
5
|
continuityVaultStatus,
|
|
6
6
|
prepareSyncedIdentityMarkdownScaffold,
|
|
7
|
+
prepareSyncedSkillsTree,
|
|
7
8
|
readContinuityFiles,
|
|
8
9
|
readPublicSkillsFile,
|
|
9
10
|
writeIdentityMarkdownScaffold,
|
|
10
11
|
type IdentityMarkdownScaffold,
|
|
11
12
|
} from '../../continuity/storage.js'
|
|
13
|
+
import {
|
|
14
|
+
appendPublicSkillEntries,
|
|
15
|
+
defaultPublicSkillsProfile as basePublicSkillsProfile,
|
|
16
|
+
} from '../../continuity/publicSkills.js'
|
|
17
|
+
import {
|
|
18
|
+
derivePublicSkillEntries,
|
|
19
|
+
syncPublicSkillsManifest,
|
|
20
|
+
} from '../../continuity/skills/publicSkillsSync.js'
|
|
12
21
|
import {
|
|
13
22
|
createWalletRestoreAccessChallenge,
|
|
14
23
|
serializeContinuitySnapshotEnvelope,
|
|
24
|
+
type ContinuityFiles,
|
|
25
|
+
type ContinuitySkillsTree,
|
|
15
26
|
type WalletChallengePurpose,
|
|
16
27
|
} from '../../continuity/envelope.js'
|
|
17
28
|
import {
|
|
@@ -78,6 +89,11 @@ type RebackupPreparedTransaction = {
|
|
|
78
89
|
publicSkills: PublicSkillsMetadata
|
|
79
90
|
identity: EthagentIdentity
|
|
80
91
|
markdownScaffold?: IdentityMarkdownScaffold
|
|
92
|
+
publishedSources: {
|
|
93
|
+
privateFiles: ContinuityFiles
|
|
94
|
+
publicSkills: string
|
|
95
|
+
skills: ContinuitySkillsTree
|
|
96
|
+
}
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
export async function runRebackupPreflight(
|
|
@@ -224,18 +240,22 @@ async function runRebackupSigningInner(
|
|
|
224
240
|
const continuityFiles = markdownScaffold
|
|
225
241
|
? { 'SOUL.md': markdownScaffold['SOUL.md'], 'MEMORY.md': markdownScaffold['MEMORY.md'] }
|
|
226
242
|
: await readContinuityFiles(nextIdentityForFiles)
|
|
227
|
-
const publicSkillsJson =
|
|
228
|
-
? markdownScaffold['skills.json']
|
|
229
|
-
: await readPublicSkillsFile(nextIdentityForFiles)
|
|
243
|
+
const publicSkillsJson = await syncPublicSkillsManifest(nextIdentityForFiles)
|
|
230
244
|
const publicSkillsPin = await addToIpfs(DEFAULT_IPFS_API_URL, publicSkillsJson, fetch, { pinataJwt: step.pinataJwt })
|
|
231
245
|
assertVerifiedPin(publicSkillsPin)
|
|
246
|
+
const publicSkillEntries = await derivePublicSkillEntries(nextIdentityForFiles)
|
|
247
|
+
const augmentedPublicProfile = appendPublicSkillEntries(
|
|
248
|
+
basePublicSkillsProfile(nextIdentityForFiles),
|
|
249
|
+
publicSkillEntries,
|
|
250
|
+
)
|
|
232
251
|
const agentCardPin = await addToIpfs(
|
|
233
252
|
DEFAULT_IPFS_API_URL,
|
|
234
|
-
serializeAgentCard(createAgentCard(
|
|
253
|
+
serializeAgentCard(createAgentCard(augmentedPublicProfile)),
|
|
235
254
|
fetch,
|
|
236
255
|
{ pinataJwt: step.pinataJwt },
|
|
237
256
|
)
|
|
238
257
|
assertVerifiedPin(agentCardPin)
|
|
258
|
+
const skillsTree = await prepareSyncedSkillsTree(nextIdentityForFiles)
|
|
239
259
|
const envelope = createContinuityEnvelopeForSave({
|
|
240
260
|
identity: nextIdentityForFiles,
|
|
241
261
|
registry: step.registry,
|
|
@@ -244,6 +264,7 @@ async function runRebackupSigningInner(
|
|
|
244
264
|
walletSignature: wallet.signature,
|
|
245
265
|
state,
|
|
246
266
|
files: continuityFiles,
|
|
267
|
+
skills: skillsTree,
|
|
247
268
|
walletAccess,
|
|
248
269
|
...(challengePurpose ? { challengePurpose } : {}),
|
|
249
270
|
})
|
|
@@ -310,6 +331,11 @@ async function runRebackupSigningInner(
|
|
|
310
331
|
publicSkills,
|
|
311
332
|
identity: { ...step.identity, state },
|
|
312
333
|
...(markdownScaffold ? { markdownScaffold } : {}),
|
|
334
|
+
publishedSources: {
|
|
335
|
+
privateFiles: continuityFiles,
|
|
336
|
+
publicSkills: publicSkillsJson,
|
|
337
|
+
skills: skillsTree,
|
|
338
|
+
},
|
|
313
339
|
},
|
|
314
340
|
}
|
|
315
341
|
}
|
|
@@ -330,6 +356,11 @@ async function runRebackupSigningInner(
|
|
|
330
356
|
publicSkills,
|
|
331
357
|
identity: { ...step.identity, state },
|
|
332
358
|
...(markdownScaffold ? { markdownScaffold } : {}),
|
|
359
|
+
publishedSources: {
|
|
360
|
+
privateFiles: continuityFiles,
|
|
361
|
+
publicSkills: publicSkillsJson,
|
|
362
|
+
skills: skillsTree,
|
|
363
|
+
},
|
|
333
364
|
},
|
|
334
365
|
}
|
|
335
366
|
},
|
|
@@ -359,7 +390,7 @@ async function runRebackupSigningInner(
|
|
|
359
390
|
await writeIdentityMarkdownScaffold(nextIdentity, result.prepared.markdownScaffold)
|
|
360
391
|
}
|
|
361
392
|
await recordPublishedContinuitySnapshot({ identity: nextIdentity, label: 'published encrypted snapshot' }).catch(() => null)
|
|
362
|
-
await markCurrentContinuityFilesPublished(nextIdentity).catch(() => null)
|
|
393
|
+
await markCurrentContinuityFilesPublished(nextIdentity, result.prepared.publishedSources).catch(() => null)
|
|
363
394
|
const resolverSyncWarning = await syncVaultOperatorsAfterOwnerSave({
|
|
364
395
|
beforeIdentity: step.identity,
|
|
365
396
|
afterIdentity: nextIdentity,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { Select } from '../../../../ui/Select.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
import type { EthagentIdentity } from '../../../../storage/config.js'
|
|
7
|
+
import {
|
|
8
|
+
listSkills,
|
|
9
|
+
listSkillFiles,
|
|
10
|
+
type SkillFileEntry,
|
|
11
|
+
} from '../../../continuity/skills/loadSkills.js'
|
|
12
|
+
import { continuityVaultRef } from '../../../continuity/storage.js'
|
|
13
|
+
import type { SkillIndexEntry } from '../../../continuity/skills/types.js'
|
|
14
|
+
|
|
15
|
+
type ConfirmChoice = 'delete' | 'cancel'
|
|
16
|
+
|
|
17
|
+
type DeleteTarget = { kind: 'skill'; relativePath: string }
|
|
18
|
+
|
|
19
|
+
interface DeleteSkillConfirmScreenProps {
|
|
20
|
+
identity?: EthagentIdentity
|
|
21
|
+
target: DeleteTarget
|
|
22
|
+
footer: React.ReactNode
|
|
23
|
+
onConfirm: () => void
|
|
24
|
+
onCancel: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_LISTED_FILES = 10
|
|
28
|
+
|
|
29
|
+
export const DeleteSkillConfirmScreen: React.FC<DeleteSkillConfirmScreenProps> = ({
|
|
30
|
+
identity,
|
|
31
|
+
target,
|
|
32
|
+
footer,
|
|
33
|
+
onConfirm,
|
|
34
|
+
onCancel,
|
|
35
|
+
}) => {
|
|
36
|
+
const [entries, setEntries] = useState<SkillIndexEntry[] | null>(null)
|
|
37
|
+
const [files, setFiles] = useState<SkillFileEntry[] | null>(null)
|
|
38
|
+
const skillName = target.relativePath.split('/')[0] ?? ''
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let cancelled = false
|
|
42
|
+
if (!identity) { setEntries([]); setFiles([]); return () => { cancelled = true } }
|
|
43
|
+
setEntries(null)
|
|
44
|
+
setFiles(null)
|
|
45
|
+
listSkills(identity)
|
|
46
|
+
.then(result => { if (!cancelled) setEntries(result) })
|
|
47
|
+
.catch(() => { if (!cancelled) setEntries([]) })
|
|
48
|
+
listSkillFiles(identity, skillName)
|
|
49
|
+
.then(result => { if (!cancelled) setFiles(result) })
|
|
50
|
+
.catch(() => { if (!cancelled) setFiles([]) })
|
|
51
|
+
return () => { cancelled = true }
|
|
52
|
+
}, [identity, target.relativePath, skillName])
|
|
53
|
+
|
|
54
|
+
const ref = identity ? continuityVaultRef(identity) : undefined
|
|
55
|
+
const skillMissing = entries !== null && !entries.some(entry => entry.relativePath === target.relativePath)
|
|
56
|
+
const supportingCount = (files ?? []).filter(f => f.relativePath !== 'SKILL.md').length
|
|
57
|
+
const subtitle = skillMissing
|
|
58
|
+
? `Skill ${skillName} was already removed elsewhere. Cancel and refresh.`
|
|
59
|
+
: supportingCount > 0
|
|
60
|
+
? `Removes ${skillName}/ and ${supportingCount} supporting file${supportingCount === 1 ? '' : 's'}. This cannot be undone.`
|
|
61
|
+
: `Removes ${skillName}/. This cannot be undone.`
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Surface title={`Delete ${skillName}?`} subtitle={subtitle} footer={footer} tone="error">
|
|
65
|
+
<Box marginTop={1} flexDirection="column">
|
|
66
|
+
<Text color={theme.dim}>This will remove:</Text>
|
|
67
|
+
{ref && (
|
|
68
|
+
<Box marginLeft={2} flexDirection="column">
|
|
69
|
+
<Text color={theme.text}>{ref.skillsDir}/{skillName}/</Text>
|
|
70
|
+
{files === null ? (
|
|
71
|
+
<Text color={theme.dim}>(loading folder contents...)</Text>
|
|
72
|
+
) : (
|
|
73
|
+
<FolderContents files={files} />
|
|
74
|
+
)}
|
|
75
|
+
</Box>
|
|
76
|
+
)}
|
|
77
|
+
</Box>
|
|
78
|
+
<Box marginTop={1}>
|
|
79
|
+
<Select<ConfirmChoice>
|
|
80
|
+
options={[
|
|
81
|
+
{ value: 'delete', label: 'Yes, delete', hint: skillMissing ? 'Skill already missing on disk' : 'Permanent. Will be reflected in the next snapshot.', bold: true, disabled: skillMissing },
|
|
82
|
+
{ value: 'cancel', label: 'No, back', hint: 'Return to the delete picker', role: 'utility' },
|
|
83
|
+
]}
|
|
84
|
+
hintLayout="inline"
|
|
85
|
+
initialIndex={1}
|
|
86
|
+
onSubmit={choice => {
|
|
87
|
+
if (choice === 'delete') return onConfirm()
|
|
88
|
+
return onCancel()
|
|
89
|
+
}}
|
|
90
|
+
onCancel={onCancel}
|
|
91
|
+
/>
|
|
92
|
+
</Box>
|
|
93
|
+
</Surface>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const FolderContents: React.FC<{ files: SkillFileEntry[] }> = ({ files }) => {
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
return <Text color={theme.dim}>(empty folder)</Text>
|
|
100
|
+
}
|
|
101
|
+
const shown = files.slice(0, MAX_LISTED_FILES)
|
|
102
|
+
const extra = files.length - shown.length
|
|
103
|
+
return (
|
|
104
|
+
<Box flexDirection="column" marginTop={1}>
|
|
105
|
+
<Text color={theme.dim}>Files to be removed:</Text>
|
|
106
|
+
{shown.map(f => (
|
|
107
|
+
<Text key={f.relativePath} color={theme.text}> · {f.relativePath}</Text>
|
|
108
|
+
))}
|
|
109
|
+
{extra > 0 && <Text color={theme.dim}> · ...and {extra} more</Text>}
|
|
110
|
+
</Box>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { TextInput } from '../../../../ui/TextInput.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
|
|
7
|
+
interface NewSkillScreenProps {
|
|
8
|
+
error?: string
|
|
9
|
+
footer: React.ReactNode
|
|
10
|
+
onSubmit: (name: string) => void
|
|
11
|
+
onCancel: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NewSkillScreen: React.FC<NewSkillScreenProps> = ({
|
|
15
|
+
error,
|
|
16
|
+
footer,
|
|
17
|
+
onSubmit,
|
|
18
|
+
onCancel,
|
|
19
|
+
}) => (
|
|
20
|
+
<Surface
|
|
21
|
+
title="New Skill"
|
|
22
|
+
subtitle="Folder name. The skill activates by this name."
|
|
23
|
+
footer={footer}
|
|
24
|
+
>
|
|
25
|
+
<Box marginTop={1} flexDirection="column">
|
|
26
|
+
<TextInput
|
|
27
|
+
label="Folder name"
|
|
28
|
+
placeholder="<folder>"
|
|
29
|
+
validate={value => validateSegment(value)}
|
|
30
|
+
onSubmit={raw => {
|
|
31
|
+
const trimmed = raw.trim()
|
|
32
|
+
if (validateSegment(trimmed) === null) onSubmit(trimmed)
|
|
33
|
+
}}
|
|
34
|
+
onCancel={onCancel}
|
|
35
|
+
/>
|
|
36
|
+
<Box marginTop={1}>
|
|
37
|
+
<Text color={theme.dim}>
|
|
38
|
+
Saved as skills/<folder>/SKILL.md. Add supporting files later by dropping them into the same folder.
|
|
39
|
+
</Text>
|
|
40
|
+
</Box>
|
|
41
|
+
{error && (
|
|
42
|
+
<Box marginTop={1}>
|
|
43
|
+
<Text color={theme.accentError}>{error}</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
)}
|
|
46
|
+
</Box>
|
|
47
|
+
</Surface>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
export function validateSegment(value: string): string | null {
|
|
51
|
+
const trimmed = value.trim()
|
|
52
|
+
if (!trimmed) return 'Enter a name.'
|
|
53
|
+
if (trimmed.includes('/') || trimmed.includes('\\')) return 'One segment only, no slashes.'
|
|
54
|
+
if (trimmed === '.' || trimmed === '..') return 'Reserved name.'
|
|
55
|
+
if (!/^[A-Za-z0-9._-]+$/.test(trimmed)) return 'Only letters, digits, dashes, underscores, dots.'
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { Surface } from '../../../../ui/Surface.js'
|
|
4
|
+
import { Select } from '../../../../ui/Select.js'
|
|
5
|
+
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
import type { SkillVisibility } from '../../../continuity/skills/types.js'
|
|
7
|
+
|
|
8
|
+
interface NewSkillVisibilityScreenProps {
|
|
9
|
+
name: string
|
|
10
|
+
error?: string
|
|
11
|
+
footer: React.ReactNode
|
|
12
|
+
onSelect: (visibility: SkillVisibility) => void
|
|
13
|
+
onCancel: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const NewSkillVisibilityScreen: React.FC<NewSkillVisibilityScreenProps> = ({
|
|
17
|
+
name,
|
|
18
|
+
error,
|
|
19
|
+
footer,
|
|
20
|
+
onSelect,
|
|
21
|
+
onCancel,
|
|
22
|
+
}) => (
|
|
23
|
+
<Surface
|
|
24
|
+
title={`Visibility · ${name}`}
|
|
25
|
+
subtitle="Discoverable is the default. You can change it later from Change Visibility."
|
|
26
|
+
footer={footer}
|
|
27
|
+
>
|
|
28
|
+
{error && (
|
|
29
|
+
<Box marginTop={1}>
|
|
30
|
+
<Text color={theme.accentError}>{error}</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
)}
|
|
33
|
+
<Box marginTop={1}>
|
|
34
|
+
<Select<SkillVisibility | 'back'>
|
|
35
|
+
options={[
|
|
36
|
+
{ value: 'private', label: 'Private', hint: 'Local-only. Not in skills.json.' },
|
|
37
|
+
{ value: 'discoverable', label: 'Discoverable', hint: 'Default. Indexed in skills.json with description.' },
|
|
38
|
+
{ value: 'public', label: 'Public', hint: 'Indexed in skills.json and Agent Card.' },
|
|
39
|
+
{ value: 'back', role: 'section', label: 'Navigation' },
|
|
40
|
+
{ value: 'back', label: 'Back', hint: 'Return to the name step', role: 'utility' },
|
|
41
|
+
]}
|
|
42
|
+
hintLayout="inline"
|
|
43
|
+
initialIndex={1}
|
|
44
|
+
onSubmit={choice => {
|
|
45
|
+
if (choice === 'back') return onCancel()
|
|
46
|
+
return onSelect(choice)
|
|
47
|
+
}}
|
|
48
|
+
onCancel={onCancel}
|
|
49
|
+
/>
|
|
50
|
+
</Box>
|
|
51
|
+
</Surface>
|
|
52
|
+
)
|
|
@@ -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
|
+
}
|