ethagent 3.3.1 → 3.3.4

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 CHANGED
@@ -1,30 +1,16 @@
1
- <img src="https://raw.githubusercontent.com/baairon/ethagent/refs/heads/master/preview/image.png" alt="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.** The ERC-8004 token is the agent's durable identity. Use the ENS name as a readable handle, or the token ID plus chain as the permanent reference, to restore the same agent anywhere.
8
- - **Private.** Soul, memory, and skills are encrypted before they are pinned to IPFS. The wallet signature used to unlock them stays local and never submits a transaction, spends funds, or grants token approval.
9
- - **Public.** The agent URI points to a public metadata payload on IPFS that includes the Agent Card, so other agents can discover the agent's capabilities through ERC-8004.
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 20 or newer.
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 ERC-8004 identity, and picks a model.
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 Vault or unwrapping it back out.
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's continuity directory holds a small set of private files. They are encrypted before they ever reach IPFS.
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
- `SOUL.md`, `MEMORY.md`, and each `SKILL.md` are plain Markdown you edit through the Identity Hub under Continuity. Skill frontmatter (name, description, when_to_use, visibility, tags) tells the agent when to load it. The body stays local; `visibility: public` lists the name and description in the Agent Card.
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 **owner wallet** owns this agent's dedicated Vault; one or more **operator wallets** handle routine URI rotations through that vault. Use Advanced when routine saves should not require an owner signature.
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
- The vault is an immutable Foundry contract at `contracts/src/Vault.sol`. New vault deployments are dedicated per agent token and reject any other token.
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 Vault (see Custody Modes), not ENS. Any ENS text-record update requires an owner signature.
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 + network somewhere safe. ENS records can be cleared and rebuilt; the token ID is the durable handle.
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 Vault back to the owner wallet.
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. A marker like `[Image #1]` appears in the prompt; delete it to drop the attachment.
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
- - The transfer flow writes a snapshot pointer and stops; it never approves or moves the token.
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
- The published CLI is source-distributed: `bin/ethagent.js` launches `src/cli/main.tsx` through `tsx`. `npm run build` is therefore a validation build; it checks the shipped TypeScript without producing a separate `dist/` directory.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "3.3.1",
3
+ "version": "3.3.4",
4
4
  "description": "A privacy-first AI agent with a portable Ethereum identity",
5
5
  "type": "module",
6
6
  "main": "bin/ethagent.js",
@@ -257,12 +257,18 @@ export const FirstRun: React.FC<FirstRunProps> = ({ onComplete, onCancel }) => {
257
257
  </Box>
258
258
  )
259
259
 
260
- const renderRaw = (currentKind: FirstRunStepKind, body: React.ReactNode): React.ReactElement => (
260
+ const renderRaw = (
261
+ currentKind: FirstRunStepKind,
262
+ body: React.ReactNode,
263
+ bodyOwnsTimeline = false,
264
+ ): React.ReactElement => (
261
265
  <Box flexDirection="column" padding={1}>
262
266
  <Splash />
263
- <Box marginTop={1} marginBottom={1}>
264
- <FirstRunTimeline current={firstRunStageNumber(currentKind)} />
265
- </Box>
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') {
@@ -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)
@@ -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 { runHuggingFace, runIdentity, runMcp } from './slashCommandHandlers.js'
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: 'Ethereum identity · /identity [status|create|load|remove]',
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
- `created ${status.createdAt}`,
121
- `backend ${status.backend}`,
77
+ `updated ${status.createdAt}`,
78
+ `wallet ${status.backend}`,
122
79
  ]
123
- if (status.source) lines.push(`source ${status.source}`)
124
- if (status.agentId) lines.push(`token #${status.agentId}`)
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 title = mode === 'first-run' ? 'Set Up Agent Identity' : 'Identity Hub'
79
- const subtitle = mode === 'first-run'
80
- ? 'Create a portable agent or load one you already own.'
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)
@@ -39,8 +39,9 @@ body {
39
39
  font-family: var(--font-ui);
40
40
  color: var(--fg-1);
41
41
  background: #120f17;
42
- display: grid;
43
- place-items: center;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
44
45
  padding: clamp(18px, 3vw, 28px);
45
46
  }
46
47
 
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useState } from 'react'
2
2
  import { Text, Box } from 'ink'
3
- import { theme } from './theme.js'
3
+ import { theme, gradientColor } from './theme.js'
4
4
 
