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 ──────────────
|
// ── Auto-init Camoufox and register browser tools ──────────────
|
||||||
{
|
{
|
||||||
let mut tool_executor = self.tool_executor.lock().await;
|
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) => {
|
crate::browser_manager::BrowserInitResult::Ready(client) => {
|
||||||
info!("Camoufox browser ready — registering web_search and web_browse tools");
|
info!("Camoufox browser ready — registering web_search and web_browse tools");
|
||||||
crate::browser_tools::register_browser_tools(&mut tool_executor, client.clone());
|
crate::browser_tools::register_browser_tools(&mut tool_executor, client.clone());
|
||||||
@@ -400,7 +400,7 @@ impl AgentLoop {
|
|||||||
// Browser init
|
// Browser init
|
||||||
{
|
{
|
||||||
let mut tool_executor = self.tool_executor.lock().await;
|
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) => {
|
crate::browser_manager::BrowserInitResult::Ready(client) => {
|
||||||
info!("Camoufox browser ready — registering web_search and web_browse tools");
|
info!("Camoufox browser ready — registering web_search and web_browse tools");
|
||||||
crate::browser_tools::register_browser_tools(&mut tool_executor, client.clone());
|
crate::browser_tools::register_browser_tools(&mut tool_executor, client.clone());
|
||||||
@@ -902,7 +902,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use gyandoot_core::config::IdentitySection;
|
use gyandoot_core::config::IdentitySection;
|
||||||
use gyandoot_core::{
|
use gyandoot_core::{
|
||||||
config::AgentConfig, config::KanbanRootConfig, config::MemoryRootConfig,
|
config::AgentConfig, config::BrowserSection, config::KanbanRootConfig, config::MemoryRootConfig,
|
||||||
config::PlatformRootConfig, config::ProviderRootConfig, config::SecurityRootConfig,
|
config::PlatformRootConfig, config::ProviderRootConfig, config::SecurityRootConfig,
|
||||||
config::SkillsRootConfig, Message, MessageRole, Role, ToolCall, ToolDefinition,
|
config::SkillsRootConfig, Message, MessageRole, Role, ToolCall, ToolDefinition,
|
||||||
};
|
};
|
||||||
@@ -921,6 +921,7 @@ mod tests {
|
|||||||
kanban: KanbanRootConfig::default(),
|
kanban: KanbanRootConfig::default(),
|
||||||
skills: SkillsRootConfig::default(),
|
skills: SkillsRootConfig::default(),
|
||||||
identity: IdentitySection::default(),
|
identity: IdentitySection::default(),
|
||||||
|
browser: BrowserSection::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
//! 2. If not, runs a quick CLI command to trigger daemon auto-start
|
//! 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)
|
//! 3. Waits for the Unix socket to appear (up to 30 seconds)
|
||||||
//! 4. Returns a ready-to-use `BrowserClient`
|
//! 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::process::Stdio;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -37,8 +44,29 @@ pub enum BrowserInitResult {
|
|||||||
Unavailable(String),
|
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.
|
/// 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 session = DEFAULT_SESSION;
|
||||||
let socket_path = default_socket_path(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...");
|
info!("Camoufox daemon not running, triggering startup...");
|
||||||
|
|
||||||
// 2. Trigger daemon auto-start via CLI
|
// 2. Trigger daemon auto-start via CLI
|
||||||
// The CLI calls `ensureDaemonRunning()` internally which spawns the daemon
|
match trigger_daemon_start(session, node_path).await {
|
||||||
match trigger_daemon_start(session).await {
|
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// 3. Wait for socket to appear
|
// 3. Wait for socket to appear
|
||||||
info!("Waiting for daemon socket at {}...", socket_path.display());
|
info!("Waiting for daemon socket at {}...", socket_path.display());
|
||||||
@@ -82,13 +109,18 @@ pub async fn init_browser() -> BrowserInitResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let npx_name = resolve_npx(node_path);
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"Could not start camoufox daemon: {}\n\n\
|
"Could not start camoufox daemon: {}\n\n\
|
||||||
Make sure Node.js v20+ and the package are installed:\n \
|
Make sure Node.js v20+ and the package are installed:\n \
|
||||||
npm install -g camoufox-browser\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.",
|
Then try again.",
|
||||||
e
|
e, npx_name
|
||||||
);
|
);
|
||||||
warn!("{}", msg);
|
warn!("{}", msg);
|
||||||
BrowserInitResult::Unavailable(msg)
|
BrowserInitResult::Unavailable(msg)
|
||||||
@@ -97,13 +129,7 @@ pub async fn init_browser() -> BrowserInitResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Trigger daemon startup by running a quick CLI command.
|
/// Trigger daemon startup by running a quick CLI command.
|
||||||
///
|
async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<(), String> {
|
||||||
/// 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
|
|
||||||
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
|
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
|
||||||
let log_dir = home.join(".gyandoot");
|
let log_dir = home.join(".gyandoot");
|
||||||
std::fs::create_dir_all(&log_dir)
|
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)
|
let log = std::fs::File::create(&log_file)
|
||||||
.map_err(|e| format!("Cannot create log file: {}", e))?;
|
.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
|
// Run a command — the CLI auto-starts the daemon as a side effect
|
||||||
let _output = Command::new("npx")
|
let _output = Command::new(npx)
|
||||||
.args(&[
|
.args(&[
|
||||||
"camoufox-browser@latest",
|
"camoufox-browser@latest",
|
||||||
"tab_list",
|
"tab_list",
|
||||||
@@ -124,9 +152,8 @@ async fn trigger_daemon_start(session: &str) -> Result<(), String> {
|
|||||||
.stderr(Stdio::from(log))
|
.stderr(Stdio::from(log))
|
||||||
.output()
|
.output()
|
||||||
.await
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,12 @@ pub struct BrowserSection {
|
|||||||
/// Camofox URL for browser automation.
|
/// Camofox URL for browser automation.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub camofox_url: Option<String>,
|
pub camofox_url: Option<String>,
|
||||||
|
/// Explicit path to the npx (or npx.cmd on Windows) executable.
|
||||||
|
/// When set, `init_browser()` uses this instead of bare `"npx"`.
|
||||||
|
/// Useful when nvm / fnm / asdf manages Node — e.g.
|
||||||
|
/// `/home/user/.nvm/versions/node/v22/bin/npx`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub node_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BrowserSection {
|
impl Default for BrowserSection {
|
||||||
@@ -237,6 +243,7 @@ impl Default for BrowserSection {
|
|||||||
Self {
|
Self {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
camofox_url: None,
|
camofox_url: None,
|
||||||
|
node_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,6 +299,7 @@ pub struct AgentConfig {
|
|||||||
pub kanban: KanbanRootConfig,
|
pub kanban: KanbanRootConfig,
|
||||||
pub skills: SkillsRootConfig,
|
pub skills: SkillsRootConfig,
|
||||||
pub identity: IdentitySection,
|
pub identity: IdentitySection,
|
||||||
|
pub browser: BrowserSection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AgentConfig {
|
impl Default for AgentConfig {
|
||||||
@@ -309,6 +317,7 @@ impl Default for AgentConfig {
|
|||||||
kanban: KanbanRootConfig::default(),
|
kanban: KanbanRootConfig::default(),
|
||||||
skills: SkillsRootConfig::default(),
|
skills: SkillsRootConfig::default(),
|
||||||
identity: IdentitySection::default(),
|
identity: IdentitySection::default(),
|
||||||
|
browser: BrowserSection::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,6 +357,7 @@ impl From<GyandootConfig> for AgentConfig {
|
|||||||
kanban: KanbanRootConfig::default(),
|
kanban: KanbanRootConfig::default(),
|
||||||
skills: SkillsRootConfig::default(),
|
skills: SkillsRootConfig::default(),
|
||||||
identity: cfg.identity,
|
identity: cfg.identity,
|
||||||
|
browser: cfg.browser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ impl SubAgentWorker {
|
|||||||
kanban: gyandoot_core::KanbanRootConfig::default(),
|
kanban: gyandoot_core::KanbanRootConfig::default(),
|
||||||
skills: gyandoot_core::SkillsRootConfig::default(),
|
skills: gyandoot_core::SkillsRootConfig::default(),
|
||||||
identity: gyandoot_core::IdentitySection::default(),
|
identity: gyandoot_core::IdentitySection::default(),
|
||||||
|
browser: gyandoot_core::BrowserSection::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to create the agent loop. If the provider config isn't viable
|
// Try to create the agent loop. If the provider config isn't viable
|
||||||
|
|||||||
Reference in New Issue
Block a user