ethagent 0.2.0 → 1.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/LICENSE +21 -0
- package/README.md +114 -32
- package/bin/ethagent.js +11 -2
- package/package.json +30 -8
- package/src/app/FirstRun.tsx +412 -0
- package/src/app/hooks/useCancelRequest.ts +22 -0
- package/src/app/hooks/useDoublePress.ts +46 -0
- package/src/app/hooks/useExitOnCtrlC.ts +36 -0
- package/src/app/input/AppInputProvider.tsx +116 -0
- package/src/app/input/appInputParser.ts +279 -0
- package/src/app/keybindings/KeybindingProvider.tsx +134 -0
- package/src/app/keybindings/resolver.ts +42 -0
- package/src/app/keybindings/types.ts +26 -0
- package/src/chat/ChatBottomPane.tsx +280 -0
- package/src/chat/ChatInput.tsx +722 -0
- package/src/chat/ChatScreen.tsx +1575 -0
- package/src/chat/ContextLimitView.tsx +95 -0
- package/src/chat/ContinuityEditReviewView.tsx +48 -0
- package/src/chat/ConversationStack.tsx +47 -0
- package/src/chat/CopyPicker.tsx +52 -0
- package/src/chat/MessageList.tsx +609 -0
- package/src/chat/PermissionPrompt.tsx +153 -0
- package/src/chat/PermissionsView.tsx +159 -0
- package/src/chat/PlanApprovalView.tsx +91 -0
- package/src/chat/ResumeView.tsx +267 -0
- package/src/chat/RewindView.tsx +386 -0
- package/src/chat/SessionStatus.tsx +51 -0
- package/src/chat/TranscriptView.tsx +202 -0
- package/src/chat/chatInputState.ts +247 -0
- package/src/chat/chatPaste.ts +49 -0
- package/src/chat/chatScreenUtils.ts +187 -0
- package/src/chat/chatSessionState.ts +142 -0
- package/src/chat/chatTurnOrchestrator.ts +701 -0
- package/src/chat/commands.ts +673 -0
- package/src/chat/textCursor.ts +202 -0
- package/src/chat/toolResultDisplay.ts +8 -0
- package/src/chat/transcriptViewport.ts +247 -0
- package/src/cli/ResetConfirmView.tsx +61 -0
- package/src/cli/main.tsx +177 -0
- package/src/cli/preview.tsx +19 -0
- package/src/cli/reset.ts +106 -0
- package/src/identity/continuity/editor.ts +149 -0
- package/src/identity/continuity/envelope.ts +345 -0
- package/src/identity/continuity/history.ts +153 -0
- package/src/identity/continuity/privateEdit.ts +334 -0
- package/src/identity/continuity/publicSkills.ts +173 -0
- package/src/identity/continuity/snapshots.ts +183 -0
- package/src/identity/continuity/storage.ts +507 -0
- package/src/identity/crypto/backupEnvelope.ts +486 -0
- package/src/identity/crypto/eth.ts +137 -0
- package/src/identity/hub/IdentityHub.tsx +868 -0
- package/src/identity/hub/identityHubEffects.ts +1146 -0
- package/src/identity/hub/identityHubModel.ts +291 -0
- package/src/identity/hub/identityHubReducer.ts +212 -0
- package/src/identity/hub/screens/BusyScreen.tsx +26 -0
- package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +144 -0
- package/src/identity/hub/screens/CreateFlow.tsx +206 -0
- package/src/identity/hub/screens/DetailsScreen.tsx +64 -0
- package/src/identity/hub/screens/EditProfileFlow.tsx +145 -0
- package/src/identity/hub/screens/ErrorScreen.tsx +35 -0
- package/src/identity/hub/screens/IdentitySummary.tsx +70 -0
- package/src/identity/hub/screens/MenuScreen.tsx +117 -0
- package/src/identity/hub/screens/NetworkScreen.tsx +41 -0
- package/src/identity/hub/screens/RebackupStorageScreen.tsx +50 -0
- package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +85 -0
- package/src/identity/hub/screens/RestoreFlow.tsx +206 -0
- package/src/identity/hub/screens/StorageCredentialScreen.tsx +128 -0
- package/src/identity/hub/screens/WalletApprovalScreen.tsx +43 -0
- package/src/identity/profile/imagePicker.ts +180 -0
- package/src/identity/registry/erc8004.ts +1106 -0
- package/src/identity/registry/registryConfig.ts +69 -0
- package/src/identity/storage/ipfs.ts +212 -0
- package/src/identity/storage/pinataJwt.ts +53 -0
- package/src/identity/wallet/browserWallet.ts +393 -0
- package/src/identity/wallet/wallet-page/wallet.html +1082 -0
- package/src/mcp/approvals.ts +113 -0
- package/src/mcp/config.ts +235 -0
- package/src/mcp/manager.ts +541 -0
- package/src/mcp/names.ts +19 -0
- package/src/mcp/output.ts +96 -0
- package/src/models/ModelPicker.tsx +1446 -0
- package/src/models/catalog.ts +296 -0
- package/src/models/huggingface.ts +651 -0
- package/src/models/llamacpp.ts +810 -0
- package/src/models/llamacppPreflight.ts +150 -0
- package/src/models/modelDisplay.ts +105 -0
- package/src/models/modelPickerOptions.ts +421 -0
- package/src/models/modelRecommendation.ts +140 -0
- package/src/models/runtimeDetection.ts +81 -0
- package/src/models/uncensoredCatalog.ts +86 -0
- package/src/providers/anthropic.ts +259 -0
- package/src/providers/contracts.ts +62 -0
- package/src/providers/errors.ts +62 -0
- package/src/providers/gemini.ts +152 -0
- package/src/providers/openai-chat.ts +472 -0
- package/src/providers/registry.ts +42 -0
- package/src/providers/retry.ts +58 -0
- package/src/providers/sse.ts +93 -0
- package/src/runtime/compaction.ts +389 -0
- package/src/runtime/cwd.ts +43 -0
- package/src/runtime/sessionMode.ts +55 -0
- package/src/runtime/systemPrompt.ts +209 -0
- package/src/runtime/toolClaimGuards.ts +143 -0
- package/src/runtime/toolExecution.ts +304 -0
- package/src/runtime/toolIntent.ts +163 -0
- package/src/runtime/turn.ts +858 -0
- package/src/storage/atomicWrite.ts +68 -0
- package/src/storage/config.ts +189 -0
- package/src/storage/factoryReset.ts +130 -0
- package/src/storage/history.ts +58 -0
- package/src/storage/identity.ts +99 -0
- package/src/storage/permissions.ts +76 -0
- package/src/storage/rewind.ts +246 -0
- package/src/storage/secrets.ts +181 -0
- package/src/storage/sessionExport.ts +49 -0
- package/src/storage/sessions.ts +482 -0
- package/src/tools/bashSafety.ts +174 -0
- package/src/tools/bashTool.ts +140 -0
- package/src/tools/changeDirectoryTool.ts +213 -0
- package/src/tools/contracts.ts +179 -0
- package/src/tools/deleteFileTool.ts +111 -0
- package/src/tools/editTool.ts +160 -0
- package/src/tools/editUtils.ts +170 -0
- package/src/tools/listDirectoryTool.ts +55 -0
- package/src/tools/mcpResourceTools.ts +95 -0
- package/src/tools/permissionRules.ts +85 -0
- package/src/tools/privateContinuityEditTool.ts +178 -0
- package/src/tools/privateContinuityReadTool.ts +107 -0
- package/src/tools/readTool.ts +85 -0
- package/src/tools/registry.ts +67 -0
- package/src/tools/writeFileTool.ts +142 -0
- package/src/ui/BrandSplash.tsx +193 -0
- package/src/ui/ProgressBar.tsx +34 -0
- package/src/ui/Select.tsx +143 -0
- package/src/ui/Spinner.tsx +269 -0
- package/src/ui/Surface.tsx +47 -0
- package/src/ui/TextInput.tsx +97 -0
- package/src/ui/theme.ts +59 -0
- package/src/utils/clipboard.ts +216 -0
- package/src/utils/markdownSegments.ts +51 -0
- package/src/utils/messages.ts +35 -0
- package/src/utils/withRetry.ts +280 -0
- package/src/cli.tsx +0 -147
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { EthagentIdentity } from '../../storage/config.js'
|
|
4
|
+
import { atomicWriteText } from '../../storage/atomicWrite.js'
|
|
5
|
+
import type { ContinuityFiles } from './envelope.js'
|
|
6
|
+
import {
|
|
7
|
+
continuityVaultRef,
|
|
8
|
+
ensureContinuityVault,
|
|
9
|
+
writePublicSkillsFile,
|
|
10
|
+
writeContinuityFiles,
|
|
11
|
+
type PrivateContinuityFile,
|
|
12
|
+
} from './storage.js'
|
|
13
|
+
|
|
14
|
+
export type PrivateContinuityHistorySnapshot = {
|
|
15
|
+
version: 1
|
|
16
|
+
id: string
|
|
17
|
+
createdAt: string
|
|
18
|
+
file: PrivateContinuityFile
|
|
19
|
+
filePath: string
|
|
20
|
+
existedBefore: boolean
|
|
21
|
+
previousContent: string
|
|
22
|
+
previousFiles?: ContinuityFiles
|
|
23
|
+
previousPublicSkills?: string
|
|
24
|
+
changeSummary: string
|
|
25
|
+
identity: {
|
|
26
|
+
address: string
|
|
27
|
+
ownerAddress?: string
|
|
28
|
+
chainId?: number
|
|
29
|
+
identityRegistryAddress?: string
|
|
30
|
+
agentId?: string
|
|
31
|
+
}
|
|
32
|
+
sessionId?: string
|
|
33
|
+
turnId?: string
|
|
34
|
+
promptSnippet?: string
|
|
35
|
+
checkpointLabel?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type RecordPrivateContinuityHistoryInput = {
|
|
39
|
+
identity: EthagentIdentity
|
|
40
|
+
file: PrivateContinuityFile
|
|
41
|
+
filePath: string
|
|
42
|
+
existedBefore: boolean
|
|
43
|
+
previousContent: string
|
|
44
|
+
previousFiles?: ContinuityFiles
|
|
45
|
+
previousPublicSkills?: string
|
|
46
|
+
changeSummary: string
|
|
47
|
+
createdAt?: string
|
|
48
|
+
sessionId?: string
|
|
49
|
+
turnId?: string
|
|
50
|
+
promptSnippet?: string
|
|
51
|
+
checkpointLabel?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function privateContinuityHistoryPath(identity: EthagentIdentity): string {
|
|
55
|
+
return path.join(continuityVaultRef(identity).dir, '.history.jsonl')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function recordPrivateContinuityHistorySnapshot(
|
|
59
|
+
input: RecordPrivateContinuityHistoryInput,
|
|
60
|
+
): Promise<PrivateContinuityHistorySnapshot> {
|
|
61
|
+
await ensureContinuityVault(input.identity)
|
|
62
|
+
const createdAt = input.createdAt ?? new Date().toISOString()
|
|
63
|
+
const snapshot: PrivateContinuityHistorySnapshot = {
|
|
64
|
+
version: 1,
|
|
65
|
+
id: `${createdAt}:${input.file}`.replaceAll('\\', '/'),
|
|
66
|
+
createdAt,
|
|
67
|
+
file: input.file,
|
|
68
|
+
filePath: path.resolve(input.filePath),
|
|
69
|
+
existedBefore: input.existedBefore,
|
|
70
|
+
previousContent: input.previousContent,
|
|
71
|
+
...(input.previousFiles ? { previousFiles: input.previousFiles } : {}),
|
|
72
|
+
...(input.previousPublicSkills !== undefined ? { previousPublicSkills: input.previousPublicSkills } : {}),
|
|
73
|
+
changeSummary: input.changeSummary,
|
|
74
|
+
identity: {
|
|
75
|
+
address: input.identity.address,
|
|
76
|
+
...(input.identity.ownerAddress ? { ownerAddress: input.identity.ownerAddress } : {}),
|
|
77
|
+
...(input.identity.chainId ? { chainId: input.identity.chainId } : {}),
|
|
78
|
+
...(input.identity.identityRegistryAddress ? { identityRegistryAddress: input.identity.identityRegistryAddress } : {}),
|
|
79
|
+
...(input.identity.agentId ? { agentId: input.identity.agentId } : {}),
|
|
80
|
+
},
|
|
81
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
82
|
+
...(input.turnId ? { turnId: input.turnId } : {}),
|
|
83
|
+
...(input.promptSnippet ? { promptSnippet: normalizeSnippet(input.promptSnippet) } : {}),
|
|
84
|
+
...(input.checkpointLabel ? { checkpointLabel: normalizeSnippet(input.checkpointLabel) } : {}),
|
|
85
|
+
}
|
|
86
|
+
await fs.appendFile(privateContinuityHistoryPath(input.identity), `${JSON.stringify(snapshot)}\n`, {
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
mode: 0o600,
|
|
89
|
+
})
|
|
90
|
+
return snapshot
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function listPrivateContinuityHistory(
|
|
94
|
+
identity: EthagentIdentity,
|
|
95
|
+
limit = 30,
|
|
96
|
+
): Promise<PrivateContinuityHistorySnapshot[]> {
|
|
97
|
+
let raw: string
|
|
98
|
+
try {
|
|
99
|
+
raw = await fs.readFile(privateContinuityHistoryPath(identity), 'utf8')
|
|
100
|
+
} catch (error: unknown) {
|
|
101
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
|
|
102
|
+
throw error
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const snapshots: PrivateContinuityHistorySnapshot[] = []
|
|
106
|
+
for (const line of raw.split('\n')) {
|
|
107
|
+
const trimmed = line.trim()
|
|
108
|
+
if (!trimmed) continue
|
|
109
|
+
try {
|
|
110
|
+
snapshots.push(JSON.parse(trimmed) as PrivateContinuityHistorySnapshot)
|
|
111
|
+
} catch {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return snapshots.reverse().slice(0, limit)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function restorePrivateContinuityHistorySnapshot(
|
|
119
|
+
identity: EthagentIdentity,
|
|
120
|
+
snapshotId: string,
|
|
121
|
+
): Promise<PrivateContinuityHistorySnapshot> {
|
|
122
|
+
const snapshot = (await listPrivateContinuityHistory(identity, 500))
|
|
123
|
+
.find(item => item.id === snapshotId)
|
|
124
|
+
if (!snapshot) throw new Error('private continuity checkpoint was not found')
|
|
125
|
+
|
|
126
|
+
if (snapshot.previousFiles) {
|
|
127
|
+
await writeContinuityFiles(identity, snapshot.previousFiles)
|
|
128
|
+
if (snapshot.previousPublicSkills !== undefined) {
|
|
129
|
+
await writePublicSkillsFile(identity, snapshot.previousPublicSkills)
|
|
130
|
+
}
|
|
131
|
+
return snapshot
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await ensureContinuityVault(identity)
|
|
135
|
+
if (snapshot.existedBefore) {
|
|
136
|
+
await atomicWriteText(snapshot.filePath, ensureTrailingNewline(snapshot.previousContent), { mode: 0o600 })
|
|
137
|
+
} else {
|
|
138
|
+
await fs.rm(snapshot.filePath, { force: true })
|
|
139
|
+
}
|
|
140
|
+
if (snapshot.previousPublicSkills !== undefined) {
|
|
141
|
+
await writePublicSkillsFile(identity, snapshot.previousPublicSkills)
|
|
142
|
+
}
|
|
143
|
+
return snapshot
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeSnippet(input: string): string {
|
|
147
|
+
const normalized = input.replace(/\s+/g, ' ').trim()
|
|
148
|
+
return normalized.length <= 120 ? normalized : `${normalized.slice(0, 117)}...`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ensureTrailingNewline(value: string): string {
|
|
152
|
+
return value.endsWith('\n') ? value : `${value}\n`
|
|
153
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { atomicWriteText } from '../../storage/atomicWrite.js'
|
|
4
|
+
import type { EthagentConfig, EthagentIdentity } from '../../storage/config.js'
|
|
5
|
+
import { applyRequestedEdit } from '../../tools/editUtils.js'
|
|
6
|
+
import {
|
|
7
|
+
continuityVaultRef,
|
|
8
|
+
defaultContinuityFiles,
|
|
9
|
+
ensureContinuityVault,
|
|
10
|
+
type PrivateContinuityFile,
|
|
11
|
+
} from './storage.js'
|
|
12
|
+
|
|
13
|
+
export type PrivateContinuityEditInput = {
|
|
14
|
+
file: PrivateContinuityFile
|
|
15
|
+
oldText?: string
|
|
16
|
+
newText?: string
|
|
17
|
+
appendToSection?: string
|
|
18
|
+
appendText?: string
|
|
19
|
+
replaceAll?: boolean
|
|
20
|
+
replaceWholeFile?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PreparedPrivateContinuityEdit = {
|
|
24
|
+
identity: EthagentIdentity
|
|
25
|
+
file: PrivateContinuityFile
|
|
26
|
+
fullPath: string
|
|
27
|
+
relativePath: string
|
|
28
|
+
directoryPath: string
|
|
29
|
+
existedBefore: boolean
|
|
30
|
+
previousContent: string
|
|
31
|
+
before: string
|
|
32
|
+
after: string
|
|
33
|
+
previewBefore: string
|
|
34
|
+
previewAfter: string
|
|
35
|
+
changeSummary: string
|
|
36
|
+
diff: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function preparePrivateContinuityEdit(
|
|
40
|
+
input: PrivateContinuityEditInput,
|
|
41
|
+
config: EthagentConfig | undefined,
|
|
42
|
+
): Promise<PreparedPrivateContinuityEdit> {
|
|
43
|
+
const identity = config?.identity
|
|
44
|
+
if (!identity) {
|
|
45
|
+
throw new Error('no active identity; create or load an identity before proposing private continuity edits')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fullPath = privateContinuityPath(identity, input.file)
|
|
49
|
+
const existing = await readPrivateContinuityFile(identity, input.file, fullPath)
|
|
50
|
+
const applied = applyPrivateContinuityEdit(input, existing.content, identity)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
identity,
|
|
54
|
+
file: input.file,
|
|
55
|
+
fullPath,
|
|
56
|
+
relativePath: `identity-vault/${input.file}`,
|
|
57
|
+
directoryPath: path.dirname(fullPath),
|
|
58
|
+
existedBefore: existing.existedBefore,
|
|
59
|
+
previousContent: existing.existedBefore ? existing.content : '',
|
|
60
|
+
before: existing.content,
|
|
61
|
+
after: applied.after,
|
|
62
|
+
previewBefore: applied.previewBefore,
|
|
63
|
+
previewAfter: applied.previewAfter,
|
|
64
|
+
changeSummary: applied.summary,
|
|
65
|
+
diff: renderPrivateContinuityDiff(input.file, applied.before, applied.after),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function applyPrivateContinuityEdit(input: PrivateContinuityEditInput, before: string, identity: EthagentIdentity) {
|
|
70
|
+
if (input.replaceWholeFile) {
|
|
71
|
+
throw new Error('private continuity files must be edited in place; whole-file replacement is disabled')
|
|
72
|
+
}
|
|
73
|
+
if (input.appendToSection || input.appendText) {
|
|
74
|
+
if (!input.appendToSection?.trim()) throw new Error('appendToSection is required for append edits')
|
|
75
|
+
if (!input.appendText?.trim()) throw new Error('appendText is required for append edits')
|
|
76
|
+
if (input.oldText || input.newText !== undefined) {
|
|
77
|
+
throw new Error('use either appendToSection+appendText or oldText+newText, not both')
|
|
78
|
+
}
|
|
79
|
+
return appendToMarkdownSection(identity, input.file, before, input.appendToSection, input.appendText)
|
|
80
|
+
}
|
|
81
|
+
if (!input.oldText?.trim()) {
|
|
82
|
+
throw new Error('oldText is required; private continuity edits must patch existing scaffold text')
|
|
83
|
+
}
|
|
84
|
+
if (input.newText === undefined) {
|
|
85
|
+
throw new Error('newText is required for targeted private continuity edits')
|
|
86
|
+
}
|
|
87
|
+
return applyRequestedEdit(
|
|
88
|
+
input.file,
|
|
89
|
+
before,
|
|
90
|
+
input.oldText,
|
|
91
|
+
input.newText,
|
|
92
|
+
input.replaceAll ?? false,
|
|
93
|
+
false,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function writePreparedPrivateContinuityEdit(edit: PreparedPrivateContinuityEdit): Promise<void> {
|
|
98
|
+
await ensureContinuityVault(edit.identity)
|
|
99
|
+
await atomicWriteText(edit.fullPath, ensureTrailingNewline(edit.after), { mode: 0o600 })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function privateContinuityPath(identity: EthagentIdentity, file: PrivateContinuityFile): string {
|
|
103
|
+
const ref = continuityVaultRef(identity)
|
|
104
|
+
return file === 'SOUL.md' ? ref.soulPath : ref.memoryPath
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function readPrivateContinuityFile(
|
|
108
|
+
identity: EthagentIdentity,
|
|
109
|
+
file: PrivateContinuityFile,
|
|
110
|
+
fullPath: string,
|
|
111
|
+
): Promise<{ content: string; existedBefore: boolean }> {
|
|
112
|
+
try {
|
|
113
|
+
return { content: await fs.readFile(fullPath, 'utf8'), existedBefore: true }
|
|
114
|
+
} catch (err: unknown) {
|
|
115
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
116
|
+
return { content: defaultContinuityFiles(identity)[file], existedBefore: false }
|
|
117
|
+
}
|
|
118
|
+
throw err
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderPrivateContinuityDiff(file: PrivateContinuityFile, before: string, after: string): string {
|
|
123
|
+
if (before === after) return '(no changes)'
|
|
124
|
+
const changedLines = changedMarkdownLines(before, after)
|
|
125
|
+
const lines = [
|
|
126
|
+
`--- ${file}`,
|
|
127
|
+
`+++ ${file}`,
|
|
128
|
+
...(changedLines.length > 0 ? changedLines : ['(only whitespace or line ending changes)']),
|
|
129
|
+
]
|
|
130
|
+
const diff = lines.join('\n')
|
|
131
|
+
return diff.length <= 2400 ? diff : `${diff.slice(0, 2397)}...`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function changedMarkdownLines(before: string, after: string): string[] {
|
|
135
|
+
const beforeLines = markdownLines(before)
|
|
136
|
+
const afterLines = markdownLines(after)
|
|
137
|
+
const lengths = lcsLengths(beforeLines, afterLines)
|
|
138
|
+
const changed: string[] = []
|
|
139
|
+
let beforeIndex = 0
|
|
140
|
+
let afterIndex = 0
|
|
141
|
+
|
|
142
|
+
while (beforeIndex < beforeLines.length && afterIndex < afterLines.length) {
|
|
143
|
+
if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
|
|
144
|
+
beforeIndex += 1
|
|
145
|
+
afterIndex += 1
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const deleteScore = lengths[beforeIndex + 1]![afterIndex]!
|
|
150
|
+
const insertScore = lengths[beforeIndex]![afterIndex + 1]!
|
|
151
|
+
const deleteRevealsMatch = beforeLines[beforeIndex + 1] === afterLines[afterIndex]
|
|
152
|
+
const insertRevealsMatch = beforeLines[beforeIndex] === afterLines[afterIndex + 1]
|
|
153
|
+
|
|
154
|
+
if (insertRevealsMatch && insertScore >= deleteScore) {
|
|
155
|
+
changed.push(`+${afterLines[afterIndex]}`)
|
|
156
|
+
afterIndex += 1
|
|
157
|
+
} else if (deleteRevealsMatch && deleteScore >= insertScore) {
|
|
158
|
+
changed.push(`-${beforeLines[beforeIndex]}`)
|
|
159
|
+
beforeIndex += 1
|
|
160
|
+
} else if (deleteScore >= insertScore) {
|
|
161
|
+
changed.push(`-${beforeLines[beforeIndex]}`)
|
|
162
|
+
beforeIndex += 1
|
|
163
|
+
} else {
|
|
164
|
+
changed.push(`+${afterLines[afterIndex]}`)
|
|
165
|
+
afterIndex += 1
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
while (beforeIndex < beforeLines.length) {
|
|
170
|
+
changed.push(`-${beforeLines[beforeIndex]}`)
|
|
171
|
+
beforeIndex += 1
|
|
172
|
+
}
|
|
173
|
+
while (afterIndex < afterLines.length) {
|
|
174
|
+
changed.push(`+${afterLines[afterIndex]}`)
|
|
175
|
+
afterIndex += 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return changed
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function lcsLengths(beforeLines: string[], afterLines: string[]): number[][] {
|
|
182
|
+
const lengths = Array.from(
|
|
183
|
+
{ length: beforeLines.length + 1 },
|
|
184
|
+
() => Array<number>(afterLines.length + 1).fill(0),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for (let beforeIndex = beforeLines.length - 1; beforeIndex >= 0; beforeIndex -= 1) {
|
|
188
|
+
for (let afterIndex = afterLines.length - 1; afterIndex >= 0; afterIndex -= 1) {
|
|
189
|
+
lengths[beforeIndex]![afterIndex] = beforeLines[beforeIndex] === afterLines[afterIndex]
|
|
190
|
+
? lengths[beforeIndex + 1]![afterIndex + 1]! + 1
|
|
191
|
+
: Math.max(lengths[beforeIndex + 1]![afterIndex]!, lengths[beforeIndex]![afterIndex + 1]!)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return lengths
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function appendToMarkdownSection(
|
|
199
|
+
identity: EthagentIdentity,
|
|
200
|
+
file: PrivateContinuityFile,
|
|
201
|
+
before: string,
|
|
202
|
+
section: string,
|
|
203
|
+
appendText: string,
|
|
204
|
+
) {
|
|
205
|
+
const heading = normalizeSectionHeading(section)
|
|
206
|
+
let working = before
|
|
207
|
+
let repairedMissingSection = false
|
|
208
|
+
let lines = working.split(/\r?\n/)
|
|
209
|
+
let bounds = findMarkdownSectionBounds(lines, heading)
|
|
210
|
+
if (!bounds) {
|
|
211
|
+
const repaired = insertDefaultScaffoldSection(identity, file, before, heading)
|
|
212
|
+
if (!repaired) {
|
|
213
|
+
throw new Error(`section "${section}" was not found in ${file}; target an existing scaffold section`)
|
|
214
|
+
}
|
|
215
|
+
working = repaired
|
|
216
|
+
repairedMissingSection = true
|
|
217
|
+
lines = working.split(/\r?\n/)
|
|
218
|
+
bounds = findMarkdownSectionBounds(lines, heading)
|
|
219
|
+
}
|
|
220
|
+
if (!bounds) {
|
|
221
|
+
throw new Error(`section "${section}" was not found in ${file}; target an existing scaffold section`)
|
|
222
|
+
}
|
|
223
|
+
const { start, end: insertAt } = bounds
|
|
224
|
+
const prefix = lines.slice(0, insertAt).join('\n').replace(/\s+$/g, '')
|
|
225
|
+
const suffix = insertAt >= lines.length ? '' : lines.slice(insertAt).join('\n').replace(/^\s+/g, '')
|
|
226
|
+
const normalizedAppend = ensureTrailingNewline(appendText.trim())
|
|
227
|
+
const after = ensureTrailingNewline(suffix
|
|
228
|
+
? `${prefix}\n${normalizedAppend}\n${suffix}`
|
|
229
|
+
: `${prefix}\n${normalizedAppend}`)
|
|
230
|
+
const afterLines = after.split(/\r?\n/)
|
|
231
|
+
const afterBounds = findMarkdownSectionBounds(afterLines, heading)
|
|
232
|
+
return {
|
|
233
|
+
before,
|
|
234
|
+
after,
|
|
235
|
+
summary: repairedMissingSection
|
|
236
|
+
? `repair ${heading} section and append to ${heading} in ${file}`
|
|
237
|
+
: `append to ${heading} in ${file}`,
|
|
238
|
+
previewBefore: repairedMissingSection
|
|
239
|
+
? `section "${heading}" was missing in ${file}; approval will add the scaffold section before appending.`
|
|
240
|
+
: previewText(sectionPreview(lines, start, insertAt)),
|
|
241
|
+
previewAfter: previewText(afterBounds
|
|
242
|
+
? sectionPreview(afterLines, afterBounds.start, afterBounds.end)
|
|
243
|
+
: normalizedAppend),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function insertDefaultScaffoldSection(
|
|
248
|
+
identity: EthagentIdentity,
|
|
249
|
+
file: PrivateContinuityFile,
|
|
250
|
+
before: string,
|
|
251
|
+
heading: string,
|
|
252
|
+
): string | null {
|
|
253
|
+
const defaults = defaultContinuityFiles(identity)[file]
|
|
254
|
+
const defaultSection = extractMarkdownSection(defaults, heading)
|
|
255
|
+
if (!defaultSection) return null
|
|
256
|
+
|
|
257
|
+
const defaultHeadings = markdownSectionHeadings(defaults)
|
|
258
|
+
const targetIndex = defaultHeadings.indexOf(heading)
|
|
259
|
+
if (targetIndex === -1) return null
|
|
260
|
+
|
|
261
|
+
const lines = before.split(/\r?\n/)
|
|
262
|
+
const followingHeadings = new Set(defaultHeadings.slice(targetIndex + 1))
|
|
263
|
+
const followingIndex = lines.findIndex(line => followingHeadings.has(normalizeSectionHeading(line)))
|
|
264
|
+
if (followingIndex !== -1) {
|
|
265
|
+
return insertSectionAtLine(before, followingIndex, defaultSection)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const previousHeadings = new Set(defaultHeadings.slice(0, targetIndex))
|
|
269
|
+
let insertAfterPrevious: number | null = null
|
|
270
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
271
|
+
if (!previousHeadings.has(normalizeSectionHeading(lines[index] ?? ''))) continue
|
|
272
|
+
const bounds = findMarkdownSectionBounds(lines, normalizeSectionHeading(lines[index] ?? ''))
|
|
273
|
+
if (bounds && bounds.end > (insertAfterPrevious ?? -1)) insertAfterPrevious = bounds.end
|
|
274
|
+
}
|
|
275
|
+
if (insertAfterPrevious !== null) {
|
|
276
|
+
return insertSectionAtLine(before, insertAfterPrevious, defaultSection)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const firstHeading = lines.findIndex(line => /^#\s+/.test(line.trim()))
|
|
280
|
+
return insertSectionAtLine(before, firstHeading === -1 ? 0 : firstHeading + 1, defaultSection)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function insertSectionAtLine(markdown: string, lineIndex: number, section: string): string {
|
|
284
|
+
const lines = markdown.split(/\r?\n/)
|
|
285
|
+
const before = lines.slice(0, lineIndex).join('\n').replace(/\s+$/g, '')
|
|
286
|
+
const after = lines.slice(lineIndex).join('\n').replace(/^\s+/g, '')
|
|
287
|
+
const block = section.trim()
|
|
288
|
+
if (!before) return ensureTrailingNewline(after ? `${block}\n\n${after}` : block)
|
|
289
|
+
return ensureTrailingNewline(after ? `${before}\n\n${block}\n\n${after}` : `${before}\n\n${block}`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function extractMarkdownSection(markdown: string, heading: string): string | null {
|
|
293
|
+
const lines = markdown.split(/\r?\n/)
|
|
294
|
+
const bounds = findMarkdownSectionBounds(lines, heading)
|
|
295
|
+
if (!bounds) return null
|
|
296
|
+
return lines.slice(bounds.start, bounds.end).join('\n').trim()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function markdownSectionHeadings(markdown: string): string[] {
|
|
300
|
+
return markdown
|
|
301
|
+
.split(/\r?\n/)
|
|
302
|
+
.filter(line => /^##\s+/.test(line.trim()))
|
|
303
|
+
.map(normalizeSectionHeading)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function findMarkdownSectionBounds(lines: string[], heading: string): { start: number; end: number } | null {
|
|
307
|
+
const start = lines.findIndex(line => normalizeSectionHeading(line) === heading && /^#{1,6}\s+/.test(line.trim()))
|
|
308
|
+
if (start === -1) return null
|
|
309
|
+
const nextSection = lines.findIndex((line, index) => index > start && /^##\s+/.test(line.trim()))
|
|
310
|
+
return { start, end: nextSection === -1 ? lines.length : nextSection }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeSectionHeading(value: string): string {
|
|
314
|
+
return value.trim().replace(/^#+\s*/, '').trim()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function sectionPreview(lines: string[], start: number, end: number): string {
|
|
318
|
+
return lines.slice(start, Math.min(end, start + 8)).join('\n')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function markdownLines(value: string): string[] {
|
|
322
|
+
const lines = value.split(/\r?\n/)
|
|
323
|
+
if (lines[lines.length - 1] === '') lines.pop()
|
|
324
|
+
return lines
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function ensureTrailingNewline(value: string): string {
|
|
328
|
+
return value.endsWith('\n') ? value : `${value}\n`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function previewText(text: string, max = 700): string {
|
|
332
|
+
if (text.length <= max) return text
|
|
333
|
+
return `${text.slice(0, max - 3)}...`
|
|
334
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { EthagentIdentity } from '../../storage/config.js'
|
|
2
|
+
|
|
3
|
+
export type PublicSkill = {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
description: string
|
|
7
|
+
inputModes: string[]
|
|
8
|
+
outputModes: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type PublicSkillsProfile = {
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
version: string
|
|
15
|
+
imageUrl?: string
|
|
16
|
+
skills: PublicSkill[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AgentCard = {
|
|
20
|
+
name: string
|
|
21
|
+
description: string
|
|
22
|
+
version: string
|
|
23
|
+
protocolVersion: string
|
|
24
|
+
url: string
|
|
25
|
+
image?: string
|
|
26
|
+
iconUrl?: string
|
|
27
|
+
defaultInputModes: string[]
|
|
28
|
+
defaultOutputModes: string[]
|
|
29
|
+
capabilities: {
|
|
30
|
+
streaming: boolean
|
|
31
|
+
pushNotifications: boolean
|
|
32
|
+
}
|
|
33
|
+
skills: Array<{
|
|
34
|
+
id: string
|
|
35
|
+
name: string
|
|
36
|
+
description: string
|
|
37
|
+
inputModes: string[]
|
|
38
|
+
outputModes: string[]
|
|
39
|
+
}>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSkillsProfile {
|
|
43
|
+
const state = identity.state ?? {}
|
|
44
|
+
const name = typeof state.name === 'string' && state.name.trim()
|
|
45
|
+
? state.name.trim()
|
|
46
|
+
: identity.agentId ? `ethagent #${identity.agentId}` : 'ethagent'
|
|
47
|
+
const description = typeof state.description === 'string' && state.description.trim()
|
|
48
|
+
? state.description.trim()
|
|
49
|
+
: 'A wallet-owned AI coding agent.'
|
|
50
|
+
const imageUrl = typeof state.imageUrl === 'string' && state.imageUrl.trim()
|
|
51
|
+
? state.imageUrl.trim()
|
|
52
|
+
: undefined
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
description,
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
58
|
+
skills: [
|
|
59
|
+
{
|
|
60
|
+
id: 'software-engineering',
|
|
61
|
+
name: 'Software engineering',
|
|
62
|
+
description: 'Read code, plan implementations, debug failures, refactor safely, and run focused tests.',
|
|
63
|
+
inputModes: ['text/markdown'],
|
|
64
|
+
outputModes: ['text/markdown'],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'workspace-tools',
|
|
68
|
+
name: 'Workspace tools',
|
|
69
|
+
description: 'Use permissioned local file, shell, clipboard, and MCP tools for project work.',
|
|
70
|
+
inputModes: ['text/markdown'],
|
|
71
|
+
outputModes: ['text/markdown', 'application/json'],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'ethereum-identity',
|
|
75
|
+
name: 'Ethereum identity',
|
|
76
|
+
description: 'Represent a portable ERC-8004 agent identity controlled by the owner wallet.',
|
|
77
|
+
inputModes: ['text/markdown'],
|
|
78
|
+
outputModes: ['text/markdown', 'application/json'],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderPublicSkillsJson(profile: PublicSkillsProfile): string {
|
|
85
|
+
const inputModes = unique(profile.skills.flatMap(skill => skill.inputModes))
|
|
86
|
+
const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
|
|
87
|
+
const summary = {
|
|
88
|
+
schema: 'ethagent.public-skills.v1',
|
|
89
|
+
visibility: 'public',
|
|
90
|
+
name: profile.name,
|
|
91
|
+
description: profile.description,
|
|
92
|
+
version: profile.version,
|
|
93
|
+
...(profile.imageUrl ? { imageUrl: profile.imageUrl } : {}),
|
|
94
|
+
inputModes,
|
|
95
|
+
outputModes,
|
|
96
|
+
boundary: 'Public discovery metadata only. This is not executable code, private memory, or a skill installation manifest.',
|
|
97
|
+
capabilities: {
|
|
98
|
+
softwareEngineering: true,
|
|
99
|
+
workspaceTools: 'permissioned',
|
|
100
|
+
mcp: true,
|
|
101
|
+
streaming: true,
|
|
102
|
+
ethereumIdentity: 'ERC-8004',
|
|
103
|
+
encryptedContinuity: true,
|
|
104
|
+
},
|
|
105
|
+
delegation: {
|
|
106
|
+
bestFor: [
|
|
107
|
+
'code reading',
|
|
108
|
+
'implementation planning',
|
|
109
|
+
'debugging',
|
|
110
|
+
'refactors',
|
|
111
|
+
'tests',
|
|
112
|
+
'workspace automation',
|
|
113
|
+
],
|
|
114
|
+
requiresApprovalFor: [
|
|
115
|
+
'workspace edits',
|
|
116
|
+
'shell commands',
|
|
117
|
+
'private continuity changes',
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
privacy: {
|
|
121
|
+
publicOnly: true,
|
|
122
|
+
includesPrivateMemory: false,
|
|
123
|
+
includesExecutableCode: false,
|
|
124
|
+
includesSecrets: false,
|
|
125
|
+
},
|
|
126
|
+
skills: profile.skills.map(skill => ({
|
|
127
|
+
id: skill.id,
|
|
128
|
+
name: skill.name,
|
|
129
|
+
description: skill.description,
|
|
130
|
+
inputModes: skill.inputModes,
|
|
131
|
+
outputModes: skill.outputModes,
|
|
132
|
+
})),
|
|
133
|
+
}
|
|
134
|
+
return `${JSON.stringify(summary, null, 2)}\n`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createAgentCard(profile: PublicSkillsProfile, url = 'ipfs://pending-agent-endpoint'): AgentCard {
|
|
138
|
+
const inputModes = unique(profile.skills.flatMap(skill => skill.inputModes))
|
|
139
|
+
const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
|
|
140
|
+
return {
|
|
141
|
+
name: profile.name,
|
|
142
|
+
description: profile.description,
|
|
143
|
+
version: profile.version,
|
|
144
|
+
protocolVersion: '0.2.6',
|
|
145
|
+
url,
|
|
146
|
+
...(profile.imageUrl ? { image: profile.imageUrl, iconUrl: profile.imageUrl } : {}),
|
|
147
|
+
defaultInputModes: inputModes.length ? inputModes : ['text/markdown'],
|
|
148
|
+
defaultOutputModes: outputModes.length ? outputModes : ['text/markdown'],
|
|
149
|
+
capabilities: {
|
|
150
|
+
streaming: true,
|
|
151
|
+
pushNotifications: false,
|
|
152
|
+
},
|
|
153
|
+
skills: profile.skills.map(skill => ({
|
|
154
|
+
id: skill.id,
|
|
155
|
+
name: skill.name,
|
|
156
|
+
description: skill.description,
|
|
157
|
+
inputModes: [...skill.inputModes],
|
|
158
|
+
outputModes: [...skill.outputModes],
|
|
159
|
+
})),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function serializeAgentCard(card: AgentCard): string {
|
|
164
|
+
return `${JSON.stringify(card, null, 2)}\n`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function unique(values: string[]): string[] {
|
|
168
|
+
const out: string[] = []
|
|
169
|
+
for (const value of values) {
|
|
170
|
+
if (!out.includes(value)) out.push(value)
|
|
171
|
+
}
|
|
172
|
+
return out
|
|
173
|
+
}
|