ethagent 2.4.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.
- 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 +125 -0
- package/src/identity/continuity/publicSkills.ts +37 -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 +105 -3
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
- 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/DeleteSkillScreen.tsx +123 -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/SkillVisibilityScreen.tsx +171 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +3 -2
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- package/src/identity/hub/identityHubReducer.ts +21 -0
- 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 +11 -1
- 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 +97 -53
- package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
- 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/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 +43 -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
|
@@ -72,16 +72,16 @@ export const PermissionPrompt: React.FC<PermissionPromptProps> = ({ request, onD
|
|
|
72
72
|
export function permissionOptionsForRequest(request: PermissionRequest): Array<{ value: PermissionDecision; label: string; hint?: string; disabled?: boolean }> {
|
|
73
73
|
if (request.kind === 'bash') {
|
|
74
74
|
return [
|
|
75
|
-
{ value: 'allow-once', label: 'Allow
|
|
75
|
+
{ value: 'allow-once', label: 'Allow once', hint: 'Approve only this command execution' },
|
|
76
76
|
{
|
|
77
77
|
value: 'allow-command-project',
|
|
78
|
-
label: 'Allow
|
|
78
|
+
label: 'Allow exact command',
|
|
79
79
|
hint: 'Remember this exact command for this project',
|
|
80
80
|
disabled: !request.canPersistExact,
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
value: 'allow-command-prefix-project',
|
|
84
|
-
label: request.commandPrefix ? `Allow ${request.commandPrefix}
|
|
84
|
+
label: request.commandPrefix ? `Allow ${request.commandPrefix} commands` : 'Allow command family',
|
|
85
85
|
hint: 'Remember this base command in this working directory for this project',
|
|
86
86
|
disabled: !request.canPersistPrefix,
|
|
87
87
|
},
|
|
@@ -91,63 +91,63 @@ export function permissionOptionsForRequest(request: PermissionRequest): Array<{
|
|
|
91
91
|
|
|
92
92
|
if (request.kind === 'mcp') {
|
|
93
93
|
const risk = request.destructive
|
|
94
|
-
? '
|
|
94
|
+
? 'Server marks this tool as destructive'
|
|
95
95
|
: request.openWorld
|
|
96
|
-
? '
|
|
96
|
+
? 'Server marks this tool as open-world'
|
|
97
97
|
: request.readOnly
|
|
98
|
-
? '
|
|
99
|
-
: '
|
|
98
|
+
? 'Server marks this tool as read-only'
|
|
99
|
+
: 'Server did not mark this tool read-only'
|
|
100
100
|
return [
|
|
101
|
-
{ value: 'allow-once', label: '
|
|
102
|
-
{ value: 'allow-mcp-tool-project', label: '
|
|
101
|
+
{ value: 'allow-once', label: 'Allow once', hint: risk },
|
|
102
|
+
{ value: 'allow-mcp-tool-project', label: 'Always allow this MCP tool', hint: request.toolKey },
|
|
103
103
|
{
|
|
104
104
|
value: 'allow-mcp-server-project',
|
|
105
|
-
label: `
|
|
106
|
-
hint: '
|
|
105
|
+
label: `Always allow ${request.serverName}`,
|
|
106
|
+
hint: 'Remember all tools from this MCP server for this project',
|
|
107
107
|
disabled: !request.canPersistServer,
|
|
108
108
|
},
|
|
109
|
-
{ value: 'deny', label: '
|
|
109
|
+
{ value: 'deny', label: 'Deny', hint: 'Return a denial back to the model' },
|
|
110
110
|
]
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (request.kind === 'delete') {
|
|
114
114
|
return [
|
|
115
|
-
{ value: 'allow-once', label: '
|
|
116
|
-
{ value: 'deny', label: '
|
|
115
|
+
{ value: 'allow-once', label: 'Delete this file', hint: 'Approve this deletion only' },
|
|
116
|
+
{ value: 'deny', label: 'Deny', hint: 'Keep the file unchanged' },
|
|
117
117
|
]
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (request.kind === 'private-continuity-read') {
|
|
121
121
|
return [
|
|
122
|
-
{ value: 'allow-once', label: '
|
|
123
|
-
{ value: 'deny', label: '
|
|
122
|
+
{ value: 'allow-once', label: 'Allow once', hint: `Read ${request.file}` },
|
|
123
|
+
{ value: 'deny', label: 'Deny', hint: 'Keep private continuity hidden' },
|
|
124
124
|
]
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
if (request.kind === 'private-continuity-edit') {
|
|
128
128
|
return [
|
|
129
|
-
{ value: 'allow-once', label: 'Approve
|
|
129
|
+
{ value: 'allow-once', label: 'Approve once', hint: `Apply this edit to ${request.file}` },
|
|
130
130
|
{ value: 'deny', label: 'Deny', hint: 'Keep private continuity unchanged' },
|
|
131
131
|
]
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
return [
|
|
135
|
-
{ value: 'allow-once', label: '
|
|
136
|
-
{ value: 'allow-path-project', label: '
|
|
137
|
-
{ value: 'allow-directory-project', label: '
|
|
135
|
+
{ value: 'allow-once', label: 'Allow once', hint: 'Approve only this action' },
|
|
136
|
+
{ value: 'allow-path-project', label: 'Always allow this file', hint: request.relativePath },
|
|
137
|
+
{ value: 'allow-directory-project', label: 'Always allow this folder', hint: request.directoryPath },
|
|
138
138
|
{
|
|
139
139
|
value: 'allow-kind-project',
|
|
140
140
|
label:
|
|
141
141
|
request.kind === 'edit'
|
|
142
|
-
? '
|
|
142
|
+
? 'Always allow edits'
|
|
143
143
|
: request.kind === 'write'
|
|
144
|
-
? '
|
|
144
|
+
? 'Always allow writes'
|
|
145
145
|
: request.kind === 'cd'
|
|
146
|
-
? '
|
|
147
|
-
: '
|
|
148
|
-
hint: '
|
|
146
|
+
? 'Always allow directory changes'
|
|
147
|
+
: 'Always allow reads',
|
|
148
|
+
hint: 'Remember this tool kind for this project',
|
|
149
149
|
},
|
|
150
|
-
{ value: 'deny', label: '
|
|
150
|
+
{ value: 'deny', label: 'Deny', hint: 'Return a denial back to the model' },
|
|
151
151
|
]
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -118,18 +118,24 @@ export const PermissionsView: React.FC<PermissionsViewProps> = ({
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function buildOptions(rules: SessionPermissionRule[]): Array<SelectOption<SessionPermissionRule | typeof CLEAR_ALL_VALUE>> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
const out: Array<SelectOption<SessionPermissionRule | typeof CLEAR_ALL_VALUE>> = []
|
|
122
|
+
if (rules.length > 0) {
|
|
123
|
+
out.push({ value: CLEAR_ALL_VALUE, role: 'section', label: 'Saved Rules' })
|
|
124
|
+
for (const rule of rules) {
|
|
125
|
+
out.push({
|
|
126
|
+
value: rule,
|
|
127
|
+
label: describeRule(rule),
|
|
128
|
+
hint: describeRuleScope(rule),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
out.push({ value: CLEAR_ALL_VALUE, role: 'section', label: 'Manage' })
|
|
133
|
+
out.push({
|
|
134
|
+
value: CLEAR_ALL_VALUE,
|
|
135
|
+
label: 'Remove all saved rules',
|
|
136
|
+
hint: 'Clear all remembered permissions for this project',
|
|
137
|
+
})
|
|
138
|
+
return out
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
function describeRule(rule: SessionPermissionRule): string {
|
|
@@ -142,11 +142,13 @@ export const RewindView: React.FC<RewindViewProps> = ({
|
|
|
142
142
|
const canRestoreCode = selectedRow.entries.length > 0
|
|
143
143
|
|
|
144
144
|
const actionOptions: Array<SelectOption<ConfirmOption>> = [
|
|
145
|
+
{ value: 'both', role: 'section', label: 'Restore' },
|
|
145
146
|
{ value: 'both', prefix: '1.', label: 'Restore code and conversation', disabled: !canRestoreCode },
|
|
146
147
|
{ value: 'conversation', prefix: '2.', label: 'Restore conversation' },
|
|
147
148
|
{ value: 'code', prefix: '3.', label: 'Restore code', disabled: !canRestoreCode },
|
|
148
149
|
{ value: 'summarize', prefix: '4.', label: 'Summarize from here' },
|
|
149
|
-
{ value: 'nevermind',
|
|
150
|
+
{ value: 'nevermind', role: 'section', label: 'Navigation' },
|
|
151
|
+
{ value: 'nevermind', prefix: '5.', label: 'Never mind', role: 'utility' },
|
|
150
152
|
]
|
|
151
153
|
const defaultValue: ConfirmOption = canRestoreCode ? 'both' : 'conversation'
|
|
152
154
|
const initialIndex = actionOptions.findIndex(option => option.value === defaultValue && !option.disabled)
|
|
@@ -5,6 +5,8 @@ import { Select } from '../ui/Select.js'
|
|
|
5
5
|
import { theme } from '../ui/theme.js'
|
|
6
6
|
import type { FactoryResetPlan } from '../storage/factoryReset.js'
|
|
7
7
|
|
|
8
|
+
type SectionTone = 'destructive' | 'safe' | 'untouched'
|
|
9
|
+
|
|
8
10
|
export const ResetConfirmView: React.FC<{
|
|
9
11
|
plan: FactoryResetPlan
|
|
10
12
|
onDone: (confirmed: boolean) => void
|
|
@@ -16,17 +18,22 @@ export const ResetConfirmView: React.FC<{
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
return (
|
|
19
|
-
<Surface
|
|
21
|
+
<Surface
|
|
22
|
+
title="Reset Local Data?"
|
|
23
|
+
subtitle="Deletes this machine's ethagent data. Models and onchain records stay."
|
|
24
|
+
footer="enter select · esc cancel"
|
|
25
|
+
tone="error"
|
|
26
|
+
>
|
|
20
27
|
<Box flexDirection="column">
|
|
21
|
-
<Section title="Deletes" lines={[
|
|
28
|
+
<Section tone="destructive" title="Deletes" lines={[
|
|
22
29
|
'Identity files, sessions, history, credentials',
|
|
23
30
|
localDataLine(plan.deletePaths.length),
|
|
24
31
|
]} />
|
|
25
|
-
<Section title="Keeps" lines={[
|
|
32
|
+
<Section tone="safe" title="Keeps" lines={[
|
|
26
33
|
'Local GGUF models and llama.cpp runners',
|
|
27
34
|
...(plan.preservedPaths.length > 0 ? [`${plan.preservedPaths.length} local model path${plan.preservedPaths.length === 1 ? '' : 's'}`] : ['No local model assets found']),
|
|
28
35
|
]} />
|
|
29
|
-
<Section title="Not Touched" lines={[
|
|
36
|
+
<Section tone="untouched" title="Not Touched" lines={[
|
|
30
37
|
'ERC-8004 tokens and onchain records',
|
|
31
38
|
'IPFS snapshots and public metadata',
|
|
32
39
|
]} />
|
|
@@ -34,9 +41,11 @@ export const ResetConfirmView: React.FC<{
|
|
|
34
41
|
<Box marginTop={1}>
|
|
35
42
|
<Select<'confirm' | 'cancel'>
|
|
36
43
|
options={[
|
|
37
|
-
{ value: 'confirm', label: '
|
|
38
|
-
{ value: 'cancel', label: '
|
|
44
|
+
{ value: 'confirm', label: 'Yes, reset local data', hint: 'Delete local ethagent data on this machine', bold: true },
|
|
45
|
+
{ value: 'cancel', label: 'No, cancel', hint: 'Leave local data unchanged', role: 'utility' },
|
|
39
46
|
]}
|
|
47
|
+
hintLayout="inline"
|
|
48
|
+
initialIndex={1}
|
|
40
49
|
onSubmit={choice => finish(choice === 'confirm')}
|
|
41
50
|
onCancel={() => finish(false)}
|
|
42
51
|
/>
|
|
@@ -45,15 +54,21 @@ export const ResetConfirmView: React.FC<{
|
|
|
45
54
|
)
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
const Section: React.FC<{ title: string; lines: string[] }> = ({ title, lines }) => (
|
|
57
|
+
const Section: React.FC<{ tone: SectionTone; title: string; lines: string[] }> = ({ tone, title, lines }) => (
|
|
49
58
|
<Box flexDirection="column" marginBottom={1}>
|
|
50
|
-
<Text color={
|
|
59
|
+
<Text color={sectionTitleColor(tone)}>{title}</Text>
|
|
51
60
|
{lines.map(line => (
|
|
52
|
-
<Text key={line} color={theme.textSubtle}
|
|
61
|
+
<Text key={line} color={theme.textSubtle}>· {line}</Text>
|
|
53
62
|
))}
|
|
54
63
|
</Box>
|
|
55
64
|
)
|
|
56
65
|
|
|
66
|
+
function sectionTitleColor(tone: SectionTone): string {
|
|
67
|
+
if (tone === 'destructive') return theme.accentError
|
|
68
|
+
if (tone === 'safe') return theme.accentPeriwinkle
|
|
69
|
+
return theme.dim
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
function localDataLine(count: number): string {
|
|
58
73
|
if (count === 0) return 'No local ethagent data found'
|
|
59
74
|
return `${count} local path${count === 1 ? '' : 's'} under ~/.ethagent`
|
|
@@ -12,9 +12,20 @@ type EditorCommand = {
|
|
|
12
12
|
shell?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function openFileInEditor(file: string): Promise<EditorOpenResult> {
|
|
15
|
+
export async function openFileInEditor(file: string): Promise<EditorOpenResult> {
|
|
16
|
+
if (isVscodeEnvironment()) {
|
|
17
|
+
const vscode = vscodeEditorCommand(file)
|
|
18
|
+
const result = await openEditorCommand(vscode)
|
|
19
|
+
if (result.ok) return result
|
|
20
|
+
}
|
|
16
21
|
const command = defaultEditorCommand(file)
|
|
17
|
-
if (!command) return
|
|
22
|
+
if (!command) return { ok: false, error: 'no default open command for this platform' }
|
|
23
|
+
return openEditorCommand(command)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function openInFileManager(target: string): Promise<EditorOpenResult> {
|
|
27
|
+
const command = defaultEditorCommand(target)
|
|
28
|
+
if (!command) return { ok: false, error: 'no default open command for this platform' }
|
|
18
29
|
return openEditorCommand(command)
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -47,3 +58,17 @@ export function defaultEditorCommand(file: string, platform: NodeJS.Platform = p
|
|
|
47
58
|
if (platform === 'darwin') return { cmd: 'open', args: [file], method: 'open', waited: false }
|
|
48
59
|
return { cmd: 'xdg-open', args: [file], method: 'xdg-open', waited: false }
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
export function isVscodeEnvironment(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
63
|
+
if (env.TERM_PROGRAM === 'vscode') return true
|
|
64
|
+
if (env.VSCODE_PID) return true
|
|
65
|
+
if (env.VSCODE_GIT_IPC_HANDLE) return true
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function vscodeEditorCommand(file: string, platform: NodeJS.Platform = process.platform): EditorCommand {
|
|
70
|
+
if (platform === 'win32') {
|
|
71
|
+
return { cmd: 'cmd', args: ['/c', 'code', '--reuse-window', file], method: 'vscode', waited: false }
|
|
72
|
+
}
|
|
73
|
+
return { cmd: 'code', args: ['--reuse-window', file], method: 'vscode', waited: false }
|
|
74
|
+
}
|
|
@@ -10,6 +10,16 @@ export type ContinuityFiles = {
|
|
|
10
10
|
'MEMORY.md': string
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export type ContinuitySkillsTree = Record<string, string>
|
|
14
|
+
|
|
15
|
+
const PRIVATE_SKILL_FILE_RE = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+$/
|
|
16
|
+
const PRIVATE_SKILL_LAST_SEG_FILE_RE = /^[A-Za-z0-9._-]+\.[A-Za-z0-9]+$/
|
|
17
|
+
const LEGACY_NESTED_SKILL_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/.+$/
|
|
18
|
+
const LEGACY_FLAT_NAME_MD_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\.md$/i
|
|
19
|
+
const MAX_PRIVATE_SKILL_ENTRIES = 500
|
|
20
|
+
const MAX_PRIVATE_SKILL_BODY_BYTES = 256 * 1024
|
|
21
|
+
const MAX_PRIVATE_SKILL_PATH_LEN = 256
|
|
22
|
+
|
|
13
23
|
type ContinuityTranscriptSummary = {
|
|
14
24
|
sessionId?: string
|
|
15
25
|
createdAt?: string
|
|
@@ -33,6 +43,7 @@ type ContinuitySnapshotPayload = {
|
|
|
33
43
|
sequence?: number
|
|
34
44
|
agent: ContinuityAgentSnapshot
|
|
35
45
|
files: ContinuityFiles
|
|
46
|
+
skills?: ContinuitySkillsTree
|
|
36
47
|
transcript: ContinuityTranscriptSummary[]
|
|
37
48
|
state: Record<string, unknown>
|
|
38
49
|
}
|
|
@@ -997,6 +1008,7 @@ function continuityPayloadFromArgs(args: {
|
|
|
997
1008
|
createdAt: string
|
|
998
1009
|
payload: Omit<ContinuitySnapshotPayload, 'version' | 'ownerAddress' | 'createdAt'> & { createdAt?: string }
|
|
999
1010
|
}): ContinuitySnapshotPayload {
|
|
1011
|
+
const skills = normalizeContinuitySkills(args.payload.skills)
|
|
1000
1012
|
return {
|
|
1001
1013
|
version: 1,
|
|
1002
1014
|
ownerAddress: args.ownerAddress,
|
|
@@ -1004,6 +1016,7 @@ function continuityPayloadFromArgs(args: {
|
|
|
1004
1016
|
...(args.payload.sequence !== undefined ? { sequence: args.payload.sequence } : {}),
|
|
1005
1017
|
agent: normalizeAgentSnapshot(args.payload.agent),
|
|
1006
1018
|
files: normalizeContinuityFiles(args.payload.files),
|
|
1019
|
+
...(skills ? { skills } : {}),
|
|
1007
1020
|
transcript: normalizeTranscript(args.payload.transcript),
|
|
1008
1021
|
state: normalizeState(args.payload.state),
|
|
1009
1022
|
}
|
|
@@ -1015,6 +1028,7 @@ function normalizeContinuityPayload(input: unknown): ContinuitySnapshotPayload {
|
|
|
1015
1028
|
if (obj.version !== 1) throw new Error('Continuity snapshot payload version is invalid')
|
|
1016
1029
|
if (typeof obj.ownerAddress !== 'string') throw new Error('Continuity snapshot owner is invalid')
|
|
1017
1030
|
if (typeof obj.createdAt !== 'string') throw new Error('Continuity snapshot timestamp is invalid')
|
|
1031
|
+
const skills = normalizeContinuitySkills(obj.skills)
|
|
1018
1032
|
return {
|
|
1019
1033
|
version: 1,
|
|
1020
1034
|
ownerAddress: toChecksumAddress(obj.ownerAddress),
|
|
@@ -1022,11 +1036,122 @@ function normalizeContinuityPayload(input: unknown): ContinuitySnapshotPayload {
|
|
|
1022
1036
|
...(typeof obj.sequence === 'number' && Number.isSafeInteger(obj.sequence) ? { sequence: obj.sequence } : {}),
|
|
1023
1037
|
agent: normalizeAgentSnapshot(obj.agent),
|
|
1024
1038
|
files: normalizeContinuityFiles(obj.files),
|
|
1039
|
+
...(skills ? { skills } : {}),
|
|
1025
1040
|
transcript: normalizeTranscript(obj.transcript),
|
|
1026
1041
|
state: normalizeState(obj.state),
|
|
1027
1042
|
}
|
|
1028
1043
|
}
|
|
1029
1044
|
|
|
1045
|
+
export function normalizeContinuitySkills(input: unknown): ContinuitySkillsTree | undefined {
|
|
1046
|
+
if (input === undefined || input === null) return undefined
|
|
1047
|
+
if (typeof input !== 'object' || Array.isArray(input)) return undefined
|
|
1048
|
+
const obj = input as Record<string, unknown>
|
|
1049
|
+
const out: ContinuitySkillsTree = {}
|
|
1050
|
+
let count = 0
|
|
1051
|
+
const tryInsert = (key: string, rawValue: unknown): void => {
|
|
1052
|
+
if (count >= MAX_PRIVATE_SKILL_ENTRIES) return
|
|
1053
|
+
if (typeof rawValue !== 'string') return
|
|
1054
|
+
if (key.length === 0 || key.length > MAX_PRIVATE_SKILL_PATH_LEN) return
|
|
1055
|
+
if (key.includes('\0')) return
|
|
1056
|
+
if (key.includes('..')) return
|
|
1057
|
+
if (key.startsWith('/')) return
|
|
1058
|
+
if (/^[A-Za-z]:/.test(key)) return
|
|
1059
|
+
if (!isAcceptableSkillKey(key)) return
|
|
1060
|
+
if (Buffer.byteLength(rawValue, 'utf8') > MAX_PRIVATE_SKILL_BODY_BYTES) return
|
|
1061
|
+
if (out[key] !== undefined) return
|
|
1062
|
+
out[key] = rawValue
|
|
1063
|
+
count++
|
|
1064
|
+
}
|
|
1065
|
+
const legacyRoots = new Set<string>()
|
|
1066
|
+
const realSkillFolders = new Set<string>()
|
|
1067
|
+
for (const rawKey of Object.keys(obj)) {
|
|
1068
|
+
const key = rawKey.replace(/\\/g, '/')
|
|
1069
|
+
const segments = key.split('/')
|
|
1070
|
+
if (segments.length === 3 && segments[2] === 'SKILL.md' && segments[0] && segments[1]) {
|
|
1071
|
+
legacyRoots.add(`${segments[0]}/${segments[1]}`)
|
|
1072
|
+
}
|
|
1073
|
+
if (segments.length === 2 && segments[1] === 'SKILL.md' && segments[0]) {
|
|
1074
|
+
realSkillFolders.add(segments[0])
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
for (const [rawKey, rawValue] of Object.entries(obj)) {
|
|
1078
|
+
const key = rawKey.replace(/\\/g, '/')
|
|
1079
|
+
if (!isCanonicalFlatKey(key)) continue
|
|
1080
|
+
if (isUnderLegacyRoot(key, legacyRoots)) continue
|
|
1081
|
+
if (!keyHasRealSkillFolder(key, realSkillFolders)) continue
|
|
1082
|
+
tryInsert(key, rawValue)
|
|
1083
|
+
}
|
|
1084
|
+
for (const [rawKey, rawValue] of Object.entries(obj)) {
|
|
1085
|
+
const key = rawKey.replace(/\\/g, '/')
|
|
1086
|
+
if (
|
|
1087
|
+
isCanonicalFlatKey(key)
|
|
1088
|
+
&& !isUnderLegacyRoot(key, legacyRoots)
|
|
1089
|
+
&& keyHasRealSkillFolder(key, realSkillFolders)
|
|
1090
|
+
) continue
|
|
1091
|
+
const upgraded = upgradeLegacySkillKey(key, legacyRoots)
|
|
1092
|
+
if (!upgraded) continue
|
|
1093
|
+
tryInsert(upgraded, rawValue)
|
|
1094
|
+
}
|
|
1095
|
+
return count > 0 ? out : undefined
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function isUnderLegacyRoot(key: string, legacyRoots: Set<string>): boolean {
|
|
1099
|
+
for (const root of legacyRoots) {
|
|
1100
|
+
if (key === `${root}/SKILL.md`) return true
|
|
1101
|
+
if (key.startsWith(`${root}/`)) return true
|
|
1102
|
+
}
|
|
1103
|
+
return false
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function keyHasRealSkillFolder(key: string, realSkillFolders: Set<string>): boolean {
|
|
1107
|
+
const first = key.split('/')[0]
|
|
1108
|
+
if (!first) return false
|
|
1109
|
+
return realSkillFolders.has(first)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function isCanonicalFlatKey(key: string): boolean {
|
|
1113
|
+
if (!PRIVATE_SKILL_FILE_RE.test(key)) return false
|
|
1114
|
+
const segments = key.split('/')
|
|
1115
|
+
if (segments.length < 2) return false
|
|
1116
|
+
const last = segments[segments.length - 1]!
|
|
1117
|
+
if (last === 'SKILL.md') return segments.length === 2
|
|
1118
|
+
return PRIVATE_SKILL_LAST_SEG_FILE_RE.test(last)
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function isAcceptableSkillKey(key: string): boolean {
|
|
1122
|
+
return isCanonicalFlatKey(key)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function upgradeLegacySkillKey(key: string, legacyRoots: Set<string>): string | null {
|
|
1126
|
+
for (const root of legacyRoots) {
|
|
1127
|
+
if (key === `${root}/SKILL.md` || key.startsWith(`${root}/`)) {
|
|
1128
|
+
const [first, second] = root.split('/')
|
|
1129
|
+
if (!first || !second) continue
|
|
1130
|
+
const rest = key.slice(root.length + 1)
|
|
1131
|
+
const flattened = `${first}-${second}/${rest}`
|
|
1132
|
+
return isCanonicalFlatKey(flattened) ? flattened : null
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (LEGACY_FLAT_NAME_MD_RE.test(key)) {
|
|
1136
|
+
const [category, file] = key.split('/')
|
|
1137
|
+
if (!category || !file) return null
|
|
1138
|
+
const slug = file.replace(/\.md$/i, '')
|
|
1139
|
+
if (!slug) return null
|
|
1140
|
+
const flattened = `${category}-${slug}/SKILL.md`
|
|
1141
|
+
return isCanonicalFlatKey(flattened) ? flattened : null
|
|
1142
|
+
}
|
|
1143
|
+
if (LEGACY_NESTED_SKILL_RE.test(key)) {
|
|
1144
|
+
const segments = key.split('/')
|
|
1145
|
+
if (segments.length < 3) return null
|
|
1146
|
+
const [first, second, ...rest] = segments
|
|
1147
|
+
if (!first || !second || rest.length === 0) return null
|
|
1148
|
+
const flattened = `${first}-${second}/${rest.join('/')}`
|
|
1149
|
+
return isCanonicalFlatKey(flattened) ? flattened : null
|
|
1150
|
+
}
|
|
1151
|
+
if (isCanonicalFlatKey(key)) return key
|
|
1152
|
+
return null
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1030
1155
|
function normalizeAgentSnapshot(input: unknown): ContinuityAgentSnapshot {
|
|
1031
1156
|
if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
|
|
1032
1157
|
const obj = input as Record<string, unknown>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EthagentIdentity } from '../../storage/config.js'
|
|
2
|
+
import type { SkillIndexEntry } from './skills/types.js'
|
|
2
3
|
|
|
3
4
|
type PublicSkill = {
|
|
4
5
|
id: string
|
|
@@ -45,7 +46,7 @@ export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSk
|
|
|
45
46
|
: identity.agentId ? `ethagent #${identity.agentId}` : 'ethagent'
|
|
46
47
|
const description = typeof state.description === 'string' && state.description.trim()
|
|
47
48
|
? state.description.trim()
|
|
48
|
-
: '
|
|
49
|
+
: 'privacy-first AI agent with a portable Ethereum identity'
|
|
49
50
|
const imageUrl = typeof state.imageUrl === 'string' && state.imageUrl.trim()
|
|
50
51
|
? state.imageUrl.trim()
|
|
51
52
|
: undefined
|
|
@@ -80,6 +81,41 @@ export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSk
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
export function appendPublicSkillEntries(
|
|
85
|
+
profile: PublicSkillsProfile,
|
|
86
|
+
entries: readonly SkillIndexEntry[],
|
|
87
|
+
): PublicSkillsProfile {
|
|
88
|
+
if (entries.length === 0) return profile
|
|
89
|
+
const baselineIds = new Set(profile.skills.map(skill => skill.id))
|
|
90
|
+
const appended: PublicSkill[] = []
|
|
91
|
+
const usedIds = new Set(baselineIds)
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (entry.visibility !== 'public' && entry.visibility !== 'discoverable') continue
|
|
94
|
+
const id = uniqueSkillId(entry.name, usedIds)
|
|
95
|
+
usedIds.add(id)
|
|
96
|
+
appended.push({
|
|
97
|
+
id,
|
|
98
|
+
name: entry.displayName ?? entry.name,
|
|
99
|
+
description: entry.description || entry.name,
|
|
100
|
+
inputModes: ['text/markdown'],
|
|
101
|
+
outputModes: ['text/markdown'],
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
if (appended.length === 0) return profile
|
|
105
|
+
return {
|
|
106
|
+
...profile,
|
|
107
|
+
skills: [...profile.skills, ...appended],
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function uniqueSkillId(base: string, used: Set<string>): string {
|
|
112
|
+
const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'skill'
|
|
113
|
+
if (!used.has(slug)) return slug
|
|
114
|
+
let i = 2
|
|
115
|
+
while (used.has(`${slug}-${i}`)) i++
|
|
116
|
+
return `${slug}-${i}`
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
export function renderPublicSkillsJson(profile: PublicSkillsProfile): string {
|
|
84
120
|
const inputModes = unique(profile.skills.flatMap(skill => skill.inputModes))
|
|
85
121
|
const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
|