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.
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 +25 -7
  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,113 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { z } from 'zod'
4
+ import { atomicWriteText } from '../storage/atomicWrite.js'
5
+ import { ensureConfigDir, getConfigDir } from '../storage/config.js'
6
+
7
+ export type ProjectMcpDecision = 'approved' | 'rejected'
8
+
9
+ const ProjectDecisionSchema = z.object({
10
+ workspaceRoot: z.string().min(1),
11
+ serverName: z.string().min(1),
12
+ configHash: z.string().min(1),
13
+ decision: z.enum(['approved', 'rejected']),
14
+ })
15
+
16
+ const DisabledServerSchema = z.object({
17
+ workspaceRoot: z.string().min(1),
18
+ serverName: z.string().min(1),
19
+ })
20
+
21
+ const McpLocalStateSchema = z.object({
22
+ version: z.literal(1),
23
+ projectServers: z.array(ProjectDecisionSchema),
24
+ disabledServers: z.array(DisabledServerSchema),
25
+ })
26
+
27
+ type McpLocalState = z.infer<typeof McpLocalStateSchema>
28
+
29
+ function getMcpLocalStatePath(): string {
30
+ return path.join(getConfigDir(), 'mcp-state.json')
31
+ }
32
+
33
+ export async function getProjectMcpDecision(params: {
34
+ workspaceRoot: string
35
+ serverName: string
36
+ configHash: string
37
+ }): Promise<ProjectMcpDecision | undefined> {
38
+ const state = await loadMcpLocalState()
39
+ const workspaceRoot = path.resolve(params.workspaceRoot)
40
+ return state.projectServers.find(entry =>
41
+ path.resolve(entry.workspaceRoot) === workspaceRoot &&
42
+ entry.serverName === params.serverName &&
43
+ entry.configHash === params.configHash,
44
+ )?.decision
45
+ }
46
+
47
+ export async function setProjectMcpDecision(params: {
48
+ workspaceRoot: string
49
+ serverName: string
50
+ configHash: string
51
+ decision: ProjectMcpDecision
52
+ }): Promise<void> {
53
+ const state = await loadMcpLocalState()
54
+ const workspaceRoot = path.resolve(params.workspaceRoot)
55
+ const next = state.projectServers.filter(entry =>
56
+ !(path.resolve(entry.workspaceRoot) === workspaceRoot && entry.serverName === params.serverName),
57
+ )
58
+ next.push({
59
+ workspaceRoot,
60
+ serverName: params.serverName,
61
+ configHash: params.configHash,
62
+ decision: params.decision,
63
+ })
64
+ await writeMcpLocalState({ ...state, projectServers: next })
65
+ }
66
+
67
+ export async function isMcpServerDisabled(workspaceRoot: string, serverName: string): Promise<boolean> {
68
+ const state = await loadMcpLocalState()
69
+ const normalizedWorkspaceRoot = path.resolve(workspaceRoot)
70
+ return state.disabledServers.some(entry =>
71
+ path.resolve(entry.workspaceRoot) === normalizedWorkspaceRoot && entry.serverName === serverName,
72
+ )
73
+ }
74
+
75
+ export async function setMcpServerEnabled(params: {
76
+ workspaceRoot: string
77
+ serverName: string
78
+ enabled: boolean
79
+ }): Promise<void> {
80
+ const state = await loadMcpLocalState()
81
+ const workspaceRoot = path.resolve(params.workspaceRoot)
82
+ const withoutServer = state.disabledServers.filter(entry =>
83
+ !(path.resolve(entry.workspaceRoot) === workspaceRoot && entry.serverName === params.serverName),
84
+ )
85
+ const disabledServers = params.enabled
86
+ ? withoutServer
87
+ : [...withoutServer, { workspaceRoot, serverName: params.serverName }]
88
+ await writeMcpLocalState({ ...state, disabledServers })
89
+ }
90
+
91
+ async function loadMcpLocalState(): Promise<McpLocalState> {
92
+ let raw: string
93
+ try {
94
+ raw = await fs.readFile(getMcpLocalStatePath(), 'utf8')
95
+ } catch (err: unknown) {
96
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return emptyState()
97
+ throw err
98
+ }
99
+ try {
100
+ return McpLocalStateSchema.parse(JSON.parse(raw))
101
+ } catch {
102
+ return emptyState()
103
+ }
104
+ }
105
+
106
+ async function writeMcpLocalState(state: McpLocalState): Promise<void> {
107
+ await ensureConfigDir()
108
+ await atomicWriteText(getMcpLocalStatePath(), JSON.stringify(McpLocalStateSchema.parse(state), null, 2) + '\n')
109
+ }
110
+
111
+ function emptyState(): McpLocalState {
112
+ return { version: 1, projectServers: [], disabledServers: [] }
113
+ }
@@ -0,0 +1,235 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import { z } from 'zod'
5
+ import { atomicWriteText } from '../storage/atomicWrite.js'
6
+ import { ensureConfigDir, getConfigDir } from '../storage/config.js'
7
+
8
+ export type McpConfigScope = 'user' | 'project'
9
+
10
+ const McpStdioServerConfigSchema = z.object({
11
+ type: z.literal('stdio').optional(),
12
+ command: z.string().min(1),
13
+ args: z.array(z.string()).optional(),
14
+ env: z.record(z.string()).optional(),
15
+ cwd: z.string().min(1).optional(),
16
+ })
17
+
18
+ const McpHttpServerConfigSchema = z.object({
19
+ type: z.literal('http'),
20
+ url: z.string().url(),
21
+ headers: z.record(z.string()).optional(),
22
+ })
23
+
24
+ const McpSseServerConfigSchema = z.object({
25
+ type: z.literal('sse'),
26
+ url: z.string().url(),
27
+ headers: z.record(z.string()).optional(),
28
+ })
29
+
30
+ export const McpServerConfigSchema = z.union([
31
+ McpStdioServerConfigSchema,
32
+ McpHttpServerConfigSchema,
33
+ McpSseServerConfigSchema,
34
+ ])
35
+
36
+ export const McpJsonConfigSchema = z.object({
37
+ mcpServers: z.record(McpServerConfigSchema),
38
+ })
39
+
40
+ export type McpServerConfig = z.infer<typeof McpServerConfigSchema>
41
+ export type McpJsonConfig = z.infer<typeof McpJsonConfigSchema>
42
+
43
+ export type ScopedMcpServerConfig = {
44
+ name: string
45
+ scope: McpConfigScope
46
+ config: McpServerConfig
47
+ configHash: string
48
+ }
49
+
50
+ export type McpConfigIssue = {
51
+ scope: McpConfigScope
52
+ filePath: string
53
+ serverName?: string
54
+ severity: 'error' | 'warning'
55
+ message: string
56
+ }
57
+
58
+ export type LoadedMcpConfigs = {
59
+ servers: ScopedMcpServerConfig[]
60
+ issues: McpConfigIssue[]
61
+ }
62
+
63
+ export function getUserMcpConfigPath(): string {
64
+ return path.join(getConfigDir(), 'mcp.json')
65
+ }
66
+
67
+ export function getProjectMcpConfigPath(cwd: string): string {
68
+ return path.join(path.resolve(cwd), '.mcp.json')
69
+ }
70
+
71
+ export async function loadMcpConfigs(cwd: string): Promise<LoadedMcpConfigs> {
72
+ const user = await readMcpConfigFile(getUserMcpConfigPath(), 'user', true)
73
+ const project = await readMcpConfigFile(getProjectMcpConfigPath(cwd), 'project', true)
74
+ const byName = new Map<string, ScopedMcpServerConfig>()
75
+ for (const server of user.servers) byName.set(server.name, server)
76
+ for (const server of project.servers) byName.set(server.name, server)
77
+ return {
78
+ servers: [...byName.values()],
79
+ issues: [...user.issues, ...project.issues],
80
+ }
81
+ }
82
+
83
+ export async function addMcpServerConfig(params: {
84
+ cwd: string
85
+ scope: McpConfigScope
86
+ name: string
87
+ config: McpServerConfig
88
+ }): Promise<string> {
89
+ const filePath = params.scope === 'user' ? getUserMcpConfigPath() : getProjectMcpConfigPath(params.cwd)
90
+ const current = await readRawMcpConfig(filePath)
91
+ current.mcpServers[params.name] = params.config
92
+ if (params.scope === 'user') await ensureConfigDir()
93
+ await atomicWriteText(filePath, JSON.stringify(current, null, 2) + '\n')
94
+ return filePath
95
+ }
96
+
97
+ export function parseMcpServerConfigJson(value: string): McpServerConfig {
98
+ return McpServerConfigSchema.parse(JSON.parse(value))
99
+ }
100
+
101
+ export function mcpServerTransport(config: McpServerConfig): 'stdio' | 'http' | 'sse' {
102
+ return config.type === 'http' || config.type === 'sse' ? config.type : 'stdio'
103
+ }
104
+
105
+ export function stableMcpConfigHash(config: McpServerConfig): string {
106
+ return crypto.createHash('sha256').update(stableJson(config)).digest('hex').slice(0, 16)
107
+ }
108
+
109
+ async function readMcpConfigFile(
110
+ filePath: string,
111
+ scope: McpConfigScope,
112
+ expandVars: boolean,
113
+ ): Promise<LoadedMcpConfigs> {
114
+ let raw: string
115
+ try {
116
+ raw = await fs.readFile(filePath, 'utf8')
117
+ } catch (err: unknown) {
118
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return { servers: [], issues: [] }
119
+ return {
120
+ servers: [],
121
+ issues: [{ scope, filePath, severity: 'error', message: `failed to read MCP config: ${(err as Error).message}` }],
122
+ }
123
+ }
124
+
125
+ let parsedJson: unknown
126
+ try {
127
+ parsedJson = JSON.parse(raw)
128
+ } catch (err: unknown) {
129
+ return {
130
+ servers: [],
131
+ issues: [{ scope, filePath, severity: 'error', message: `MCP config is not valid JSON: ${(err as Error).message}` }],
132
+ }
133
+ }
134
+
135
+ const parsed = McpJsonConfigSchema.safeParse(parsedJson)
136
+ if (!parsed.success) {
137
+ return {
138
+ servers: [],
139
+ issues: parsed.error.issues.map(issue => ({
140
+ scope,
141
+ filePath,
142
+ severity: 'error',
143
+ message: `MCP config schema error at ${issue.path.join('.') || 'root'}: ${issue.message}`,
144
+ })),
145
+ }
146
+ }
147
+
148
+ const servers: ScopedMcpServerConfig[] = []
149
+ const issues: McpConfigIssue[] = []
150
+ for (const [name, config] of Object.entries(parsed.data.mcpServers)) {
151
+ const expanded = expandVars ? expandEnvVars(config) : { config, missing: [] }
152
+ if (expanded.missing.length > 0) {
153
+ issues.push({
154
+ scope,
155
+ filePath,
156
+ serverName: name,
157
+ severity: 'warning',
158
+ message: `missing environment variables: ${expanded.missing.join(', ')}`,
159
+ })
160
+ }
161
+ if (process.platform === 'win32' && 'command' in expanded.config && isBareNpxCommand(expanded.config.command)) {
162
+ issues.push({
163
+ scope,
164
+ filePath,
165
+ serverName: name,
166
+ severity: 'warning',
167
+ message: 'Windows MCP stdio servers should use command "cmd" with args ["/c", "npx", ...]',
168
+ })
169
+ }
170
+ servers.push({
171
+ name,
172
+ scope,
173
+ config: expanded.config,
174
+ configHash: stableMcpConfigHash(expanded.config),
175
+ })
176
+ }
177
+ return { servers, issues }
178
+ }
179
+
180
+ async function readRawMcpConfig(filePath: string): Promise<McpJsonConfig> {
181
+ let raw: string
182
+ try {
183
+ raw = await fs.readFile(filePath, 'utf8')
184
+ } catch (err: unknown) {
185
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return { mcpServers: {} }
186
+ throw err
187
+ }
188
+ return McpJsonConfigSchema.parse(JSON.parse(raw))
189
+ }
190
+
191
+ function expandEnvVars(config: McpServerConfig): { config: McpServerConfig; missing: string[] } {
192
+ const missing = new Set<string>()
193
+ const expanded = expandValue(config, missing)
194
+ return { config: McpServerConfigSchema.parse(expanded), missing: [...missing].sort() }
195
+ }
196
+
197
+ function expandValue(value: unknown, missing: Set<string>): unknown {
198
+ if (typeof value === 'string') return expandString(value, missing)
199
+ if (Array.isArray(value)) return value.map(item => expandValue(item, missing))
200
+ if (value && typeof value === 'object') {
201
+ const out: Record<string, unknown> = {}
202
+ for (const [key, child] of Object.entries(value)) out[key] = expandValue(child, missing)
203
+ return out
204
+ }
205
+ return value
206
+ }
207
+
208
+ function expandString(value: string, missing: Set<string>): string {
209
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-(.*?))?\}/g, (_match, name: string, fallback: string | undefined) => {
210
+ const envValue = process.env[name]
211
+ if (envValue !== undefined) return envValue
212
+ if (fallback !== undefined) return fallback
213
+ missing.add(name)
214
+ return ''
215
+ })
216
+ }
217
+
218
+ function isBareNpxCommand(command: string): boolean {
219
+ const normalized = command.replace(/\\/g, '/').toLowerCase()
220
+ return normalized === 'npx' || normalized.endsWith('/npx')
221
+ }
222
+
223
+ function stableJson(value: unknown): string {
224
+ return JSON.stringify(sortForStableJson(value))
225
+ }
226
+
227
+ function sortForStableJson(value: unknown): unknown {
228
+ if (Array.isArray(value)) return value.map(sortForStableJson)
229
+ if (value && typeof value === 'object') {
230
+ const out: Record<string, unknown> = {}
231
+ for (const key of Object.keys(value).sort()) out[key] = sortForStableJson((value as Record<string, unknown>)[key])
232
+ return out
233
+ }
234
+ return value
235
+ }