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:
@@ -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(':'),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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(':'),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user