ethagent 4.1.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.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "4.1.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": {
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(