fix(autostart): detect hermes-agent installed by Nous installer

User reported on X:
  'error: hermes agent not found clone it as a sibling directory or set
   hermes agent path in .env'

after running the fixed install.sh. The root cause is that Nous's installer
puts hermes-agent at ~/.hermes/hermes-agent/ (or installs a `hermes` binary
to ~/.hermes/bin/), but the workspace auto-start logic only looked at:
  - HERMES_AGENT_PATH env var
  - ../hermes-agent (sibling)
  - ../../hermes-agent (monorepo)

None of which match Nous's actual install layout. So every user installing
via the one-liner hit this error.

Changes in both vite.config.ts and src/server/hermes-agent.ts:
- Add ~/.hermes/hermes-agent and ~/hermes-agent to the candidate list
- Add a resolveHermesBinary() that finds ~/.hermes/bin/hermes or
  ~/.local/bin/hermes (Nous installer + fallback)
- Prefer launching `hermes gateway run` via the binary (canonical
  entrypoint) over reconstructing uvicorn against the source tree
- Fall back to uvicorn-from-source when only a directory is available
  (dev / cloned-in-place setups)
- Update the 'not found' error message to point at the installer URL
  instead of asking users to clone a sibling dir
- PATH for the spawned child now includes ~/.hermes/bin and ~/.local/bin
This commit is contained in:
Eric
2026-04-20 16:38:09 -04:00
parent 62ffc6a2d5
commit 1867955693
2 changed files with 113 additions and 38 deletions

View File

