ethagent 0.2.1 → 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 +25 -7
- 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,296 @@
|
|
|
1
|
+
import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storage/config.js'
|
|
2
|
+
import { getKey } from '../storage/secrets.js'
|
|
3
|
+
import { loadLocalHfModels } from './huggingface.js'
|
|
4
|
+
|
|
5
|
+
export type ModelCatalogSource = 'installed' | 'discovered' | 'fallback'
|
|
6
|
+
|
|
7
|
+
export type ModelCatalogEntry = {
|
|
8
|
+
provider: ProviderId
|
|
9
|
+
id: string
|
|
10
|
+
label: string
|
|
11
|
+
description?: string
|
|
12
|
+
source: ModelCatalogSource
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ModelCatalogResult = {
|
|
16
|
+
provider: ProviderId
|
|
17
|
+
entries: ModelCatalogEntry[]
|
|
18
|
+
status: 'ok' | 'fallback'
|
|
19
|
+
error?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type DiscoverDeps = {
|
|
23
|
+
fetchImpl?: typeof fetch
|
|
24
|
+
loadKey?: (provider: ProviderId) => Promise<string | null>
|
|
25
|
+
listHfLocal?: () => Promise<Array<{ id: string; displayName: string }>>
|
|
26
|
+
now?: () => number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const OPENAI_DEFAULT_BASE_URL = 'https://api.openai.com/v1'
|
|
30
|
+
const ANTHROPIC_VERSION = '2023-06-01'
|
|
31
|
+
const CACHE_TTL_MS = 60_000
|
|
32
|
+
|
|
33
|
+
type CacheValue = {
|
|
34
|
+
expiresAt: number
|
|
35
|
+
entries: ModelCatalogEntry[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cache = new Map<string, CacheValue>()
|
|
39
|
+
|
|
40
|
+
export function openAIBaseUrlFor(config: Pick<EthagentConfig, 'provider' | 'baseUrl'>): string {
|
|
41
|
+
return config.provider === 'openai' && config.baseUrl
|
|
42
|
+
? config.baseUrl
|
|
43
|
+
: OPENAI_DEFAULT_BASE_URL
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function clearModelCatalogCache(): void {
|
|
47
|
+
cache.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function discoverProviderModels(
|
|
51
|
+
config: EthagentConfig,
|
|
52
|
+
deps: DiscoverDeps = {},
|
|
53
|
+
): Promise<ModelCatalogResult> {
|
|
54
|
+
const provider = config.provider
|
|
55
|
+
if (provider === 'llamacpp') {
|
|
56
|
+
try {
|
|
57
|
+
const installed = await (deps.listHfLocal ?? (() => loadLocalHfModels()))()
|
|
58
|
+
return {
|
|
59
|
+
provider,
|
|
60
|
+
status: 'ok',
|
|
61
|
+
entries: dedupeEntries(installed.map(model => ({
|
|
62
|
+
provider,
|
|
63
|
+
id: model.id,
|
|
64
|
+
label: model.displayName,
|
|
65
|
+
source: 'installed' as const,
|
|
66
|
+
}))),
|
|
67
|
+
}
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
return fallbackResult(config, (err as Error).message)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const loadKey = deps.loadKey ?? getKey
|
|
74
|
+
const apiKey = await loadKey(provider)
|
|
75
|
+
if (!apiKey) return fallbackResult(config, `missing ${provider} API key`)
|
|
76
|
+
|
|
77
|
+
const baseUrl = provider === 'openai' ? openAIBaseUrlFor(config) : ''
|
|
78
|
+
const key = cacheKey(provider, baseUrl, true)
|
|
79
|
+
const now = deps.now?.() ?? Date.now()
|
|
80
|
+
const cached = cache.get(key)
|
|
81
|
+
if (cached && cached.expiresAt > now) {
|
|
82
|
+
return { provider, status: 'ok', entries: cached.entries }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const fetchImpl = deps.fetchImpl ?? fetch
|
|
87
|
+
const entries =
|
|
88
|
+
provider === 'openai'
|
|
89
|
+
? await discoverOpenAIModels(fetchImpl, provider, baseUrl, apiKey, isDefaultOpenAIBaseUrl(baseUrl))
|
|
90
|
+
: provider === 'anthropic'
|
|
91
|
+
? await discoverAnthropicModels(fetchImpl, apiKey)
|
|
92
|
+
: await discoverGeminiModels(fetchImpl, apiKey)
|
|
93
|
+
const deduped = dedupeEntries(entries)
|
|
94
|
+
cache.set(key, { expiresAt: now + CACHE_TTL_MS, entries: deduped })
|
|
95
|
+
return { provider, status: 'ok', entries: deduped }
|
|
96
|
+
} catch (err: unknown) {
|
|
97
|
+
return fallbackResult(config, (err as Error).message)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function fallbackResult(config: EthagentConfig, error?: string): ModelCatalogResult {
|
|
102
|
+
const provider = config.provider
|
|
103
|
+
return {
|
|
104
|
+
provider,
|
|
105
|
+
status: 'fallback',
|
|
106
|
+
error,
|
|
107
|
+
entries: dedupeEntries([
|
|
108
|
+
{
|
|
109
|
+
provider,
|
|
110
|
+
id: config.model,
|
|
111
|
+
label: config.model,
|
|
112
|
+
source: 'fallback',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
provider,
|
|
116
|
+
id: defaultModelFor(provider),
|
|
117
|
+
label: defaultModelFor(provider),
|
|
118
|
+
source: 'fallback',
|
|
119
|
+
},
|
|
120
|
+
]),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const OPENAI_CHAT_PREFIXES = ['gpt-', 'chatgpt-', 'o1', 'o3', 'o4']
|
|
125
|
+
const OPENAI_NON_CHAT_PATTERNS: RegExp[] = [
|
|
126
|
+
/-instruct(?:$|-)/,
|
|
127
|
+
/-realtime(?:$|-)/,
|
|
128
|
+
/-audio(?:$|-)/,
|
|
129
|
+
/-transcribe(?:$|-)/,
|
|
130
|
+
/-tts(?:$|-)/,
|
|
131
|
+
/-search-preview(?:$|-)/,
|
|
132
|
+
/-image(?:$|-)/,
|
|
133
|
+
]
|
|
134
|
+
const OPENAI_NON_CHAT_FAMILIES = [
|
|
135
|
+
'text-embedding-',
|
|
136
|
+
'text-similarity-',
|
|
137
|
+
'text-search-',
|
|
138
|
+
'whisper-',
|
|
139
|
+
'tts-',
|
|
140
|
+
'dall-e-',
|
|
141
|
+
'gpt-image-',
|
|
142
|
+
'omni-moderation-',
|
|
143
|
+
'text-moderation-',
|
|
144
|
+
'babbage-',
|
|
145
|
+
'davinci-',
|
|
146
|
+
'computer-use-',
|
|
147
|
+
]
|
|
148
|
+
const OPENAI_NON_CHAT_EXACT = new Set(['davinci', 'babbage', 'ada', 'curie'])
|
|
149
|
+
|
|
150
|
+
function isChatCapableOpenAIModel(id: string): boolean {
|
|
151
|
+
if (OPENAI_NON_CHAT_EXACT.has(id)) return false
|
|
152
|
+
if (OPENAI_NON_CHAT_FAMILIES.some(prefix => id.startsWith(prefix))) return false
|
|
153
|
+
if (OPENAI_NON_CHAT_PATTERNS.some(re => re.test(id))) return false
|
|
154
|
+
return OPENAI_CHAT_PREFIXES.some(prefix => id.startsWith(prefix))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isDefaultOpenAIBaseUrl(baseUrl: string): boolean {
|
|
158
|
+
return baseUrl.replace(/\/+$/, '') === OPENAI_DEFAULT_BASE_URL
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function discoverOpenAIModels(
|
|
162
|
+
fetchImpl: typeof fetch,
|
|
163
|
+
provider: ProviderId,
|
|
164
|
+
baseUrl: string,
|
|
165
|
+
apiKey: string,
|
|
166
|
+
applyChatFilter: boolean,
|
|
167
|
+
): Promise<ModelCatalogEntry[]> {
|
|
168
|
+
const urls = openAIModelUrls(baseUrl)
|
|
169
|
+
let lastError: Error | undefined
|
|
170
|
+
for (const url of urls) {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetchImpl(url, {
|
|
173
|
+
method: 'GET',
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Bearer ${apiKey}`,
|
|
176
|
+
Accept: 'application/json',
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
lastError = new Error(`HTTP ${response.status}`)
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
const data = await response.json() as { data?: Array<{ id?: unknown }> }
|
|
184
|
+
return (data.data ?? [])
|
|
185
|
+
.filter(item => typeof item.id === 'string' && item.id.length > 0)
|
|
186
|
+
.filter(item => !applyChatFilter || isChatCapableOpenAIModel(item.id as string))
|
|
187
|
+
.map(item => ({
|
|
188
|
+
provider,
|
|
189
|
+
id: item.id as string,
|
|
190
|
+
label: item.id as string,
|
|
191
|
+
source: 'discovered' as const,
|
|
192
|
+
}))
|
|
193
|
+
} catch (err: unknown) {
|
|
194
|
+
lastError = err as Error
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw lastError ?? new Error('no OpenAI model endpoint responded')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function openAIModelUrls(baseUrl: string): string[] {
|
|
201
|
+
const normalized = baseUrl.replace(/\/+$/, '')
|
|
202
|
+
const fallback = normalized.endsWith('/v1')
|
|
203
|
+
? `${normalized.slice(0, -3)}/models`
|
|
204
|
+
: `${normalized}/v1/models`
|
|
205
|
+
return dedupeStrings([`${normalized}/models`, fallback])
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function discoverAnthropicModels(
|
|
209
|
+
fetchImpl: typeof fetch,
|
|
210
|
+
apiKey: string,
|
|
211
|
+
): Promise<ModelCatalogEntry[]> {
|
|
212
|
+
const response = await fetchImpl('https://api.anthropic.com/v1/models', {
|
|
213
|
+
method: 'GET',
|
|
214
|
+
headers: {
|
|
215
|
+
'x-api-key': apiKey,
|
|
216
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
217
|
+
Accept: 'application/json',
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
221
|
+
const data = await response.json() as { data?: Array<{ id?: unknown; display_name?: unknown }> }
|
|
222
|
+
return (data.data ?? [])
|
|
223
|
+
.filter(item => typeof item.id === 'string' && item.id.length > 0)
|
|
224
|
+
.map(item => ({
|
|
225
|
+
provider: 'anthropic',
|
|
226
|
+
id: item.id as string,
|
|
227
|
+
label: typeof item.display_name === 'string' && item.display_name.length > 0
|
|
228
|
+
? item.display_name
|
|
229
|
+
: item.id as string,
|
|
230
|
+
source: 'discovered' as const,
|
|
231
|
+
}))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function discoverGeminiModels(
|
|
235
|
+
fetchImpl: typeof fetch,
|
|
236
|
+
apiKey: string,
|
|
237
|
+
): Promise<ModelCatalogEntry[]> {
|
|
238
|
+
const response = await fetchImpl(
|
|
239
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`,
|
|
240
|
+
{
|
|
241
|
+
method: 'GET',
|
|
242
|
+
headers: { Accept: 'application/json' },
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
246
|
+
const data = await response.json() as {
|
|
247
|
+
models?: Array<{
|
|
248
|
+
name?: unknown
|
|
249
|
+
displayName?: unknown
|
|
250
|
+
description?: unknown
|
|
251
|
+
supportedGenerationMethods?: unknown
|
|
252
|
+
}>
|
|
253
|
+
}
|
|
254
|
+
return (data.models ?? [])
|
|
255
|
+
.filter(item => typeof item.name === 'string' && item.name.length > 0)
|
|
256
|
+
.filter(item => Array.isArray(item.supportedGenerationMethods)
|
|
257
|
+
&& item.supportedGenerationMethods.includes('generateContent'))
|
|
258
|
+
.map(item => {
|
|
259
|
+
const id = (item.name as string).replace(/^models\//, '')
|
|
260
|
+
return {
|
|
261
|
+
provider: 'gemini' as const,
|
|
262
|
+
id,
|
|
263
|
+
label: typeof item.displayName === 'string' && item.displayName.length > 0
|
|
264
|
+
? item.displayName
|
|
265
|
+
: id,
|
|
266
|
+
description: typeof item.description === 'string' ? item.description : undefined,
|
|
267
|
+
source: 'discovered' as const,
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cacheKey(provider: ProviderId, baseUrl: string, hasKey: boolean): string {
|
|
273
|
+
return `${provider}\0${baseUrl}\0${hasKey ? 'key' : 'no-key'}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function dedupeEntries(entries: ModelCatalogEntry[]): ModelCatalogEntry[] {
|
|
277
|
+
const seen = new Set<string>()
|
|
278
|
+
const out: ModelCatalogEntry[] = []
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
if (seen.has(entry.id)) continue
|
|
281
|
+
seen.add(entry.id)
|
|
282
|
+
out.push(entry)
|
|
283
|
+
}
|
|
284
|
+
return out
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function dedupeStrings(values: string[]): string[] {
|
|
288
|
+
const seen = new Set<string>()
|
|
289
|
+
const out: string[] = []
|
|
290
|
+
for (const value of values) {
|
|
291
|
+
if (seen.has(value)) continue
|
|
292
|
+
seen.add(value)
|
|
293
|
+
out.push(value)
|
|
294
|
+
}
|
|
295
|
+
return out
|
|
296
|
+
}
|