feat(browser): cross-platform node_path config for npx resolution
Adds [browser].node_path config option so camoufox-browser can use an explicit npx path (essential for nvm/fnm/asdf users). Resolution order (cross-platform): 1. config.browser.node_path — explicit user override 2. 'npx.cmd' on Windows, 'npx' on Linux/macOS — PATH lookup Changes: - BrowserSection config: added node_path: Option<String> - AgentConfig: now carries browser section (was dropped in From<GyandootConfig>) - browser_manager::init_browser(node_path): accepts optional path - All call sites updated (run(), init_session(), subagents, tests) - Error messages now hint at browser.node_path config
This commit is contained in:
@@ -193,7 +193,7 @@ impl AgentLoop {
|
||||
// ── Auto-init Camoufox and register browser tools ──────────────
|
||||
{
|
||||
let mut tool_executor = self.tool_executor.lock().await;
|
||||
match crate::browser_manager::init_browser().await {
|
||||
match crate::browser_manager::init_browser(self.config.browser.node_path.as_deref()).await {
|
||||
crate::browser_manager::BrowserInitResult::Ready(client) => {
|
||||
info!("Camoufox browser ready — registering web_search and web_browse tools");
|
||||
crate::browser_tools::register_browser_tools(&mut tool_executor, client.clone());
|
||||
@@ -400,7 +400,7 @@ impl AgentLoop {
|
||||
// Browser init
|
||||
{
|
||||
let mut tool_executor = self.tool_executor.lock().await;
|
||||
match crate::browser_manager::init_browser().await {
|
||||
match crate::browser_manager::init_browser(self.config.browser.node_path.as_deref()).await {
|
||||
crate::browser_manager::BrowserInitResult::Ready(client) => {
|
||||
info!("Camoufox browser ready — registering web_search and web_browse tools");
|
||||
crate::browser_tools::register_browser_tools(&mut tool_executor, client.clone());
|
||||
@@ -902,7 +902,7 @@ mod tests {
|
||||
use super::*;
|
||||
use gyandoot_core::config::IdentitySection;
|
||||
use gyandoot_core::{
|
||||
config::AgentConfig, config::KanbanRootConfig, config::MemoryRootConfig,
|
||||
config::AgentConfig, config::BrowserSection, config::KanbanRootConfig, config::MemoryRootConfig,
|
||||
config::PlatformRootConfig, config::ProviderRootConfig, config::SecurityRootConfig,
|
||||
config::SkillsRootConfig, Message, MessageRole, Role, ToolCall, ToolDefinition,
|
||||
};
|
||||
@@ -921,6 +921,7 @@ mod tests {
|
||||
kanban: KanbanRootConfig::default(),
|
||||
skills: SkillsRootConfig::default(),
|
||||
identity: IdentitySection::default(),
|
||||
browser: BrowserSection::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
//! 2. If not, runs a quick CLI command to trigger daemon auto-start
|
||||
//! 3. Waits for the Unix socket to appear (up to 30 seconds)
|
||||
//! 4. Returns a ready-to-use `BrowserClient`
|
||||
//!
|
||||
//! # Cross-platform npx resolution
|
||||
//!
|
||||
//! The daemon is started via Node.js's `npx`. Resolution order:
|
||||
//! 1. `config.browser.node_path` — explicit path in config (e.g.
|
||||
//! `/home/user/.nvm/versions/node/v22/bin/npx`)
|
||||
//! 2. `"npx"` on Linux/macOS, `"npx.cmd"` on Windows — PATH lookup
|
||||
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
@@ -37,8 +44,29 @@ pub enum BrowserInitResult {
|
||||
Unavailable(String),
|
||||
}
|
||||
|
||||
/// Resolve the npx executable name, respecting config and platform.
|
||||
///
|
||||
/// Order:
|
||||
/// 1. `node_path` from config (explicit user override)
|
||||
/// 2. Platform default: `"npx.cmd"` on Windows, `"npx"` everywhere else
|
||||
fn resolve_npx(node_path: Option<&str>) -> &str {
|
||||
if let Some(p) = node_path {
|
||||
if !p.is_empty() {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
if cfg!(target_os = "windows") {
|
||||
"npx.cmd"
|
||||
} else {
|
||||
"npx"
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the camoufox-browser daemon and return a ready client.
|
||||
pub async fn init_browser() -> BrowserInitResult {
|
||||
///
|
||||
/// `node_path` — optional explicit path to `npx`/`npx.cmd` (from
|
||||
/// `config.browser.node_path`). Pass `None` for default PATH lookup.
|
||||
pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult {
|
||||
let session = DEFAULT_SESSION;
|
||||
let socket_path = default_socket_path(session);
|
||||
|
||||
@@ -52,8 +80,7 @@ pub async fn init_browser() -> BrowserInitResult {
|
||||
info!("Camoufox daemon not running, triggering startup...");
|
||||
|
||||
// 2. Trigger daemon auto-start via CLI
|
||||
// The CLI calls `ensureDaemonRunning()` internally which spawns the daemon
|
||||
match trigger_daemon_start(session).await {
|
||||
match trigger_daemon_start(session, node_path).await {
|
||||
Ok(()) => {
|
||||
// 3. Wait for socket to appear
|
||||
info!("Waiting for daemon socket at {}...", socket_path.display());
|
||||
@@ -82,13 +109,18 @@ pub async fn init_browser() -> BrowserInitResult {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let npx_name = resolve_npx(node_path);
|
||||
let msg = format!(
|
||||
"Could not start camoufox daemon: {}\n\n\
|
||||
Make sure Node.js v20+ and the package are installed:\n \
|
||||
npm install -g camoufox-browser\n \
|
||||
npx camoufox-browser@latest install\n\n\
|
||||
{} camoufox-browser@latest install\n\n\
|
||||
If Node is installed via nvm/fnm/asdf, set browser.node_path\n\
|
||||
in your config, e.g.:\n \
|
||||
[browser]\n \
|
||||
node_path = \"/home/user/.nvm/versions/node/v22/bin/npx\"\n\n\
|
||||
Then try again.",
|
||||
e
|
||||
e, npx_name
|
||||
);
|
||||
warn!("{}", msg);
|
||||
BrowserInitResult::Unavailable(msg)
|
||||
@@ -97,13 +129,7 @@ pub async fn init_browser() -> BrowserInitResult {
|
||||
}
|
||||
|
||||
/// Trigger daemon startup by running a quick CLI command.
|
||||
///
|
||||
/// The `camoufox-browser@latest` CLI auto-starts the daemon via
|
||||
/// `ensureDaemonRunning()` on any command. We run a harmless
|
||||
/// `tab_list` which spawns the daemon in the background and
|
||||
/// returns once complete.
|
||||
async fn trigger_daemon_start(session: &str) -> Result<(), String> {
|
||||
// Create log directory for any stderr output
|
||||
async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<(), String> {
|
||||
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
|
||||
let log_dir = home.join(".gyandoot");
|
||||
std::fs::create_dir_all(&log_dir)
|
||||
@@ -113,8 +139,10 @@ async fn trigger_daemon_start(session: &str) -> Result<(), String> {
|
||||
let log = std::fs::File::create(&log_file)
|
||||
.map_err(|e| format!("Cannot create log file: {}", e))?;
|
||||
|
||||
let npx = resolve_npx(node_path);
|
||||
|
||||
// Run a command — the CLI auto-starts the daemon as a side effect
|
||||
let _output = Command::new("npx")
|
||||
let _output = Command::new(npx)
|
||||
.args(&[
|
||||
"camoufox-browser@latest",
|
||||
"tab_list",
|
||||
@@ -124,9 +152,8 @@ async fn trigger_daemon_start(session: &str) -> Result<(), String> {
|
||||
.stderr(Stdio::from(log))
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("npx failed: {}. Is Node.js installed?", e))?;
|
||||
.map_err(|e| format!("{} failed: {}. Is Node.js installed?", npx, e))?;
|
||||
|
||||
// Return immediately — socket polling in the caller will detect the daemon
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user