ethagent 3.0.1 → 3.1.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 (73) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +32 -117
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/cli/main.tsx +7 -0
  23. package/src/identity/continuity/challenges.ts +123 -0
  24. package/src/identity/continuity/envelope.ts +49 -1484
  25. package/src/identity/continuity/envelopeCreate.ts +322 -0
  26. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  27. package/src/identity/continuity/envelopeParse.ts +441 -0
  28. package/src/identity/continuity/envelopeTypes.ts +204 -0
  29. package/src/identity/continuity/envelopeVersion.ts +1 -0
  30. package/src/identity/continuity/payloadNormalization.ts +183 -0
  31. package/src/identity/continuity/publicSkills.ts +5 -5
  32. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  33. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  34. package/src/identity/continuity/skillsNormalization.ts +119 -0
  35. package/src/identity/continuity/snapshotToken.ts +28 -0
  36. package/src/identity/hub/continuity/completion.ts +67 -0
  37. package/src/identity/hub/continuity/effects.ts +5 -62
  38. package/src/identity/hub/profile/effects.ts +6 -170
  39. package/src/identity/hub/profile/operatorSave.ts +202 -0
  40. package/src/identity/registry/erc8004/metadata.ts +31 -23
  41. package/src/identity/wallet/browserWallet/html.ts +1 -57
  42. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  43. package/src/identity/wallet/page/controller.ts +1 -1
  44. package/src/identity/wallet/page/errorView.ts +122 -0
  45. package/src/identity/wallet/page/view.ts +3 -114
  46. package/src/mcp/manager.ts +8 -66
  47. package/src/mcp/managerHelpers.ts +70 -0
  48. package/src/models/ModelPicker.tsx +69 -889
  49. package/src/models/huggingface.ts +20 -137
  50. package/src/models/huggingfaceStorage.ts +136 -0
  51. package/src/models/llamacpp.ts +37 -303
  52. package/src/models/llamacppCommands.ts +44 -0
  53. package/src/models/llamacppConfig.ts +34 -0
  54. package/src/models/llamacppDiscovery.ts +176 -0
  55. package/src/models/llamacppOutput.ts +65 -0
  56. package/src/models/modelPickerCatalogFlow.ts +56 -0
  57. package/src/models/modelPickerCredentials.ts +166 -0
  58. package/src/models/modelPickerData.ts +41 -0
  59. package/src/models/modelPickerDisplay.tsx +132 -0
  60. package/src/models/modelPickerHfFlow.ts +192 -0
  61. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  62. package/src/models/modelPickerTypes.ts +69 -0
  63. package/src/models/modelPickerUninstallFlow.ts +48 -0
  64. package/src/models/modelPickerViewHelpers.ts +174 -0
  65. package/src/providers/openai-chat.ts +5 -124
  66. package/src/providers/openaiChatWire.ts +124 -0
  67. package/src/runtime/providerTurn.ts +38 -0
  68. package/src/runtime/textToolParser.ts +161 -0
  69. package/src/runtime/toolIntent.ts +1 -1
  70. package/src/runtime/turn.ts +43 -499
  71. package/src/runtime/turnNudges.ts +223 -0
  72. package/src/runtime/turnTypes.ts +86 -0
  73. package/src/ui/terminalTitle.ts +30 -0
@@ -1,8 +1,26 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { createHash } from 'node:crypto'
4
- import { getConfigDir, ensureConfigDir } from '../storage/config.js'
5
- import { atomicWriteText } from '../storage/atomicWrite.js'
4
+ import {
5
+ findLocalHfModel,
6
+ getLocalHfCacheDir,
7
+ getLocalHfModelsPath,
8
+ loadLocalHfModels,
9
+ localPathFor,
10
+ saveLocalHfModels,
11
+ uninstallLocalHfModel,
12
+ upsertLocalHfModel,
13
+ } from './huggingfaceStorage.js'
14
+
15
+ export {
16
+ findLocalHfModel,
17
+ getLocalHfCacheDir,
18
+ getLocalHfModelsPath,
19
+ loadLocalHfModels,
20
+ saveLocalHfModels,
21
+ uninstallLocalHfModel,
22
+ upsertLocalHfModel,
23
+ } from './huggingfaceStorage.js'
6
24
 
7
25
  export type HfFileFormat = 'gguf' | 'safetensors' | 'pickle/bin' | 'unknown'
