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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "4.0.0",
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 once:
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. Come back to `ethagent` only to edit your agent by hand or back it up.
39
+ That's the whole setup. You'll only open `ethagent` again to hand-edit your agent or save a backup.
35
40
 
36
- ## 📝 Three files you shape
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, then fetches and decrypts the snapshot to rebuild your agent, found automatically from your connected wallet, or by ENS name or token id.
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? One command syncs it with `ethagent`:
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 only syncs files between `ethagent` and your harness on this machine. To back it up so you can restore it anywhere, open `ethagent` and choose **Save Snapshot**.
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
- - A public skill is **not** decrypted. The Agent Card on your token publishes your agent's profile (name, description, optional image), each public skill's name and description, and your owner wallet (already public as the token holder).
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: mint, ENS, custody, snapshots, transfer. |
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.0.0",
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))
@@ -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
  }
@@ -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[]
@@ -88,7 +88,6 @@ export async function restoreSkillsTree(
88
88
  await materializeSkillsTree(identity, tree)
89
89
  }
90
90
 
91
-
92
91
  export async function ensureAgentCardFile(
93
92
  identity: EthagentIdentity,
94
93
  options: { fallback?: string | (() => Promise<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
- export const Wordmark: React.FC = () => (
35
- <Box flexDirection="row">
36
- <Text color={theme.wordmarkEth}>{LEFT_DECOR.join('\n')}</Text>
37
- <Box flexDirection="column">
38
- {LINES.map((line, i) => {
39
- const eth = line.slice(0, SPLIT)
40
- const agent = line.slice(SPLIT)
41
- const maxAgent = Math.max(1, agent.length - 1)
42
- return (
43
- <Text key={i}>
44
- <Text color={theme.wordmarkEth}>{eth}</Text>
45
- {[...agent].map((ch, j) => (
46
- <Text key={j} color={gradientColor(j / maxAgent)}>{ch}</Text>
47
- ))}
48
- </Text>
49
- )
50
- })}
51
- </Box>
52
- <Text color={theme.wordmarkEth}>{RIGHT_DECOR.join('\n')}</Text>
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
+ }
@@ -224,7 +224,6 @@ async function probeAgentUri(args: {
224
224
  }
225
225
  }
226
226
 
227
-
228
227
  type VaultProbe = { kind: 'confirmed' | 'missing' | 'unset' | 'unknown' }
229
228
 
230
229
  async function probeVault(args: {
@@ -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 (_) { /* noop */ }
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(