#!/usr/bin/env node import React, { useEffect, useState } from 'react' import { render, Box, Text, useApp, useStdout } from 'ink' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { theme } from '../ui/theme.js' import { Spinner } from '../ui/Spinner.js' import { KeybindingProvider } from '../app/keybindings/KeybindingProvider.js' import { IdentityManager } from '../identity/manager/IdentityManager.js' import type { IdentityManagerResult } from '../identity/manager/IdentityManager.js' import { loadConfig, saveConfig, type EthagentConfig } from '../storage/config.js' import { runSyncOnEdit } from './sync.js' import { runMemoryGuard } from './memoryGuard.js' import { runSkillGuard } from './skillGuard.js' import { runPreToolGuard } from './pretoolGuard.js' import { runSessionStart } from './sessionStart.js' import { runStatus } from './status.js' import { runVaultDir } from './vaultDir.js' import { runResetCommand } from './reset.js' import { runSave } from './save.js' import { ensureBootstrapped } from './bootstrap.js' import { ensureDaemon } from './daemon.js' import { runWatch } from './watch.js' import { runAddTool, runPause, runResume } from './prefs.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) function readVersion(): string { try { const pkgPath = path.resolve(__dirname, '..', '..', 'package.json') const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { version?: string } return pkg.version ?? '0.0.0' } catch { return '0.0.0' } } function printHelp(): void { const lines = [ 'ethagent: privacy-first AI agent with a portable Ethereum identity', '', 'usage:', ' ethagent manage identity', ' ethagent save back up your agent onchain (you approve in your wallet)', ' ethagent reset delete local identity, continuity, and secrets', ' ethagent --add wire another tool by its config file (e.g. AGENTS.md)', ' ethagent pause pause background sync (resume with: ethagent resume)', ' ethagent --status print a one-line identity summary', " ethagent --vault-dir print this agent's vault directory path", ' ethagent --version print version', ' ethagent --help print this help', ] for (const line of lines) process.stdout.write(line + '\n') } type Phase = | { kind: 'loading' } | { kind: 'ready'; config: EthagentConfig | null } | { kind: 'error'; message: string } type RootProps = { setExit: (n: number) => void initialConfig: EthagentConfig | null | undefined } const Root: React.FC = ({ setExit, initialConfig }) => { const [phase, setPhase] = useState( initialConfig !== undefined ? { kind: 'ready', config: initialConfig } : { kind: 'loading' }, ) const { exit } = useApp() const { stdout } = useStdout() const [rows, setRows] = useState(() => stdout?.rows ?? 24) useEffect(() => { if (!stdout) return const onResize = () => setRows(stdout.rows ?? 24) stdout.on('resize', onResize) return () => { stdout.off('resize', onResize) } }, [stdout]) useEffect(() => { if (phase.kind !== 'loading') return let cancelled = false loadConfig() .then(c => { if (!cancelled) setPhase({ kind: 'ready', config: c }) }) .catch(err => { if (!cancelled) setPhase({ kind: 'error', message: (err as Error).message }) }) return () => { cancelled = true } }, [phase.kind]) if (phase.kind === 'loading') { return } if (phase.kind === 'error') { return Error: {phase.message} } const mode = phase.config?.identity ? 'manage' : 'first-run' const handleComplete = (result: IdentityManagerResult): void => { setExit(result.kind === 'cancel' ? 1 : 0) setTimeout(() => exit(), 10) } const handleConfigChange = (next: EthagentConfig): void => { setPhase({ kind: 'ready', config: next }) void saveConfig(next) } return ( ) } let altScreenActive = false function enterAltScreen(): void { if (altScreenActive) return altScreenActive = true process.stdout.write('\x1b[?1049h\x1b[22;0t\x1b]0;ethagent\x07') if (process.platform === 'win32') process.title = 'ethagent' } function leaveAltScreen(): void { if (!altScreenActive) return altScreenActive = false process.stdout.write('\x1b[?25h\x1b[23;0t\x1b[?1049l') } async function renderHub(initialConfig: EthagentConfig | null | undefined): Promise { enterAltScreen() let exitCode = 0 const instance = render( { exitCode = n }} initialConfig={initialConfig} /> , { exitOnCtrlC: false }, ) const guard = (reason: unknown): void => { const message = reason instanceof Error ? reason.message : String(reason) process.stderr.write(`background error: ${message}\n`) } const onUnhandledRejection = (reason: unknown): void => guard(reason) const onUncaughtException = (err: unknown): void => guard(err) process.on('unhandledRejection', onUnhandledRejection) process.on('uncaughtException', onUncaughtException) try { await instance.waitUntilExit() } catch { exitCode = 1 } finally { process.removeListener('unhandledRejection', onUnhandledRejection) process.removeListener('uncaughtException', onUncaughtException) leaveAltScreen() } return exitCode } async function main(): Promise { const argv = process.argv.slice(2) const flags = new Set(argv) const version = readVersion() if (flags.has('--version') || flags.has('-v')) { process.stdout.write(`ethagent ${version}\n`) return 0 } if (flags.has('--help') || flags.has('-h') || argv[0] === 'help') { printHelp() return 0 } if (flags.has('--sync-on-edit')) return runSyncOnEdit() if (flags.has('--session-start')) return runSessionStart() if (flags.has('--pretool-guard')) return runPreToolGuard() if (flags.has('--memory-guard')) return runMemoryGuard() if (flags.has('--skill-guard')) return runSkillGuard() if (flags.has('--ensure-daemon')) { ensureDaemon(); return 0 } if (argv[0] === 'watch') return runWatch(argv.slice(1)) if (flags.has('--add')) return runAddTool(argv.filter(a => a !== '--add')) if (argv[0] === 'pause') return runPause() if (argv[0] === 'resume') return runResume() if (flags.has('--vault-dir')) return runVaultDir() if (argv[0] !== 'reset') await ensureBootstrapped() if (flags.has('--status')) return runStatus(version) if (argv[0] === 'save') return runSave(argv.slice(1)) if (flags.has('--save')) return runSave(argv.filter(a => a !== '--save')) if (argv[0] === 'reset') return runResetCommand(argv.slice(1)) const unknown = argv.find(a => a.startsWith('-')) if (unknown) { process.stderr.write(`unknown flag: ${unknown}\nrun 'ethagent --help' for usage\n`) return 2 } const positional = argv[0] if (positional) { process.stderr.write(`unknown command: ${positional}\nrun 'ethagent --help' for usage\n`) return 2 } const exitCode = await renderHub(undefined) await ensureBootstrapped() return exitCode } main() .then(code => process.exit(code)) .catch(err => { leaveAltScreen() process.stderr.write(`${(err as Error).message}\n`) process.exit(1) })