@@ -49,7 +49,7 @@ function readHermesEnv(): Record<string, string> {
}
}
/** Same directory resolution logic as vite.config.ts. */
/** Same directory resolution logic as vite.config.ts. Kept in sync. */
export function resolveHermesAgentDir(
env: Record<string, string | undefined> = process.env,
): string | null {
@@ -61,8 +61,10 @@ export function resolveHermesAgentDir(
const workspaceRoot = dirname(resolve('.'))
candidates.push(
resolve(workspaceRoot, 'hermes-agent'),
resolve(workspaceRoot, '..', 'hermes-agent'),
resolve(workspaceRoot, 'hermes-agent'), // sibling (old README)
resolve(workspaceRoot, '..', 'hermes-agent'), // one level up
resolve(homedir(), '.hermes', 'hermes-agent'), // Nous installer default
resolve(homedir(), 'hermes-agent'), // ~/hermes-agent
)
for (const candidate of candidates) {
@@ -72,11 +74,26 @@ export function resolveHermesAgentDir(
return null
}
/** Find the `hermes` CLI binary installed by Nous's installer (or on PATH). */
export function resolveHermesBinary(): string | null {
const candidates = [
resolve(homedir(), '.hermes', 'bin', 'hermes'),
resolve(homedir(), '.local', 'bin', 'hermes'),
]
for (const c of candidates) {
if (existsSync(c)) return c
}
return null
}
export function resolveHermesPython(agentDir: string): string {
const venvPython = resolve(agentDir, '.venv', 'bin', 'python')
if (existsSync(venvPython)) return venvPython
const uvVenv = resolve(agentDir, 'venv', 'bin', 'python')
if (existsSync(uvVenv)) return uvVenv
// Nous installer ships its own uv-managed python alongside the binary
const nousPython = resolve(homedir(), '.hermes', 'venv', 'bin', 'python')
if (existsSync(nousPython)) return nousPython
return 'python3'
}
@@ -104,21 +121,24 @@ export async function startHermesAgent(): Promise<StartHermesAgentResult> {
startPromise = (async () => {
try {
const agentDir = resolveHermesAgentDir()
if (!agentDir) {
return {
ok: false,
error:
'hermes-agent not found. Clone it as a sibling directory or set HERMES_AGENT_PATH in .env',
}
}
const python = resolveHermesPython(agentDir)
const hermesEnv = readHermesEnv()
const hermesBin = resolveHermesBinary()
const agentDir = resolveHermesAgentDir()
const child = spawn(
python,
[
// Prefer the `hermes gateway run` binary path (the Nous installer's
// canonical entrypoint). Fall back to launching uvicorn against the
// source tree if we only have a directory.
let command: string
let commandArgs: Array<string>
let cwd: string | undefined
if (hermesBin) {
command = hermesBin
commandArgs = ['gateway', 'run']
cwd = agentDir ?? undefined
} else if (agentDir) {
command = resolveHermesPython(agentDir)
commandArgs = [
'-m',
'uvicorn',
'webapi.app:app',
@@ -126,15 +146,33 @@ export async function startHermesAgent(): Promise<StartHermesAgentResult> {
'0.0.0.0',
'--port',
String(HERMES_START_PORT),
],
]
cwd = agentDir
} else {
return {
ok: false,
error:
"hermes-agent not found. Run the installer: curl -fsSL https://hermes-workspace.com/install.sh | bash",
}
}
const child = spawn(
command,
commandArgs,
{
cwd: agentDir,
cwd,
detached: true,
stdio: 'ignore',
env: {
...process.env,
...hermesEnv,
PATH: `${resolve(agentDir, '.venv', 'bin')}:${resolve(agentDir, 'venv', 'bin')}:${process.env.PATH || ''}`,
PATH: [
resolve(homedir(), '.hermes', 'bin'),
resolve(homedir(), '.local', 'bin'),
agentDir ? resolve(agentDir, '.venv', 'bin') : '',
agentDir ? resolve(agentDir, 'venv', 'bin') : '',
process.env.PATH || '',
].filter(Boolean).join(':'),
},
},
)

View File

@@ -34,8 +34,10 @@ function resolveHermesAgentDir(env: Record<string, string>): string | null {
// Resolve relative to the workspace root (parent of hermes-workspace/)
const workspaceRoot = dirname(resolve('.'))
candidates.push(
resolve(workspaceRoot, 'hermes-agent'), // sibling hermes-agent directory
resolve(workspaceRoot, '..', 'hermes-agent'), // one level up
resolve(workspaceRoot, 'hermes-agent'), // sibling (old README)
resolve(workspaceRoot, '..', 'hermes-agent'), // one level up
resolve(os.homedir(), '.hermes', 'hermes-agent'), // Nous installer default
resolve(os.homedir(), 'hermes-agent'), // ~/hermes-agent
)
for (const candidate of candidates) {
@@ -44,6 +46,18 @@ function resolveHermesAgentDir(env: Record<string, string>): string | null {
return null
}
/** Find the `hermes` CLI binary installed by Nous's installer. */
function resolveHermesBinary(): string | null {
const candidates = [
resolve(os.homedir(), '.hermes', 'bin', 'hermes'),
resolve(os.homedir(), '.local', 'bin', 'hermes'),
]
for (const c of candidates) {
if (existsSync(c)) return c
}
return null
}
/** Resolve the Python executable to use for Hermes backend startup.
* Prefers .venv/bin/python inside agentDir, falls back to system python3.
*/
@@ -98,36 +112,59 @@ const config = defineConfig(({ mode, command }) => {
return
}
const hermesBin = resolveHermesBinary()
const agentDir = resolveHermesAgentDir(env)
if (!agentDir) {
// Prefer the `hermes gateway run` binary path (Nous installer's canonical
// entrypoint). Fall back to launching uvicorn against the source tree if
// only a directory is present (dev / cloned-in-place setups).
let launchCmd: string
let commandArgs: string[]
let launchCwd: string | undefined
if (hermesBin) {
launchCmd = hermesBin
commandArgs = ['gateway', 'run']
launchCwd = agentDir ?? undefined
console.log(`[hermes-agent] Starting ${hermesBin} gateway run`)
} else if (agentDir) {
launchCmd = resolveHermesPython(agentDir)
const useGatewayRun = existsSync(resolve(agentDir, 'gateway', 'run.py'))
commandArgs = useGatewayRun
? ['-m', 'gateway.run']
: ['-m', 'uvicorn', 'webapi.app:app', '--host', '0.0.0.0', '--port', '8642']
launchCwd = agentDir
console.log(
`[hermes-agent] Starting from ${agentDir} using ${launchCmd} (${useGatewayRun ? 'gateway.run' : 'uvicorn'})`,
)
} else {
console.warn(
'[hermes-agent] Could not find hermes-agent directory.\n' +
' Set HERMES_AGENT_PATH in .env or clone hermes-agent as a sibling:\n' +
' git clone https://github.com/outsourc-e/hermes-agent.git ../hermes-agent',
'[hermes-agent] Could not find hermes-agent installation.\n' +
' Run the installer:\n' +
' curl -fsSL https://hermes-workspace.com/install.sh | bash\n' +
' Or set HERMES_AGENT_PATH in .env to point at your hermes-agent clone.',
)
return
}
const python = resolveHermesPython(agentDir)
const useGatewayRun = existsSync(resolve(agentDir, 'gateway', 'run.py'))
const commandArgs = useGatewayRun
? ['-m', 'gateway.run']
: ['-m', 'uvicorn', 'webapi.app:app', '--host', '0.0.0.0', '--port', '8642']
console.log(
`[hermes-agent] Starting from ${agentDir} using ${python} (${useGatewayRun ? 'gateway.run' : 'uvicorn'})`,
)
const child = spawn(
python,
launchCmd,
commandArgs,
{
cwd: agentDir,
cwd: launchCwd,
detached: false, // keep tied to vite process — stops when dev server stops
stdio: 'pipe',
env: {
...process.env,
PATH: `${resolve(agentDir, '.venv', 'bin')}:${resolve(agentDir, 'venv', 'bin')}:${process.env.PATH || ''}`,
PATH: [
resolve(os.homedir(), '.hermes', 'bin'),
resolve(os.homedir(), '.local', 'bin'),
agentDir ? resolve(agentDir, '.venv', 'bin') : '',
agentDir ? resolve(agentDir, 'venv', 'bin') : '',
process.env.PATH || '',
]
.filter(Boolean)
.join(':'),
},
},
)