ethagent 4.0.0 → 4.1.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +14 -9
- package/package.json +1 -3
- package/src/cli/hookIo.ts +0 -5
- package/src/cli/memoryGuard.ts +1 -8
- package/src/cli/sessionStart.ts +1 -7
- package/src/cli/syncAdapters/claude-code.ts +0 -6
- package/src/identity/continuity/storage/scaffold.ts +0 -1
- package/src/identity/manager/shared/components/Wordmark.tsx +51 -21
- package/src/identity/manager/shared/reconciliation/agentReconciliation/run.ts +0 -1
- package/src/identity/wallet/page/resize.ts +1 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ethagent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Portable Ethereum identity for your AI agent. Its soul, memory, and skills live onchain via ERC-8004 + IPFS and snap back into any session.",
|
|
5
5
|
"author": { "name": "bairon.dev" },
|
|
6
6
|
"homepage": "https://github.com/baairon/ethagent",
|
package/README.md
CHANGED
|
@@ -22,50 +22,55 @@ You'll need an Ethereum wallet, the same wallet that holds and unlocks your agen
|
|
|
22
22
|
|
|
23
23
|
A guided menu does the rest: create its token, give it a name, and write who it is. Your wallet signs each step.
|
|
24
24
|
|
|
25
|
-
**2. Add it to Claude Code.** Paste these in
|
|
25
|
+
**2. Add it to Claude Code.** Paste these in one at a time:
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
/plugin marketplace add baairon/ethagent
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
then:
|
|
32
|
+
|
|
33
|
+
```
|
|
29
34
|
/plugin install ethagent@ethagent
|
|
30
35
|
```
|
|
31
36
|
|
|
32
37
|
**3. Talk to your agent.** From here on it shows up in every session and gets to know you as you go.
|
|
33
38
|
|
|
34
|
-
That's the whole setup.
|
|
39
|
+
That's the whole setup. You'll only open `ethagent` again to hand-edit your agent or save a backup.
|
|
35
40
|
|
|
36
|
-
##
|
|
41
|
+
## 📦 Soul, memory, skills
|
|
37
42
|
|
|
38
43
|
- **Soul** (`SOUL.md`): who it is, your standards, your voice, the way you work.
|
|
39
44
|
- **Memory** (`MEMORY.md`): what it has learned about you, your preferences, and your projects, so context survives the move to a new machine.
|
|
40
45
|
- **Skills:** the commands, tools, and prompts you teach it. Public by default, so other agents can discover them; mark one private to keep it off your public Agent Card (the profile your token publishes).
|
|
41
46
|
|
|
42
|
-
You grow these mostly by talking: with the plugin on, your agent updates its own soul and memory as you converse, and the changes sync automatically. To edit them by hand, open `ethagent`. To save your agent onchain so it can come back on any machine, choose Save Snapshot and sign.
|
|
47
|
+
You grow these mostly by talking: with the plugin on, your agent updates its own soul and memory as you converse, and the changes sync automatically. To edit them by hand, open `ethagent`. To save your agent onchain so it can come back on any machine, choose **Save Snapshot** and sign.
|
|
43
48
|
|
|
44
49
|
## 💡 How it works
|
|
45
50
|
|
|
46
51
|
1. **Own it.** Your wallet holds an ERC-8004 token; that token, not a platform account, is the agent.
|
|
47
52
|
2. **Configure it.** Shape its soul, memory, and skills under an ENS name you own.
|
|
48
53
|
3. **Save it.** `ethagent` encrypts everything on your machine, stores the encrypted copy on IPFS, and updates your token to point at it.
|
|
49
|
-
4. **Restore it.** On any machine, `ethagent` reads the pointer, asks your wallet to sign,
|
|
54
|
+
4. **Restore it.** On any machine, `ethagent` finds your agent automatically from your connected wallet, or by ENS name or token id, then reads the pointer, asks your wallet to sign, and fetches and decrypts the snapshot to rebuild it.
|
|
50
55
|
|
|
51
56
|
## ✨ Using your agent
|
|
52
57
|
|
|
53
58
|
**Claude Code comes first.** Install the plugin and your agent shows up in every session, already up to date, and anything it learns gets saved back. Nothing to set up.
|
|
54
59
|
|
|
55
|
-
Using another harness?
|
|
60
|
+
Using another harness? You can still sync, but only Claude Code does it automatically: the plugin's hooks refresh on every session and after edits. Anywhere else, you run it yourself whenever you want to pull changes in:
|
|
56
61
|
|
|
57
62
|
```bash
|
|
58
63
|
npx ethagent --sync
|
|
59
64
|
```
|
|
60
65
|
|
|
61
|
-
It
|
|
66
|
+
It syncs files between `ethagent` and your harness on this machine, but only when you run it. To back it up so you can restore it anywhere, open `ethagent` and choose **Save Snapshot**.
|
|
62
67
|
|
|
63
68
|
## 🔒 What stays private
|
|
64
69
|
|
|
65
70
|
Everything is encrypted on your machine before it leaves: `SOUL.md`, `MEMORY.md`, and every skill.
|
|
66
71
|
|
|
67
72
|
- The encryption keys come from a wallet signature `ethagent` never sees. Signing it is free and moves none of your money. (Saving a backup is separate: it updates your token, a normal transaction with a small fee, usually less than a cent on Base.)
|
|
68
|
-
-
|
|
73
|
+
- Even a public skill keeps its body encrypted: only its name and description go on the Agent Card. That card, carried by your token, also publishes your agent's profile (name, description, optional image) and your owner wallet, which is already public as the token holder.
|
|
69
74
|
- Private skills, soul, and memory are never exposed.
|
|
70
75
|
|
|
71
76
|
In short: the network stores a locked box, and only your wallet holds the key.
|
|
@@ -96,7 +101,7 @@ Run with `npx ethagent`:
|
|
|
96
101
|
|
|
97
102
|
| Command | What it does |
|
|
98
103
|
| --- | --- |
|
|
99
|
-
| `ethagent` | Open the interactive identity manager:
|
|
104
|
+
| `ethagent` | Open the interactive identity manager: create, ENS, custody, snapshots, transfer. |
|
|
100
105
|
| `--sync` | Sync soul, memory, and public skills into every harness it detects. |
|
|
101
106
|
| `--sync-list` | List sync adapters and which ones detect in the current environment. |
|
|
102
107
|
| `--status` | Print a one-line identity summary. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ethagent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Portable Ethereum identity for your AI agent. Its soul, memory, and skills live onchain via ERC-8004 + IPFS and snap back into any session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,9 +33,7 @@
|
|
|
33
33
|
"ai-agent",
|
|
34
34
|
"erc-8004",
|
|
35
35
|
"ens",
|
|
36
|
-
"codex",
|
|
37
36
|
"claude-code",
|
|
38
|
-
"agents",
|
|
39
37
|
"ipfs",
|
|
40
38
|
"wallet",
|
|
41
39
|
"privacy"
|
package/src/cli/hookIo.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
-
// Shared helpers for the Claude Code hook entrypoints (--sync-on-edit, --memory-guard).
|
|
4
|
-
// A hook receives the harness payload as JSON on stdin and may answer with JSON on stdout.
|
|
5
|
-
|
|
6
3
|
export async function readHookPayload(): Promise<Record<string, unknown> | null> {
|
|
7
4
|
if (process.stdin.isTTY) return null
|
|
8
5
|
let raw = ''
|
|
@@ -33,8 +30,6 @@ export function samePath(a: string, b: string): boolean {
|
|
|
33
30
|
return process.platform === 'win32' ? na.toLowerCase() === nb.toLowerCase() : na === nb
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
// True when `file` is `dir` itself or sits anywhere beneath it. Compares on a
|
|
37
|
-
// separator boundary so `.../memory` does not match a sibling like `.../memory-notes`.
|
|
38
33
|
export function isWithinDir(dir: string, file: string): boolean {
|
|
39
34
|
const fold = (p: string): string => (process.platform === 'win32' ? p.toLowerCase() : p)
|
|
40
35
|
const nd = fold(path.resolve(dir))
|
package/src/cli/memoryGuard.ts
CHANGED
|
@@ -2,9 +2,6 @@ import { loadConfig } from '../storage/config.js'
|
|
|
2
2
|
import { hookFilePath, isWithinDir, readHookPayload } from './hookIo.js'
|
|
3
3
|
import { claudeCodeNativeMemoryDir } from './syncAdapters/claude-code.js'
|
|
4
4
|
|
|
5
|
-
// Shown to the model when it tries to write into the harness-native memory dir.
|
|
6
|
-
// The redirect points at a different file (~/.claude/CLAUDE.md), so the model's
|
|
7
|
-
// next attempt succeeds and there is no deny loop.
|
|
8
5
|
export const MEMORY_REDIRECT_REASON =
|
|
9
6
|
"ethagent manages this agent's portable memory. Don't write to the Claude Code native memory directory; " +
|
|
10
7
|
'those files stay on this machine and never reach your onchain vault. Record durable facts by editing ' +
|
|
@@ -24,8 +21,6 @@ export function decideMemoryGuard(
|
|
|
24
21
|
return { deny: false }
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
// PreToolUse hook for Edit|Write|MultiEdit. Fail-open: any error or missing
|
|
28
|
-
// identity allows the write, so the guard never wedges unrelated projects.
|
|
29
24
|
export async function runMemoryGuard(): Promise<number> {
|
|
30
25
|
try {
|
|
31
26
|
const config = await loadConfig()
|
|
@@ -42,8 +37,6 @@ export async function runMemoryGuard(): Promise<number> {
|
|
|
42
37
|
}) + '\n',
|
|
43
38
|
)
|
|
44
39
|
}
|
|
45
|
-
} catch {
|
|
46
|
-
// fail open
|
|
47
|
-
}
|
|
40
|
+
} catch {}
|
|
48
41
|
return 0
|
|
49
42
|
}
|
package/src/cli/sessionStart.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { runSync } from './sync.js'
|
|
2
2
|
|
|
3
|
-
// Injected into the model's context on session start so it records durable facts
|
|
4
|
-
// in the portable markers (which sync to the vault) instead of harness-local files.
|
|
5
3
|
export function buildSessionStartContext(): string {
|
|
6
4
|
return (
|
|
7
5
|
"ethagent portable memory is active. As you converse, keep this agent's soul and memory current by editing " +
|
|
@@ -13,14 +11,10 @@ export function buildSessionStartContext(): string {
|
|
|
13
11
|
)
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
// SessionStart hook: restore (sync vault -> harness) then remind (inject guidance).
|
|
17
|
-
// runSync is quiet here so only the JSON envelope reaches stdout for the harness to parse.
|
|
18
14
|
export async function runSessionStart(): Promise<number> {
|
|
19
15
|
try {
|
|
20
16
|
await runSync({ quiet: true })
|
|
21
|
-
} catch {
|
|
22
|
-
// still emit guidance even if the sync step failed
|
|
23
|
-
}
|
|
17
|
+
} catch {}
|
|
24
18
|
process.stdout.write(
|
|
25
19
|
JSON.stringify({
|
|
26
20
|
hookSpecificOutput: {
|
|
@@ -22,16 +22,10 @@ function claudeProjectMemoryMdPath(): string {
|
|
|
22
22
|
return path.join(claudeDir(), 'projects', slug, 'memory', 'MEMORY.md')
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// The Claude Code native per-project memory directory for the current project.
|
|
26
|
-
// ethagent's portable memory supersedes this; the --memory-guard hook redirects
|
|
27
|
-
// the model away from writing here so nothing siloes on one machine.
|
|
28
25
|
export function claudeCodeNativeMemoryDir(): string {
|
|
29
26
|
return path.dirname(claudeProjectMemoryMdPath())
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
// Every project's mirrored MEMORY.md under a given ~/.claude root, across all
|
|
33
|
-
// directories the agent has ever been synced in, not just the current cwd.
|
|
34
|
-
// Reset uses this so no project is left whispering a stale ethagent block.
|
|
35
29
|
export async function projectMemoryMirrorsUnder(claudeRoot: string): Promise<string[]> {
|
|
36
30
|
const projectsDir = path.join(claudeRoot, 'projects')
|
|
37
31
|
let slugs: string[]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { Box, Text } from 'ink'
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Box, Text, useStdout } from 'ink'
|
|
3
3
|
import { theme, gradientColor } from '../../../../ui/theme.js'
|
|
4
4
|
|
|
5
5
|
export const LINES = [
|
|
@@ -31,24 +31,54 @@ export const RIGHT_DECOR = [
|
|
|
31
31
|
' ',
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
const WORDMARK_WIDTH = Math.max(...LINES.map(line => line.length))
|
|
35
|
+
const DECOR_WIDTH = 12
|
|
36
|
+
|
|
37
|
+
export type WordmarkLayout = 'full' | 'bare' | 'hidden'
|
|
38
|
+
|
|
39
|
+
export function wordmarkLayout(columns: number): WordmarkLayout {
|
|
40
|
+
if (columns >= WORDMARK_WIDTH + DECOR_WIDTH * 2) return 'full'
|
|
41
|
+
if (columns >= WORDMARK_WIDTH) return 'bare'
|
|
42
|
+
return 'hidden'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const Banner: React.FC = () => (
|
|
46
|
+
<Box flexDirection="column">
|
|
47
|
+
{LINES.map((line, i) => {
|
|
48
|
+
const eth = line.slice(0, SPLIT)
|
|
49
|
+
const agent = line.slice(SPLIT)
|
|
50
|
+
const maxAgent = Math.max(1, agent.length - 1)
|
|
51
|
+
return (
|
|
52
|
+
<Text key={i}>
|
|
53
|
+
<Text color={theme.wordmarkEth}>{eth}</Text>
|
|
54
|
+
{[...agent].map((ch, j) => (
|
|
55
|
+
<Text key={j} color={gradientColor(j / maxAgent)}>{ch}</Text>
|
|
56
|
+
))}
|
|
57
|
+
</Text>
|
|
58
|
+
)
|
|
59
|
+
})}
|
|
53
60
|
</Box>
|
|
54
61
|
)
|
|
62
|
+
|
|
63
|
+
export const Wordmark: React.FC = () => {
|
|
64
|
+
const { stdout } = useStdout()
|
|
65
|
+
const [columns, setColumns] = useState<number>(() => Math.floor(stdout?.columns ?? 80))
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!stdout) return
|
|
68
|
+
const handleResize = () => setColumns(Math.floor(stdout.columns ?? 80))
|
|
69
|
+
stdout.on('resize', handleResize)
|
|
70
|
+
return () => { stdout.off('resize', handleResize) }
|
|
71
|
+
}, [stdout])
|
|
72
|
+
|
|
73
|
+
const layout = wordmarkLayout(columns)
|
|
74
|
+
if (layout === 'hidden') return null
|
|
75
|
+
if (layout === 'bare') return <Banner />
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Box flexDirection="row">
|
|
79
|
+
<Text color={theme.wordmarkEth}>{LEFT_DECOR.join('\n')}</Text>
|
|
80
|
+
<Banner />
|
|
81
|
+
<Text color={theme.wordmarkEth}>{RIGHT_DECOR.join('\n')}</Text>
|
|
82
|
+
</Box>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
// Smoothly animates the card's height whenever its content changes, so the
|
|
2
|
-
// surface glides between states instead of snapping. The animation is the
|
|
3
|
-
// native Web Animations API, with no external library, so it stays lightweight
|
|
4
|
-
// and runs the same on every machine. It falls back to an instant resize when
|
|
5
|
-
// the API is unavailable or the user prefers reduced motion.
|
|
6
1
|
|
|
7
2
|
let cardEl: HTMLElement | null = null;
|
|
8
3
|
let cardInnerEl: HTMLElement | null = null;
|
|
@@ -11,7 +6,6 @@ let trackedHeight = 0;
|
|
|
11
6
|
let measuredOnce = false;
|
|
12
7
|
let cardResizeReady = false;
|
|
13
8
|
|
|
14
|
-
// Snappy ease-out (mirrors --ease-out in css.ts): quick to start, gentle to settle.
|
|
15
9
|
const RESIZE_DURATION_MS = 200;
|
|
16
10
|
const RESIZE_EASING = "cubic-bezier(0.16, 1, 0.3, 1)";
|
|
17
11
|
|
|
@@ -57,11 +51,9 @@ function animateHeight(from: number, to: number): void {
|
|
|
57
51
|
const el = cardEl;
|
|
58
52
|
if (!el) return;
|
|
59
53
|
if (resizeAnim) {
|
|
60
|
-
try { resizeAnim.cancel(); } catch (_) {
|
|
54
|
+
try { resizeAnim.cancel(); } catch (_) {}
|
|
61
55
|
resizeAnim = null;
|
|
62
56
|
}
|
|
63
|
-
// The rounded inline height is the source of truth: set it first so the card
|
|
64
|
-
// rests at the final size once the animation ends (default fill is none).
|
|
65
57
|
applyHeight(to);
|
|
66
58
|
if (typeof el.animate !== "function" || prefersReducedMotion() || Math.abs(to - from) < 1) return;
|
|
67
59
|
const anim = el.animate(
|