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.
- package/README.md +6 -1
- package/package.json +3 -1
- package/src/app/FirstRun.tsx +1 -24
- package/src/app/firstRunConfig.ts +26 -0
- package/src/auth/openaiOAuth/landingPage.ts +2 -11
- package/src/chat/ChatScreen.tsx +32 -117
- package/src/chat/MessageList.tsx +18 -260
- package/src/chat/chatEnvironment.ts +16 -0
- package/src/chat/chatTurnContext.ts +50 -0
- package/src/chat/chatTurnOrchestrator.ts +5 -112
- package/src/chat/chatTurnRows.ts +64 -0
- package/src/chat/commands.ts +3 -178
- package/src/chat/continuityEditReview.ts +42 -0
- package/src/chat/input/ChatInput.tsx +10 -144
- package/src/chat/input/chatInputHelpers.ts +62 -0
- package/src/chat/input/inputRendering.tsx +93 -0
- package/src/chat/messageMarkdown.ts +220 -0
- package/src/chat/messageRows.ts +43 -0
- package/src/chat/planImplementation.ts +62 -0
- package/src/chat/slashCommandHandlers.ts +165 -0
- package/src/chat/slashCommandViews.ts +120 -0
- package/src/cli/main.tsx +7 -0
- package/src/identity/continuity/challenges.ts +123 -0
- package/src/identity/continuity/envelope.ts +49 -1484
- package/src/identity/continuity/envelopeCreate.ts +322 -0
- package/src/identity/continuity/envelopeCrypto.ts +182 -0
- package/src/identity/continuity/envelopeParse.ts +441 -0
- package/src/identity/continuity/envelopeTypes.ts +204 -0
- package/src/identity/continuity/envelopeVersion.ts +1 -0
- package/src/identity/continuity/payloadNormalization.ts +183 -0
- package/src/identity/continuity/publicSkills.ts +5 -5
- package/src/identity/continuity/skills/loadSkills.ts +12 -69
- package/src/identity/continuity/skills/skillPaths.ts +76 -0
- package/src/identity/continuity/skillsNormalization.ts +119 -0
- package/src/identity/continuity/snapshotToken.ts +28 -0
- package/src/identity/hub/continuity/completion.ts +67 -0
- package/src/identity/hub/continuity/effects.ts +5 -62
- package/src/identity/hub/profile/effects.ts +6 -170
- package/src/identity/hub/profile/operatorSave.ts +202 -0
- package/src/identity/registry/erc8004/metadata.ts +31 -23
- package/src/identity/wallet/browserWallet/html.ts +1 -57
- package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
- package/src/identity/wallet/page/controller.ts +1 -1
- package/src/identity/wallet/page/errorView.ts +122 -0
- package/src/identity/wallet/page/view.ts +3 -114
- package/src/mcp/manager.ts +8 -66
- package/src/mcp/managerHelpers.ts +70 -0
- package/src/models/ModelPicker.tsx +69 -889
- package/src/models/huggingface.ts +20 -137
- package/src/models/huggingfaceStorage.ts +136 -0
- package/src/models/llamacpp.ts +37 -303
- package/src/models/llamacppCommands.ts +44 -0
- package/src/models/llamacppConfig.ts +34 -0
- package/src/models/llamacppDiscovery.ts +176 -0
- package/src/models/llamacppOutput.ts +65 -0
- package/src/models/modelPickerCatalogFlow.ts +56 -0
- package/src/models/modelPickerCredentials.ts +166 -0
- package/src/models/modelPickerData.ts +41 -0
- package/src/models/modelPickerDisplay.tsx +132 -0
- package/src/models/modelPickerHfFlow.ts +192 -0
- package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
- package/src/models/modelPickerTypes.ts +69 -0
- package/src/models/modelPickerUninstallFlow.ts +48 -0
- package/src/models/modelPickerViewHelpers.ts +174 -0
- package/src/providers/openai-chat.ts +5 -124
- package/src/providers/openaiChatWire.ts +124 -0
- package/src/runtime/providerTurn.ts +38 -0
- package/src/runtime/textToolParser.ts +161 -0
- package/src/runtime/toolIntent.ts +1 -1
- package/src/runtime/turn.ts +43 -499
- package/src/runtime/turnNudges.ts +223 -0
- package/src/runtime/turnTypes.ts +86 -0
- 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 {
|
|
5
|
-
|
|
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
|
+
}
|