5
5
  const glyphs = {
6
6
  ethagent: `░░░░░░░╗░░░░░░░░╗░░╗ ░░╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗
@@ -39,10 +39,15 @@ const glyphs = {
39
39
 
40
40
  const Eyes = () => {
41
41
  const lines = glyphs.eyes.split('\n')
42
+ const maxWidth = Math.max(1, ...lines.map(l => l.length))
42
43
  return (
43
44
  <Box flexDirection="column">
44
45
  {lines.map((line, li) => (
45
- <Text key={li} color={theme.text}>{line}</Text>
46
+ <Text key={li}>
47
+ {[...line].map((ch, ci) => (
48
+ <Text key={ci} color={gradientColor(maxWidth <= 1 ? 0 : ci / (maxWidth - 1))}>{ch}</Text>
49
+ ))}
50
+ </Text>
46
51
  ))}
47
52
  </Box>
48
53
  )
@@ -74,10 +79,9 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
74
79
  <Box flexDirection="column" alignSelf="flex-start" padding={1}>
75
80
  <Eyes />
76
81
  <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
82
  {contextLine ? <Text color={theme.dim}>{contextLine}</Text> : null}
79
83
  {tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
80
- {updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
84
+ {updateNotice ? <Text color={theme.dim}>{updateNotice}</Text> : null}
81
85
  </Box>
82
86
  )
83
87
  }
@@ -85,8 +89,6 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
85
89
  const w = 69
86
90
  const logoLines = glyphs.ethagent.split('\n').map(line => line.padEnd(w, ' '))
87
91
 
88
- const topPad = Math.max(0, w - glyphs.tagline.length - 1)
89
-
90
92
  const bottomInline = contextLine ? ` ${truncateToFit(contextLine, w - 4)} ` : ''
91
93
  const bottomPad = Math.max(0, w - bottomInline.length - 1)
92
94
 
@@ -94,30 +96,30 @@ export const BrandSplash: React.FC<SplashProps> = ({ contextLine, tipLine, updat
94
96
  <Box flexDirection="column" alignSelf="flex-start" padding={1}>
95
97
  <Eyes />
96
98
  <Text>
97
- <Text color={theme.border}>{glyphs.frame.topLeft}</Text>
98
- <Text color={theme.dim}>{' privacy-first AI agent with a portable '}<Text color={theme.accentPeriwinkle}>Ethereum</Text>{' identity '}</Text>
99
- <Text color={theme.border}>{glyphs.frame.horizontal.repeat(topPad)}{glyphs.frame.topRight}</Text>
99
+ <Text color={theme.dim}>{glyphs.frame.topLeft}</Text>
100
+ <Text color={theme.dim}>{glyphs.tagline}</Text>
101
+ <Text color={theme.dim}>{glyphs.frame.horizontal.repeat(Math.max(0, w - glyphs.tagline.length - 1))}{glyphs.frame.topRight}</Text>
100
102
  </Text>
101
103
  {logoLines.map((line, i) => (
102
104
  <Box key={i}>
103
- <Text color={theme.border}>{glyphs.frame.side}</Text>
104
- <Text color={theme.border}>{line}</Text>
105
- <Text color={theme.border}>{glyphs.frame.side}</Text>
105
+ <Text color={theme.dim}>{glyphs.frame.side}</Text>
106
+ <Text color={theme.dim}>{line}</Text>
107
+ <Text color={theme.dim}>{glyphs.frame.side}</Text>
106
108
  </Box>
107
109
  ))}
108
110
  {bottomInline ? (
109
111
  <Text>
110
- <Text color={theme.border}>{glyphs.frame.bottomLeft}</Text>
111
- <Text color={theme.accentPeriwinkle}>{bottomInline}</Text>
112
- <Text color={theme.border}>{glyphs.frame.horizontal.repeat(bottomPad)}{glyphs.frame.bottomRight}</Text>
112
+ <Text color={theme.dim}>{glyphs.frame.bottomLeft}</Text>
113
+ <Text color={theme.dim}>{bottomInline}</Text>
114
+ <Text color={theme.dim}>{glyphs.frame.horizontal.repeat(bottomPad)}{glyphs.frame.bottomRight}</Text>
113
115
  </Text>
114
116
  ) : (
115
- <Text color={theme.border}>{glyphs.frame.bottomLeft.slice(0, 1) + glyphs.frame.horizontal.repeat(w) + glyphs.frame.bottomRight}</Text>
117
+ <Text color={theme.dim}>{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">
119
121
  {tipLine ? <Text color={theme.dim}>{tipLine}</Text> : null}
120
- {updateNotice ? <Text color={theme.accentPeriwinkle}>{updateNotice}</Text> : null}
122
+ {updateNotice ? <Text color={theme.dim}>{updateNotice}</Text> : null}
121
123
  </Box>
122
124
  ) : null}
123
125
  </Box>
package/src/ui/theme.ts CHANGED
@@ -31,7 +31,7 @@ export const theme = {
31
31
  codeTag: '#ffb3b3',
32
32
  codeAttribute: '#f2d087',
33
33
  border: '#555555',
34
- dim: '#777777',
34
+ dim: '#909090',
35
35
  text: '#f1f1f1',
36
36
  textSubtle: '#9b9b9b',
37
37
  } as const