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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -32
  3. package/bin/ethagent.js +11 -2
  4. package/package.json +30 -8
  5. package/src/app/FirstRun.tsx +412 -0
  6. package/src/app/hooks/useCancelRequest.ts +22 -0
  7. package/src/app/hooks/useDoublePress.ts +46 -0
  8. package/src/app/hooks/useExitOnCtrlC.ts +36 -0
  9. package/src/app/input/AppInputProvider.tsx +116 -0
  10. package/src/app/input/appInputParser.ts +279 -0
  11. package/src/app/keybindings/KeybindingProvider.tsx +134 -0
  12. package/src/app/keybindings/resolver.ts +42 -0
  13. package/src/app/keybindings/types.ts +26 -0
  14. package/src/chat/ChatBottomPane.tsx +280 -0
  15. package/src/chat/ChatInput.tsx +722 -0
  16. package/src/chat/ChatScreen.tsx +1575 -0
  17. package/src/chat/ContextLimitView.tsx +95 -0
  18. package/src/chat/ContinuityEditReviewView.tsx +48 -0
  19. package/src/chat/ConversationStack.tsx +47 -0
  20. package/src/chat/CopyPicker.tsx +52 -0
  21. package/src/chat/MessageList.tsx +609 -0
  22. package/src/chat/PermissionPrompt.tsx +153 -0
  23. package/src/chat/PermissionsView.tsx +159 -0
  24. package/src/chat/PlanApprovalView.tsx +91 -0
  25. package/src/chat/ResumeView.tsx +267 -0
  26. package/src/chat/RewindView.tsx +386 -0
  27. package/src/chat/SessionStatus.tsx +51 -0
  28. package/src/chat/TranscriptView.tsx +202 -0
  29. package/src/chat/chatInputState.ts +247 -0
  30. package/src/chat/chatPaste.ts +49 -0
  31. package/src/chat/chatScreenUtils.ts +187 -0
  32. package/src/chat/chatSessionState.ts +142 -0
  33. package/src/chat/chatTurnOrchestrator.ts +701 -0
  34. package/src/chat/commands.ts +673 -0
  35. package/src/chat/textCursor.ts +202 -0
  36. package/src/chat/toolResultDisplay.ts +8 -0
  37. package/src/chat/transcriptViewport.ts +247 -0
  38. package/src/cli/ResetConfirmView.tsx +61 -0
  39. package/src/cli/main.tsx +177 -0
  40. package/src/cli/preview.tsx +19 -0
  41. package/src/cli/reset.ts +106 -0
  42. package/src/identity/continuity/editor.ts +149 -0
  43. package/src/identity/continuity/envelope.ts +345 -0
  44. package/src/identity/continuity/history.ts +153 -0
  45. package/src/identity/continuity/privateEdit.ts +334 -0
  46. package/src/identity/continuity/publicSkills.ts +173 -0
  47. package/src/identity/continuity/snapshots.ts +183 -0
  48. package/src/identity/continuity/storage.ts +507 -0
  49. package/src/identity/crypto/backupEnvelope.ts +486 -0
  50. package/src/identity/crypto/eth.ts +137 -0
  51. package/src/identity/hub/IdentityHub.tsx +868 -0
  52. package/src/identity/hub/identityHubEffects.ts +1146 -0
  53. package/src/identity/hub/identityHubModel.ts +291 -0
  54. package/src/identity/hub/identityHubReducer.ts +212 -0
  55. package/src/identity/hub/screens/BusyScreen.tsx +26 -0
  56. package/src/identity/hub/screens/ContinuityDashboardScreen.tsx +144 -0
  57. package/src/identity/hub/screens/CreateFlow.tsx +206 -0
  58. package/src/identity/hub/screens/DetailsScreen.tsx +64 -0
  59. package/src/identity/hub/screens/EditProfileFlow.tsx +145 -0
  60. package/src/identity/hub/screens/ErrorScreen.tsx +35 -0
  61. package/src/identity/hub/screens/IdentitySummary.tsx +70 -0
  62. package/src/identity/hub/screens/MenuScreen.tsx +117 -0
  63. package/src/identity/hub/screens/NetworkScreen.tsx +41 -0
  64. package/src/identity/hub/screens/RebackupStorageScreen.tsx +50 -0
  65. package/src/identity/hub/screens/RecoveryConfirmScreen.tsx +85 -0
  66. package/src/identity/hub/screens/RestoreFlow.tsx +206 -0
  67. package/src/identity/hub/screens/StorageCredentialScreen.tsx +128 -0
  68. package/src/identity/hub/screens/WalletApprovalScreen.tsx +43 -0
  69. package/src/identity/profile/imagePicker.ts +180 -0
  70. package/src/identity/registry/erc8004.ts +1106 -0
  71. package/src/identity/registry/registryConfig.ts +69 -0
  72. package/src/identity/storage/ipfs.ts +212 -0
  73. package/src/identity/storage/pinataJwt.ts +53 -0
  74. package/src/identity/wallet/browserWallet.ts +393 -0
  75. package/src/identity/wallet/wallet-page/wallet.html +1082 -0
  76. package/src/mcp/approvals.ts +113 -0
  77. package/src/mcp/config.ts +235 -0
  78. package/src/mcp/manager.ts +541 -0
  79. package/src/mcp/names.ts +19 -0
  80. package/src/mcp/output.ts +96 -0
  81. package/src/models/ModelPicker.tsx +1446 -0
  82. package/src/models/catalog.ts +296 -0
  83. package/src/models/huggingface.ts +651 -0
  84. package/src/models/llamacpp.ts +810 -0
  85. package/src/models/llamacppPreflight.ts +150 -0
  86. package/src/models/modelDisplay.ts +105 -0
  87. package/src/models/modelPickerOptions.ts +421 -0
  88. package/src/models/modelRecommendation.ts +140 -0
  89. package/src/models/runtimeDetection.ts +81 -0
  90. package/src/models/uncensoredCatalog.ts +86 -0
  91. package/src/providers/anthropic.ts +259 -0
  92. package/src/providers/contracts.ts +62 -0
  93. package/src/providers/errors.ts +62 -0
  94. package/src/providers/gemini.ts +152 -0
  95. package/src/providers/openai-chat.ts +472 -0
  96. package/src/providers/registry.ts +42 -0
  97. package/src/providers/retry.ts +58 -0
  98. package/src/providers/sse.ts +93 -0
  99. package/src/runtime/compaction.ts +389 -0
  100. package/src/runtime/cwd.ts +43 -0
  101. package/src/runtime/sessionMode.ts +55 -0
  102. package/src/runtime/systemPrompt.ts +209 -0
  103. package/src/runtime/toolClaimGuards.ts +143 -0
  104. package/src/runtime/toolExecution.ts +304 -0
  105. package/src/runtime/toolIntent.ts +163 -0
  106. package/src/runtime/turn.ts +858 -0
  107. package/src/storage/atomicWrite.ts +68 -0
  108. package/src/storage/config.ts +189 -0
  109. package/src/storage/factoryReset.ts +130 -0
  110. package/src/storage/history.ts +58 -0
  111. package/src/storage/identity.ts +99 -0
  112. package/src/storage/permissions.ts +76 -0
  113. package/src/storage/rewind.ts +246 -0
  114. package/src/storage/secrets.ts +181 -0
  115. package/src/storage/sessionExport.ts +49 -0
  116. package/src/storage/sessions.ts +482 -0
  117. package/src/tools/bashSafety.ts +174 -0
  118. package/src/tools/bashTool.ts +140 -0
  119. package/src/tools/changeDirectoryTool.ts +213 -0
  120. package/src/tools/contracts.ts +179 -0
  121. package/src/tools/deleteFileTool.ts +111 -0
  122. package/src/tools/editTool.ts +160 -0
  123. package/src/tools/editUtils.ts +170 -0
  124. package/src/tools/listDirectoryTool.ts +55 -0
  125. package/src/tools/mcpResourceTools.ts +95 -0
  126. package/src/tools/permissionRules.ts +85 -0
  127. package/src/tools/privateContinuityEditTool.ts +178 -0
  128. package/src/tools/privateContinuityReadTool.ts +107 -0
  129. package/src/tools/readTool.ts +85 -0
  130. package/src/tools/registry.ts +67 -0
  131. package/src/tools/writeFileTool.ts +142 -0
  132. package/src/ui/BrandSplash.tsx +193 -0
  133. package/src/ui/ProgressBar.tsx +34 -0
  134. package/src/ui/Select.tsx +143 -0
  135. package/src/ui/Spinner.tsx +269 -0
  136. package/src/ui/Surface.tsx +47 -0
  137. package/src/ui/TextInput.tsx +97 -0
  138. package/src/ui/theme.ts +59 -0
  139. package/src/utils/clipboard.ts +216 -0
  140. package/src/utils/markdownSegments.ts +51 -0
  141. package/src/utils/messages.ts +35 -0
  142. package/src/utils/withRetry.ts +280 -0
  143. 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
+ }