8
26
  export type HfRuntime = 'llama.cpp runnable' | 'download-only' | 'unsupported'
@@ -110,11 +128,6 @@ export type HfDownloadProgress = {
110
128
  }
111
129
 
112
130
  type FetchImpl = typeof fetch
113
- type UninstallDeps = {
114
- unlink?: (target: string) => Promise<void>
115
- rmdir?: (target: string) => Promise<void>
116
- }
117
-
118
131
  type ModelInfoResponse = {
119
132
  id?: unknown
120
133
  author?: unknown
@@ -142,70 +155,6 @@ const COMMIT_RE = /^[a-f0-9]{40}$/i
142
155
  const DOWNLOAD_PROGRESS_MIN_MS = 100
143
156
  const DOWNLOAD_PROGRESS_MIN_BYTES = 16 * 1024 * 1024
144
157
 
145
- export function getLocalHfModelsPath(): string {
146
- return path.join(getConfigDir(), 'local-models.json')
147
- }
148
-
149
- export function getLocalHfCacheDir(): string {
150
- return path.join(getConfigDir(), 'models', 'huggingface')
151
- }
152
-
153
- export async function loadLocalHfModels(): Promise<LocalHfModel[]> {
154
- try {
155
- const raw = await fs.readFile(getLocalHfModelsPath(), 'utf8')
156
- const parsed = JSON.parse(raw) as unknown
157
- if (!Array.isArray(parsed)) return []
158
- return parsed.filter(isLocalHfModel)
159
- } catch (err: unknown) {
160
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
161
- return []
162
- }
163
- }
164
-
165
- export async function saveLocalHfModels(models: LocalHfModel[]): Promise<void> {
166
- await ensureConfigDir()
167
- await atomicWriteText(getLocalHfModelsPath(), JSON.stringify(models, null, 2) + '\n')
168
- }
169
-
170
- export async function upsertLocalHfModel(model: LocalHfModel): Promise<void> {
171
- const current = await loadLocalHfModels()
172
- const next = [
173
- model,
174
- ...current.filter(existing => existing.id !== model.id),
175
- ]
176
- await saveLocalHfModels(next)
177
- }
178
-
179
- export async function findLocalHfModel(id: string): Promise<LocalHfModel | null> {
180
- const models = await loadLocalHfModels()
181
- return models.find(model => model.id === id) ?? null
182
- }
183
-
184
- export async function uninstallLocalHfModel(
185
- id: string,
186
- deps: UninstallDeps = {},
187
- ): Promise<LocalHfModel | null> {
188
- const models = await loadLocalHfModels()
189
- const model = models.find(item => item.id === id)
190
- if (!model) return null
191
-
192
- const cacheRoot = path.resolve(getLocalHfCacheDir())
193
- const modelPath = path.resolve(model.localPath)
194
- const partialPath = path.resolve(`${model.localPath}.partial`)
195
- if (!isPathInside(cacheRoot, modelPath) || !isPathInside(cacheRoot, partialPath)) {
196
- throw new Error('Refusing to uninstall a local model outside EthAgent model cache')
197
- }
198
-
199
- const unlink = deps.unlink ?? ((target: string) => fs.unlink(target))
200
- const rmdir = deps.rmdir ?? ((target: string) => fs.rmdir(target))
201
- await unlinkIfPresent(modelPath, unlink)
202
- await unlinkIfPresent(partialPath, unlink)
203
- await cleanupEmptyParents(path.dirname(modelPath), cacheRoot, rmdir)
204
-
205
- await saveLocalHfModels(models.filter(item => item.id !== id))
206
- return model
207
- }
208
-
209
158
  export function parseHuggingFaceRef(input: string): HuggingFaceRef {
210
159
  const trimmed = input.trim()
211
160
  if (!trimmed) throw new Error('Hugging Face model link or repo id is required')
@@ -675,55 +624,6 @@ export function quantizationFromFilename(filename: string): string | undefined {
675
624
  return match?.[1]
676
625
  }
677
626
 
678
- function localPathFor(repoId: string, revision: string, filename: string): string {
679
- const repoParts = repoId.split('/').map(safePathPart)
680
- const fileParts = filename.split('/').map(safePathPart)
681
- return path.join(getLocalHfCacheDir(), ...repoParts, safePathPart(revision), ...fileParts)
682
- }
683
-
684
- async function unlinkIfPresent(
685
- target: string,
686
- unlink: (target: string) => Promise<void>,
687
- ): Promise<void> {
688
- try {
689
- await unlink(target)
690
- } catch (err: unknown) {
691
- const code = (err as NodeJS.ErrnoException).code
692
- if (code === 'ENOENT') return
693
- if (code === 'EBUSY' || code === 'EPERM' || code === 'EACCES') {
694
- throw new Error('That model file is currently in use. Stop the local runner and try uninstall again.')
695
- }
696
- throw err
697
- }
698
- }
699
-
700
- async function cleanupEmptyParents(
701
- startDir: string,
702
- cacheRoot: string,
703
- rmdir: (target: string) => Promise<void>,
704
- ): Promise<void> {
705
- let current = path.resolve(startDir)
706
- while (isPathInside(cacheRoot, current) && current !== cacheRoot) {
707
- try {
708
- await rmdir(current)
709
- } catch (err: unknown) {
710
- const code = (err as NodeJS.ErrnoException).code
711
- if (code === 'ENOENT') {
712
- current = path.dirname(current)
713
- continue
714
- }
715
- if (code === 'ENOTEMPTY' || code === 'EEXIST' || code === 'EPERM') return
716
- throw err
717
- }
718
- current = path.dirname(current)
719
- }
720
- }
721
-
722
- function isPathInside(root: string, target: string): boolean {
723
- const relative = path.relative(root, target)
724
- return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative)
725
- }
726
-
727
627
  function resolveUrl(repoId: string, revision: string, filename: string): string {
728
628
  return `${HF_BASE_URL}/${encodeRepoPath(repoId)}/resolve/${encodeURIComponent(revision)}/${encodePath(filename)}?download=true`
729
629
  }
@@ -736,11 +636,6 @@ function encodePath(value: string): string {
736
636
  return value.split('/').map(part => encodeURIComponent(part)).join('/')
737
637
  }
738
638
 
739
- function safePathPart(value: string): string {
740
- const cleaned = value.replace(/[^a-zA-Z0-9._-]/g, '_').replace(/^\.+$/, '_')
741
- return cleaned || '_'
742
- }
743
-
744
639
  function parseSibling(value: unknown): HuggingFaceSibling[] {
745
640
  if (!value || typeof value !== 'object') return []
746
641
  const record = value as { rfilename?: unknown; filename?: unknown; size?: unknown; lfs?: unknown }
@@ -815,15 +710,3 @@ function sizeClassFor(sizeBytes: number): HfSizeClass {
815
710
  if (gb < 24) return 'medium'
816
711
  return 'large'
817
712
  }
818
-
819
- function isLocalHfModel(value: unknown): value is LocalHfModel {
820
- if (!value || typeof value !== 'object' || Array.isArray(value)) return false
821
- const item = value as Partial<LocalHfModel>
822
- return item.provider === 'llamacpp'
823
- && typeof item.id === 'string'
824
- && typeof item.repoId === 'string'
825
- && typeof item.filename === 'string'
826
- && typeof item.displayName === 'string'
827
- && typeof item.localPath === 'string'
828
- && item.status === 'ready'
829
- }
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { getConfigDir, ensureConfigDir } from '../storage/config.js'
4
+ import { atomicWriteText } from '../storage/atomicWrite.js'
5
+ import type { LocalHfModel } from './huggingface.js'
6
+
7
+ type UninstallDeps = {
8
+ unlink?: (target: string) => Promise<void>
9
+ rmdir?: (target: string) => Promise<void>
10
+ }
11
+
12
+ export function getLocalHfModelsPath(): string {
13
+ return path.join(getConfigDir(), 'local-models.json')
14
+ }
15
+
16
+ export function getLocalHfCacheDir(): string {
17
+ return path.join(getConfigDir(), 'models', 'huggingface')
18
+ }
19
+
20
+ export async function loadLocalHfModels(): Promise<LocalHfModel[]> {
21
+ try {
22
+ const raw = await fs.readFile(getLocalHfModelsPath(), 'utf8')
23
+ const parsed = JSON.parse(raw) as unknown
24
+ if (!Array.isArray(parsed)) return []
25
+ return parsed.filter(isLocalHfModel)
26
+ } catch (err: unknown) {
27
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
28
+ return []
29
+ }
30
+ }
31
+
32
+ export async function saveLocalHfModels(models: LocalHfModel[]): Promise<void> {
33
+ await ensureConfigDir()
34
+ await atomicWriteText(getLocalHfModelsPath(), JSON.stringify(models, null, 2) + '\n')
35
+ }
36
+
37
+ export async function upsertLocalHfModel(model: LocalHfModel): Promise<void> {
38
+ const current = await loadLocalHfModels()
39
+ const next = [
40
+ model,
41
+ ...current.filter(existing => existing.id !== model.id),
42
+ ]
43
+ await saveLocalHfModels(next)
44
+ }
45
+
46
+ export async function findLocalHfModel(id: string): Promise<LocalHfModel | null> {
47
+ const models = await loadLocalHfModels()
48
+ return models.find(model => model.id === id) ?? null
49
+ }
50
+
51
+ export async function uninstallLocalHfModel(
52
+ id: string,
53
+ deps: UninstallDeps = {},
54
+ ): Promise<LocalHfModel | null> {
55
+ const models = await loadLocalHfModels()
56
+ const model = models.find(item => item.id === id)
57
+ if (!model) return null
58
+
59
+ const cacheRoot = path.resolve(getLocalHfCacheDir())
60
+ const modelPath = path.resolve(model.localPath)
61
+ const partialPath = path.resolve(`${model.localPath}.partial`)
62
+ if (!isPathInside(cacheRoot, modelPath) || !isPathInside(cacheRoot, partialPath)) {
63
+ throw new Error('Refusing to uninstall a local model outside EthAgent model cache')
64
+ }
65
+
66
+ const unlink = deps.unlink ?? ((target: string) => fs.unlink(target))
67
+ const rmdir = deps.rmdir ?? ((target: string) => fs.rmdir(target))
68
+ await unlinkIfPresent(modelPath, unlink)
69
+ await unlinkIfPresent(partialPath, unlink)
70
+ await cleanupEmptyParents(path.dirname(modelPath), cacheRoot, rmdir)
71
+
72
+ await saveLocalHfModels(models.filter(item => item.id !== id))
73
+ return model
74
+ }
75
+
76
+ export function localPathFor(repoId: string, revision: string, filename: string): string {
77
+ const repoParts = repoId.split('/').map(safePathPart)
78
+ const fileParts = filename.split('/').map(safePathPart)
79
+ return path.join(getLocalHfCacheDir(), ...repoParts, safePathPart(revision), ...fileParts)
80
+ }
81
+
82
+ async function unlinkIfPresent(
83
+ target: string,
84
+ unlink: (target: string) => Promise<void>,
85
+ ): Promise<void> {
86
+ try {
87
+ await unlink(target)
88
+ } catch (err: unknown) {
89
+ const code = (err as NodeJS.ErrnoException).code
90
+ if (code === 'ENOENT') return
91
+ if (code === 'EBUSY' || code === 'EPERM' || code === 'EACCES') {
92
+ throw new Error('That model file is currently in use. Stop the local runner and try uninstall again.')
93
+ }
94
+ throw err
95
+ }
96
+ }
97
+
98
+ async function cleanupEmptyParents(
99
+ startDir: string,
100
+ cacheRoot: string,
101
+ rmdir: (target: string) => Promise<void>,
102
+ ): Promise<void> {
103
+ let current = startDir
104
+ while (isPathInside(cacheRoot, current) && current !== cacheRoot) {
105
+ try {
106
+ await rmdir(current)
107
+ current = path.dirname(current)
108
+ } catch {
109
+ return
110
+ }
111
+ }
112
+ }
113
+
114
+ function isPathInside(root: string, target: string): boolean {
115
+ const relative = path.relative(root, target)
116
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
117
+ }
118
+
119
+ function safePathPart(value: string): string {
120
+ return value
121
+ .replace(/[^a-zA-Z0-9._-]+/g, '_')
122
+ .replace(/^_+|_+$/g, '')
123
+ .slice(0, 120) || 'unknown'
124
+ }
125
+
126
+ function isLocalHfModel(value: unknown): value is LocalHfModel {
127
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false
128
+ const item = value as Partial<LocalHfModel>
129
+ return item.provider === 'llamacpp'
130
+ && typeof item.id === 'string'
131
+ && typeof item.repoId === 'string'
132
+ && typeof item.filename === 'string'
133
+ && typeof item.localPath === 'string'
134
+ && typeof item.sizeBytes === 'number'
135
+ && item.status === 'ready'
136
+ }