ethagent 3.3.1 → 3.3.3
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 +19 -62
- package/package.json +1 -1
- package/src/app/FirstRun.tsx +13 -7
- package/src/chat/ChatScreen.tsx +16 -4
- package/src/chat/commands.ts +4 -10
- package/src/chat/slashCommandHandlers.ts +4 -47
- package/src/identity/hub/shared/components/MenuScreen.tsx +5 -3
- package/src/ui/BrandSplash.tsx +17 -15
- package/src/ui/theme.ts +1 -1
package/README.md
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
<img src="https://raw.githubusercontent.com/baairon/ethagent/
|
|
1
|
+
<img src="https://raw.githubusercontent.com/baairon/ethagent/master/preview/image.svg" alt="ethagent" width="640" />
|
|
2
2
|
|
|
3
3
|
A privacy-first AI agent with a portable Ethereum identity.
|
|
4
4
|
|
|
5
5
|
Switch providers or machines and the AI agent you customized stays behind. `ethagent` ties the agent to a wallet you own, so its soul, memory, and skills follow you across providers, machines, and models.
|
|
6
6
|
|
|
7
|
-
- **Portable.**
|
|
8
|
-
- **Private.** Soul, memory, and skills are encrypted
|
|
9
|
-
- **Public.**
|
|
10
|
-
|
|
11
|
-
<details>
|
|
12
|
-
<summary><strong>Glossary</strong> (click to expand)</summary>
|
|
13
|
-
|
|
14
|
-
| Term | Meaning |
|
|
15
|
-
| --- | --- |
|
|
16
|
-
| Owner Wallet | Holds and controls the ERC-8004 agent token. Signs custody changes and, in Simple custody, every URI rotation. |
|
|
17
|
-
| Operator Wallet | Additional wallet authorized to rotate the onchain URI on behalf of the owner. Used in Advanced custody. Never receives token approval. |
|
|
18
|
-
| Vault | Immutable per-agent custody contract used in Advanced custody. Holds at most one ERC-8004 token. |
|
|
19
|
-
| Snapshot | Encrypted bundle of SOUL.md, MEMORY.md, the skills/ tree, and session state. Pinned to IPFS; decrypts only against the owner wallet's signature. |
|
|
20
|
-
| Agent URI | IPFS URI stored in the ERC-8004 `tokenURI`. Resolves to a public metadata payload that references the Agent Card. |
|
|
21
|
-
| Agent Card | Public JSON describing the agent: name, description, capabilities, and skills. Pinned on IPFS and linked from the agent URI; other agents fetch it for discovery. |
|
|
22
|
-
|
|
23
|
-
</details>
|
|
7
|
+
- **Portable.** Restore the same agent on any machine from a wallet you own.
|
|
8
|
+
- **Private.** Soul, memory, and skills are encrypted on your machine before they sync. The wallet signature that unlocks them stays local and never spends funds.
|
|
9
|
+
- **Public.** Other agents discover the agent's capabilities through a public Agent Card.
|
|
24
10
|
|
|
25
11
|
## Install
|
|
26
12
|
|
|
27
|
-
ethagent runs on Node.js
|
|
13
|
+
ethagent runs on [Node.js](https://nodejs.org).
|
|
28
14
|
|
|
29
15
|
```bash
|
|
30
16
|
npm install -g ethagent
|
|
@@ -33,10 +19,7 @@ ethagent
|
|
|
33
19
|
|
|
34
20
|
## First Run
|
|
35
21
|
|
|
36
|
-
First run inspects the machine for local-model fit, sets up the
|
|
37
|
-
|
|
38
|
-
- **Models** include OpenAI, Anthropic, Gemini, or a local GGUF served through a llama.cpp-compatible endpoint.
|
|
39
|
-
- **Identity** can be a fresh ERC-8004 token created with a browser wallet, an existing token already owned by your wallet, or set up later from the Identity Hub.
|
|
22
|
+
First run inspects the machine for local-model fit, sets up the agent's onchain identity, and picks a model. Identity can be a fresh ERC-8004 token created with a browser wallet, an existing token in your wallet, or skipped and set up later from the Identity Hub.
|
|
40
23
|
|
|
41
24
|
Once running:
|
|
42
25
|
|
|
@@ -50,18 +33,14 @@ The Identity Hub manages everything portable about the agent:
|
|
|
50
33
|
|
|
51
34
|
- **Public Profile** edits name, description, and icon: what other agents see in the Agent Card.
|
|
52
35
|
- **ENS Name** links the agent to a subdomain under a parent name the owner wallet controls.
|
|
53
|
-
- **Custody Mode** switches between Simple and Advanced by depositing the token into its
|
|
36
|
+
- **Custody Mode** switches between Simple and Advanced by depositing the token into its operator delegation vault or unwrapping it back out.
|
|
54
37
|
- **Prepare Transfer** stages a dual-wallet snapshot so the receiver can restore the agent after the token moves externally.
|
|
55
38
|
- **Refetch Latest** pulls the most recent published snapshot back to local files.
|
|
56
39
|
- **Switch Agent** accepts an ENS name or an ERC-8004 token ID on any supported chain, and loads any agent owned by or linked to the connected wallet.
|
|
57
40
|
|
|
58
|
-
The menu surfaces drift automatically. Token ownership, vault state, ENS record alignment, and pending URI rotations are checked against the live chain when the menu opens.
|
|
59
|
-
|
|
60
|
-
Every agent has a continuity directory at `~/.ethagent/continuity`.
|
|
61
|
-
|
|
62
41
|
## Continuity
|
|
63
42
|
|
|
64
|
-
Each agent
|
|
43
|
+
Each agent has a continuity directory at `~/.ethagent/continuity` holding a small set of private files. They are encrypted before they ever reach IPFS.
|
|
65
44
|
|
|
66
45
|
| File | Visibility | Purpose |
|
|
67
46
|
| --- | --- | --- |
|
|
@@ -69,7 +48,7 @@ Each agent's continuity directory holds a small set of private files. They are e
|
|
|
69
48
|
| `MEMORY.md` | Private | Durable preferences, project context, decisions, and operating notes. |
|
|
70
49
|
| `skills/` | Private | Skill folders. The SKILL.md body never leaves your machine. The visibility flag only controls whether the skill's name and description get listed in the Agent Card. New skills default to public. |
|
|
71
50
|
|
|
72
|
-
|
|
51
|
+
Skill frontmatter (name, description, when_to_use, visibility, tags) tells the agent when to load each skill.
|
|
73
52
|
|
|
74
53
|
- **Save Snapshot Now** encrypts the private files, pins them to IPFS, and rotates the onchain pointer to the new CID.
|
|
75
54
|
- **Refetch Latest** reads the pointer back, signs the decrypt challenge with your wallet, and overwrites local files from the snapshot.
|
|
@@ -81,31 +60,27 @@ Custody comes in two modes. Switch between them anytime from **Custody Mode**.
|
|
|
81
60
|
|
|
82
61
|
**Simple** relies on one wallet to own the token, sign every snapshot save, and rotate the onchain URI directly. Use Simple when one wallet operates the agent.
|
|
83
62
|
|
|
84
|
-
**Advanced** splits an owner wallet from one or more operator wallets. The
|
|
85
|
-
|
|
86
|
-
Granting an operator wallet ERC-721 approval would let it rotate the URI, but that same approval also lets it transfer the token away. The Vault holds the token instead and exposes only a URI-rotation lane for that agent. Operators never receive token approval or transfer rights, cannot touch ENS, and cannot grant rights to other operators. The owner still signs to authorize or revoke operators for the agent, withdraw the token, or transfer the agent.
|
|
63
|
+
**Advanced** splits an owner wallet from one or more operator wallets. The owner wallet owns this agent's operator delegation vault; one or more operator wallets handle routine URI rotations through that vault. Use Advanced when routine saves should not require an owner signature.
|
|
87
64
|
|
|
88
|
-
|
|
65
|
+
Granting an operator wallet ERC-721 approval would let it rotate the URI, but that same approval also lets it transfer the token away. The vault holds the token instead and exposes only a URI-rotation lane for that agent. Operators never receive token approval or transfer rights, cannot touch ENS, and cannot grant rights to other operators. The owner still signs to authorize or revoke operators for the agent, withdraw the token, or transfer the agent.
|
|
89
66
|
|
|
90
67
|
## ENS Names
|
|
91
68
|
|
|
92
69
|
Subdomains live under a parent name you control, never on root `.eth` names directly. You keep `you.eth`; the agent gets `agent.you.eth`. The split makes the boundary explicit: one address speaks for the human, the other speaks for the agent.
|
|
93
70
|
|
|
94
|
-
ENS records stay owner-controlled in both custody modes. Operator wallets in Advanced custody rotate the ERC-8004 token URI through the
|
|
71
|
+
ENS records stay owner-controlled in both custody modes. Operator wallets in Advanced custody rotate the ERC-8004 token URI through the vault (see Custody Modes), not ENS. Any ENS text-record update requires an owner signature.
|
|
95
72
|
|
|
96
|
-
Save the token ID
|
|
73
|
+
Save the token ID and network somewhere safe. ENS records can be cleared and rebuilt; the token ID is the durable handle.
|
|
97
74
|
|
|
98
75
|
## Token Transfers
|
|
99
76
|
|
|
100
|
-
**Prepare Token Transfer** runs before any ERC-8004 token transfer, and only when the token sits directly in your wallet. An agent in Advanced custody has to switch to Simple first from Custody Mode, which unwraps the token from its
|
|
77
|
+
**Prepare Token Transfer** runs before any ERC-8004 token transfer, and only when the token sits directly in your wallet. An agent in Advanced custody has to switch to Simple first from Custody Mode, which unwraps the token from its vault back to the owner wallet.
|
|
101
78
|
|
|
102
79
|
- Sender signs snapshot access, receiver signs restore access.
|
|
103
80
|
- Sender publishes the snapshot pointer to the agent URI.
|
|
104
81
|
- The actual transfer happens externally afterwards, in whichever wallet UI you prefer.
|
|
105
82
|
- Once the token has moved, the receiver opens **Switch Agent** with the receiving wallet and restores the same agent from the published snapshot.
|
|
106
83
|
|
|
107
|
-
The token transfer flow prepares decrypt access and agent URI pointers only. It does not initiate the transfer and does not request approval over the token.
|
|
108
|
-
|
|
109
84
|
## Models
|
|
110
85
|
|
|
111
86
|
ethagent works with OpenAI, Anthropic, Gemini, and local GGUF models served through a llama.cpp-compatible endpoint.
|
|
@@ -116,14 +91,7 @@ ethagent works with OpenAI, Anthropic, Gemini, and local GGUF models served thro
|
|
|
116
91
|
|
|
117
92
|
### Image Input
|
|
118
93
|
|
|
119
|
-
Press `Alt+V` to paste an image from the clipboard.
|
|
120
|
-
|
|
121
|
-
Vision support is available on:
|
|
122
|
-
|
|
123
|
-
- **OpenAI** (Chat Completions and Responses API): `gpt-4o`, `gpt-4.1`, `gpt-4-turbo`, `gpt-4-vision`, `gpt-5`, `o1`, `o3`, `o4`, `chatgpt-4`.
|
|
124
|
-
- **Anthropic**: `claude-3`, `claude-sonnet-4`, `claude-opus-4`, `claude-haiku-4`.
|
|
125
|
-
- **Gemini**: `gemini-1.5`, `gemini-2.0`, `gemini-2.5`.
|
|
126
|
-
- **Local llama.cpp**: vision works when both the main GGUF and a `mmproj-*.gguf` projector are loaded. The picker recommends the bundle during install; if you skipped, open `Alt+P` and any installed model with a vision encoder available shows an `Add Vision Encoder` row directly beneath it.
|
|
94
|
+
Press `Alt+V` to paste an image from the clipboard. Vision works on current OpenAI, Anthropic, and Gemini models, and on local GGUF models loaded with an `mmproj-*.gguf` projector.
|
|
127
95
|
|
|
128
96
|
## Tools and Sessions
|
|
129
97
|
|
|
@@ -138,8 +106,7 @@ Vision support is available on:
|
|
|
138
106
|
- **Public:** token ownership, the agent URI, the Agent Card it references, and IPFS CIDs.
|
|
139
107
|
- **Private:** plaintext `SOUL.md`, plaintext `MEMORY.md`, the local skills/ tree, sessions, prompt history, API keys, local permissions, and the wallet signatures used for decryption.
|
|
140
108
|
- Snapshots use a wallet signature as unlock material. The signature does not submit a transaction, spend funds, or grant token approval.
|
|
141
|
-
-
|
|
142
|
-
- `ethagent reset` deletes local ethagent data from the current machine while preserving installed local model assets. It does not burn or transfer tokens, remove public IPFS content, or mutate the onchain agent URI. Run **Save Snapshot Now** before resetting if local edits should become the recoverable state.
|
|
109
|
+
- `ethagent reset` clears local ethagent data from the current machine while preserving installed local model assets; it does not touch onchain state or pinned IPFS content. Run **Save Snapshot Now** first if local edits should become the recoverable state.
|
|
143
110
|
|
|
144
111
|
## Architecture
|
|
145
112
|
|
|
@@ -152,8 +119,6 @@ Vision support is available on:
|
|
|
152
119
|
| Discovery | The agent URI and the Agent Card it points to. |
|
|
153
120
|
| Recovery | Refetch the current agent URI, decrypt the latest snapshot, and restore local files. |
|
|
154
121
|
|
|
155
|
-
The ERC-8004 token is the durable handle. The machine, model, and local session all change around it.
|
|
156
|
-
|
|
157
122
|
## Development
|
|
158
123
|
|
|
159
124
|
```bash
|
|
@@ -162,9 +127,7 @@ cd ethagent && npm install
|
|
|
162
127
|
npm start
|
|
163
128
|
```
|
|
164
129
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Repository structure and refactoring rules are documented in `ARCHITECTURE.md` and `CONTRIBUTING.md`. Stable facades keep public import paths intact while focused sibling modules hold private implementation details.
|
|
130
|
+
`npm run build` is a type-check pass; the published CLI runs the shipped TypeScript directly through `tsx`.
|
|
168
131
|
|
|
169
132
|
| Command | What it does |
|
|
170
133
|
| --- | --- |
|
|
@@ -172,16 +135,10 @@ Repository structure and refactoring rules are documented in `ARCHITECTURE.md` a
|
|
|
172
135
|
| `npm run build` | Validate the shipped TypeScript source package. |
|
|
173
136
|
| `npm test` | Test suite. |
|
|
174
137
|
| `npm run typecheck` | Run the same TypeScript check directly. |
|
|
175
|
-
| `npm run contracts:test` | Foundry tests. |
|
|
176
|
-
|
|
177
|
-
Foundry is only needed for `contracts/` changes.
|
|
138
|
+
| `npm run contracts:test` | Foundry tests (only needed for `contracts/` changes). |
|
|
178
139
|
|
|
179
140
|
## Contributing
|
|
180
141
|
|
|
181
|
-
Contributions are welcome. For anything beyond a typo, open an issue first at [github.com/baairon/ethagent/issues](https://github.com/baairon/ethagent/issues) so the scope and approach can be agreed before code is written.
|
|
182
|
-
|
|
183
|
-
Each PR should cover one logical change, include a clear description, and list the commands you ran for testing. Match project conventions. Do not bundle unrelated cleanup, broad refactors, formatting churn, or changes that have not been reviewed as part of the issue.
|
|
184
|
-
|
|
185
|
-
Contributions are released under the MIT license.
|
|
142
|
+
Contributions are welcome. For anything beyond a typo, open an issue first at [github.com/baairon/ethagent/issues](https://github.com/baairon/ethagent/issues) so the scope and approach can be agreed before code is written. Each PR should cover one logical change, include a clear description, and list the commands you ran for testing. Contributions are released under the MIT license.
|
|
186
143
|
|
|
187
144
|
[npm](https://www.npmjs.com/package/ethagent) | [GitHub](https://github.com/baairon/ethagent) | [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) | [soul.md](https://soul.md/)
|
package/package.json
CHANGED
package/src/app/FirstRun.tsx
CHANGED
|
@@ -244,7 +244,7 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
|
|
|
244
244
|
footer?: string,
|
|
245
245
|
): React.ReactElement => (
|
|
246
246
|
<Box flexDirection="column" padding={1}>
|
|
247
|
-
<Splash />
|
|
247
|
+
<Splash showTagline />
|
|
248
248
|
<Box marginTop={1}>
|
|
249
249
|
<Surface
|
|
250
250
|
title={title}
|
|
@@ -257,12 +257,18 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
|
|
|
257
257
|
</Box>
|
|
258
258
|
)
|
|
259
259
|
|
|
260
|
-
const renderRaw = (
|
|
260
|
+
const renderRaw = (
|
|
261
|
+
currentKind: FirstRunStepKind,
|
|
262
|
+
body: React.ReactNode,
|
|
263
|
+
bodyOwnsTimeline = false,
|
|
264
|
+
): React.ReactElement => (
|
|
261
265
|
<Box flexDirection="column" padding={1}>
|
|
262
|
-
<Splash />
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
+
<Splash showTagline />
|
|
267
|
+
{bodyOwnsTimeline ? null : (
|
|
268
|
+
<Box marginTop={1} marginBottom={1}>
|
|
269
|
+
<FirstRunTimeline current={firstRunStageNumber(currentKind)} />
|
|
270
|
+
</Box>
|
|
271
|
+
)}
|
|
266
272
|
{body}
|
|
267
273
|
</Box>
|
|
268
274
|
)
|
|
@@ -306,7 +312,7 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
|
|
|
306
312
|
setStep({ kind: 'identity-start-saving', spec: step.spec, result })
|
|
307
313
|
}}
|
|
308
314
|
/>
|
|
309
|
-
))
|
|
315
|
+
), true)
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
if (step.kind === 'identity-start-saving') {
|
package/src/chat/ChatScreen.tsx
CHANGED
|
@@ -998,10 +998,6 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
998
998
|
void appendHistory(value)
|
|
999
999
|
|
|
1000
1000
|
if (streaming || pullInFlight || compactionUiRef.current) {
|
|
1001
|
-
if (parseSlash(value)) {
|
|
1002
|
-
pushNote('Slash commands cannot be queued. Wait for the current task to finish.', 'dim')
|
|
1003
|
-
return
|
|
1004
|
-
}
|
|
1005
1001
|
setQueuedInputs(prev => [...prev, value])
|
|
1006
1002
|
return
|
|
1007
1003
|
}
|
|
@@ -1538,6 +1534,22 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
|
|
|
1538
1534
|
setQueuedInputs(prev => prev.slice(1))
|
|
1539
1535
|
void (async () => {
|
|
1540
1536
|
if (!next) return
|
|
1537
|
+
if (parseSlash(next)) {
|
|
1538
|
+
const ctx = buildSlashContext()
|
|
1539
|
+
const result = await dispatchSlash(next, ctx)
|
|
1540
|
+
if (result && result.kind === 'note') {
|
|
1541
|
+
pushNote(result.text, result.variant ?? 'info')
|
|
1542
|
+
}
|
|
1543
|
+
if (result && result.kind === 'submit') {
|
|
1544
|
+
const projected = projectedUsageForInput(result.text)
|
|
1545
|
+
if (shouldConfirmContextUsage(projected, CONTEXT_CONFIRM_PERCENT)) {
|
|
1546
|
+
showContextLimitForPrompt(result.text)
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
await runStream(result.text)
|
|
1550
|
+
}
|
|
1551
|
+
return
|
|
1552
|
+
}
|
|
1541
1553
|
const projected = projectedUsageForInput(next)
|
|
1542
1554
|
if (shouldConfirmContextUsage(projected, CONTEXT_CONFIRM_PERCENT)) {
|
|
1543
1555
|
showContextLimitForPrompt(next)
|
package/src/chat/commands.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { ContextUsage } from '../runtime/compaction.js'
|
|
|
18
18
|
import { formatModelDisplayName } from '../models/modelDisplay.js'
|
|
19
19
|
import { providerDisplayName } from '../models/modelPickerOptions.js'
|
|
20
20
|
import type { McpManager } from '../mcp/manager.js'
|
|
21
|
-
import {
|
|
21
|
+
import { runIdentity, runMcp } from './slashCommandHandlers.js'
|
|
22
22
|
import { renderStatus } from './slashCommandViews.js'
|
|
23
23
|
|
|
24
24
|
export type IdentityRequestAction =
|
|
@@ -112,7 +112,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
name: 'status',
|
|
115
|
-
summary: 'provider, model, session id, turns, context, elapsed',
|
|
115
|
+
summary: 'provider, model, session id, turns, context, state, cwd, elapsed',
|
|
116
116
|
run: (_args, ctx) => ({ kind: 'note', text: renderStatus(ctx) }),
|
|
117
117
|
},
|
|
118
118
|
{
|
|
@@ -213,12 +213,6 @@ const COMMANDS: CommandSpec[] = [
|
|
|
213
213
|
return { kind: 'note', text: `Now using ${providerDisplayName(next.provider)} · ${formatModelDisplayName(next.provider, name, { maxLength: 64 })}.` }
|
|
214
214
|
},
|
|
215
215
|
},
|
|
216
|
-
{
|
|
217
|
-
name: 'hf',
|
|
218
|
-
enterBehavior: 'fill',
|
|
219
|
-
summary: 'local model files · /hf [installed|download <link>]',
|
|
220
|
-
run: async (args, ctx) => runHuggingFace(args, ctx),
|
|
221
|
-
},
|
|
222
216
|
{
|
|
223
217
|
name: 'resume',
|
|
224
218
|
summary: 'reopen a prior session',
|
|
@@ -271,7 +265,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
271
265
|
},
|
|
272
266
|
{
|
|
273
267
|
name: 'copy',
|
|
274
|
-
summary: 'copy an assistant reply to the clipboard · /copy [n]',
|
|
268
|
+
summary: 'copy an assistant reply to the clipboard · /copy [n] (1 = latest)',
|
|
275
269
|
run: async (args, ctx) => {
|
|
276
270
|
const assistant = ctx.assistantTurns()
|
|
277
271
|
if (assistant.length === 0) {
|
|
@@ -334,7 +328,7 @@ const COMMANDS: CommandSpec[] = [
|
|
|
334
328
|
{
|
|
335
329
|
name: 'identity',
|
|
336
330
|
enterBehavior: 'fill',
|
|
337
|
-
summary: '
|
|
331
|
+
summary: 'agent identity · /identity [status|create|load|remove confirm]',
|
|
338
332
|
run: async (args, ctx) => runIdentity(args, ctx),
|
|
339
333
|
},
|
|
340
334
|
{
|
|
@@ -1,48 +1,5 @@
|
|
|
1
1
|
import { clearIdentity, getIdentityStatus } from '../storage/identity.js'
|
|
2
|
-
import { formatModelDisplayName } from '../models/modelDisplay.js'
|
|
3
|
-
import { loadLocalHfModels } from '../models/huggingface.js'
|
|
4
2
|
import type { SlashContext, SlashResult } from './commands.js'
|
|
5
|
-
import { formatBytes } from './slashCommandViews.js'
|
|
6
|
-
|
|
7
|
-
export async function runHuggingFace(args: string, ctx: SlashContext): Promise<SlashResult> {
|
|
8
|
-
const tokens = args.trim().split(/\s+/).filter(Boolean)
|
|
9
|
-
const sub = tokens[0]?.toLowerCase() ?? ''
|
|
10
|
-
|
|
11
|
-
if (!sub || sub === 'installed') {
|
|
12
|
-
const installed = await loadLocalHfModels()
|
|
13
|
-
if (installed.length === 0) {
|
|
14
|
-
return {
|
|
15
|
-
kind: 'note',
|
|
16
|
-
variant: 'dim',
|
|
17
|
-
text: 'No local model files downloaded. Press Alt+P and choose "Add Local Model File".',
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const lines = installed.map(model => {
|
|
21
|
-
const marker = model.id === ctx.config.model && ctx.config.provider === 'llamacpp' ? '*' : ' '
|
|
22
|
-
const displayName = formatModelDisplayName('llamacpp', model.id, { displayName: model.displayName, maxLength: 64 })
|
|
23
|
-
return `${marker} ${displayName} ${formatBytes(model.sizeBytes)} ${model.risk}`
|
|
24
|
-
})
|
|
25
|
-
return { kind: 'note', text: ['installed Hugging Face models:', ...lines].join('\n') }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (sub === 'download' || sub === 'model') {
|
|
29
|
-
const link = tokens.slice(1).join(' ')
|
|
30
|
-
ctx.onModelPickerRequest()
|
|
31
|
-
return {
|
|
32
|
-
kind: 'note',
|
|
33
|
-
variant: 'dim',
|
|
34
|
-
text: link
|
|
35
|
-
? `Alt+P opened. Choose "Add Local Model File" and paste: ${link}`
|
|
36
|
-
: 'Alt+P opened. Choose "Add Local Model File" and paste the model URL or repo ID.',
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
kind: 'note',
|
|
42
|
-
variant: 'error',
|
|
43
|
-
text: 'usage: /hf [installed|download <huggingface.co link or repo id>]',
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
3
|
|
|
47
4
|
export async function runMcp(args: string, ctx: SlashContext): Promise<SlashResult> {
|
|
48
5
|
if (!ctx.mcp) {
|
|
@@ -117,11 +74,11 @@ export async function runIdentity(args: string, ctx: SlashContext): Promise<Slas
|
|
|
117
74
|
}
|
|
118
75
|
const lines = [
|
|
119
76
|
`address ${status.address}`,
|
|
120
|
-
`
|
|
121
|
-
`
|
|
77
|
+
`updated ${status.createdAt}`,
|
|
78
|
+
`wallet ${status.backend}`,
|
|
122
79
|
]
|
|
123
|
-
if (status.source) lines.push(`
|
|
124
|
-
if (status.agentId) lines.push(`
|
|
80
|
+
if (status.source) lines.push(`registry ${status.source}`)
|
|
81
|
+
if (status.agentId) lines.push(`agent #${status.agentId}`)
|
|
125
82
|
return { kind: 'note', text: lines.join('\n') }
|
|
126
83
|
}
|
|
127
84
|
|
|
@@ -3,6 +3,7 @@ import { Box, Text } from 'ink'
|
|
|
3
3
|
import { Surface } from '../../../../ui/Surface.js'
|
|
4
4
|
import { Select, type SelectOption } from '../../../../ui/Select.js'
|
|
5
5
|
import { theme } from '../../../../ui/theme.js'
|
|
6
|
+
import { FirstRunTimeline } from '../../../../app/FirstRunTimeline.js'
|
|
6
7
|
import type { EthagentConfig, EthagentIdentity } from '../../../../storage/config.js'
|
|
7
8
|
import type { ContinuityWorkingTreeStatus } from '../../../continuity/storage.js'
|
|
8
9
|
import { identityPerspective, readCustodyMode } from '../../custody/state.js'
|
|
@@ -75,9 +76,10 @@ export const MenuScreen: React.FC<MenuScreenProps> = ({
|
|
|
75
76
|
onSkip,
|
|
76
77
|
onCancel,
|
|
77
78
|
}) => {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
79
|
+
const isFirstRun = mode === 'first-run'
|
|
80
|
+
const title = isFirstRun ? 'Set Up Agent Identity' : 'Identity Hub'
|
|
81
|
+
const subtitle: React.ReactNode = isFirstRun
|
|
82
|
+
? <FirstRunTimeline current={2} />
|
|
81
83
|
: 'Manage agent identity, custody, encrypted continuity, and recovery.'
|
|
82
84
|
|
|
83
85
|
const canRefetch = Boolean(canRebackup && identity?.backup?.cid)
|
package/src/ui/BrandSplash.tsx
CHANGED
|
@@ -53,9 +53,10 @@ type SplashProps = {
|
|
|
53
53
|
tipLine?: string
|
|
54
54
|
updateNotice?: string | null
|
|
55
55
|
compact?: boolean
|
|
56
|
+
showTagline?: boolean
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updateNotice, compact }) => {
|
|
59
|
+
export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updateNotice, compact, showTagline }) => {
|
|
59
60
|
const [width, setWidth] = useState<number>(() => process.stdout.columns ?? 80)
|
|
60
61
|
|
|
61
62
|
useEffect(() => {
|
|
@@ -74,7 +75,6 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
|
|
|
74
75
|
<Box flexDirection="column" alignSelf="flex-start" padding={1}>
|
|
75
76
|
<Eyes />
|
|
76
77
|
<Text bold color={theme.accentWhite}>ethagent</Text>
|
|
77
|
-
<Text color={theme.dim}>privacy-first AI agent with a portable <Text color={theme.accentPeriwinkle}>Ethereum</Text> identity</Text>
|
|
78
78
|
{contextLine ? <Text color={theme.dim}>{contextLine}</Text> : null}
|
|
79
79
|
{tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
|
|
80
80
|
{updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
|
|
@@ -85,34 +85,36 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
|
|
|
85
85
|
const w = 69
|
|
86
86
|
const logoLines = glyphs.ethagent.split('\n').map(line => line.padEnd(w, ' '))
|
|
87
87
|
|
|
88
|
-
const topPad = Math.max(0, w - glyphs.tagline.length - 1)
|
|
89
|
-
|
|
90
88
|
const bottomInline = contextLine ? ` ${truncateToFit(contextLine, w - 4)} ` : ''
|
|
91
89
|
const bottomPad = Math.max(0, w - bottomInline.length - 1)
|
|
92
90
|
|
|
93
91
|
return (
|
|
94
92
|
<Box flexDirection="column" alignSelf="flex-start" padding={1}>
|
|
95
93
|
<Eyes />
|
|
96
|
-
|
|
97
|
-
<Text
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
{showTagline ? (
|
|
95
|
+
<Text>
|
|
96
|
+
<Text color={theme.accentWhite}>{glyphs.frame.topLeft}</Text>
|
|
97
|
+
<Text color={theme.accentPeriwinkle}>{glyphs.tagline}</Text>
|
|
98
|
+
<Text color={theme.accentWhite}>{glyphs.frame.horizontal.repeat(Math.max(0, w - glyphs.tagline.length - 1))}{glyphs.frame.topRight}</Text>
|
|
99
|
+
</Text>
|
|
100
|
+
) : (
|
|
101
|
+
<Text color={theme.accentWhite}>{glyphs.frame.topLeft.slice(0, 1) + glyphs.frame.horizontal.repeat(w) + glyphs.frame.topRight}</Text>
|
|
102
|
+
)}
|
|
101
103
|
{logoLines.map((line, i) => (
|
|
102
104
|
<Box key={i}>
|
|
103
|
-
<Text color={theme.
|
|
104
|
-
<Text color={theme.
|
|
105
|
-
<Text color={theme.
|
|
105
|
+
<Text color={theme.accentWhite}>{glyphs.frame.side}</Text>
|
|
106
|
+
<Text color={theme.accentWhite}>{line}</Text>
|
|
107
|
+
<Text color={theme.accentWhite}>{glyphs.frame.side}</Text>
|
|
106
108
|
</Box>
|
|
107
109
|
))}
|
|
108
110
|
{bottomInline ? (
|
|
109
111
|
<Text>
|
|
110
|
-
<Text color={theme.
|
|
112
|
+
<Text color={theme.accentWhite}>{glyphs.frame.bottomLeft}</Text>
|
|
111
113
|
<Text color={theme.accentPeriwinkle}>{bottomInline}</Text>
|
|
112
|
-
<Text color={theme.
|
|
114
|
+
<Text color={theme.accentWhite}>{glyphs.frame.horizontal.repeat(bottomPad)}{glyphs.frame.bottomRight}</Text>
|
|
113
115
|
</Text>
|
|
114
116
|
) : (
|
|
115
|
-
<Text color={theme.
|
|
117
|
+
<Text color={theme.accentWhite}>{glyphs.frame.bottomLeft.slice(0, 1) + glyphs.frame.horizontal.repeat(w) + glyphs.frame.bottomRight}</Text>
|
|
116
118
|
)}
|
|
117
119
|
{tipLine || updateNotice ? (
|
|
118
120
|
<Box marginTop={1} flexDirection="column">
|