///|
/// moonclaw is the main AI coding agent that provides intelligent assistance
/// for software development tasks. It integrates various tools for file management,
/// command execution, and code analysis.
pub struct Moonclaw {
logger : @pino.Logger
agent : @agent.Agent
}
///|
fn setup_agent(
agent : @agent.Agent,
cwd~ : String,
home~ : String,
restricted_workspace~ : Bool,
web_search~ : Bool,
command_approval_scope? : String,
) -> Unit {
let file_manager = @file.manager(cwd~)
let todo_list = @todo.new(uuid=agent.uuid, cwd=agent.cwd)
if restricted_workspace {
// Flow capsules deliberately omit shell, search, OCR, and patch tools.
// Their only filesystem capabilities are scoped reads/listing and a
// traversal-rejecting writer rooted at the task capsule.
agent.add_tools([
@list_files.new(file_manager).to_agent_tool(),
@read_file.new(file_manager).to_agent_tool(),
@write_to_file.new(agent.cwd).to_agent_tool(),
])
if web_search {
agent.add_tools([
@web_search.new().to_agent_tool(),
@web_fetch.new().to_agent_tool(),
])
}
} else {
let execute_command_tool = match command_approval_scope {
Some(scope) =>
@execute_command.new(agent.cwd, home~, approval_scope=scope)
None => @execute_command.new(agent.cwd, home~)
}
agent.add_tools([
execute_command_tool.to_agent_tool(),
@list_files.new(file_manager).to_agent_tool(),
@read_file.new(file_manager).to_agent_tool(),
@todo.new_tool(todo_list).to_agent_tool(),
@search_files.new(agent.cwd).to_agent_tool(),
@unlimited_ocr.new(agent.cwd).to_agent_tool(),
])
// GPT 5.1+ uses apply_patch only, other models use meta_write_to_file
if agent.model.supports_apply_patch {
agent.add_tool(@apply_patch.new(agent.cwd))
} else {
agent.add_tool(@write_to_file.new(agent.cwd))
}
}
}
///|
/// Gives an agent bounded, read-only workspace discovery. This is intended for
/// retained Q&A and other Cowork sessions that must inspect durable artifacts
/// without acquiring shell or workspace-mutation authority.
pub fn setup_read_only_agent(agent : @agent.Agent, cwd~ : String) -> Unit {
let file_manager = @file.manager(cwd~)
agent.add_tools([
@list_files.new(file_manager).to_agent_tool(),
@read_file.new(file_manager).to_agent_tool(),
@search_files.new(cwd).to_agent_tool(),
])
}
///|
fn default_log_path() -> String {
@moonsuite.product_artifact_for_workspace_root(
@os.home() catch {
_ => "."
},
"moonclaw",
"logs/moonclaw.jsonl",
)
}
///|
/// Creates a new moonclaw agent instance.
///
/// Parameters:
/// - logger: Optional logger for recording agent activities (defaults to file-based logger)
/// - model: The AI model to use for the agent
/// - cwd: Optional working directory (defaults to current working directory)
/// - user_message: Optional initial user message to start the conversation
/// - web_search: Whether to enable web search capabilities (defaults to false)
///
/// Returns a configured moonclaw instance ready to assist with coding tasks.
#as_free_fn
pub async fn Moonclaw::new(
name? : String,
logger? : @pino.Logger = @pino.logger(
"moonclaw",
try! @pino.Transport::parse("file:\{default_log_path()}"),
),
model~ : @model.Model,
home? : String,
cwd? : String,
user_message? : String,
web_search? : Bool = false,
restricted_workspace? : Bool = false,
command_approval_scope? : String,
) -> Moonclaw {
let cwd = match cwd {
Some(cwd) => cwd
None => @os.cwd()
}
let system_prompt = [@prompt.prelude, @todo.prompt, @search_files.prompt].join(
"\n",
)
let agent = @agent.new(
name?,
model,
logger~,
home?=home.map(home => home),
cwd~,
system_message=system_prompt,
user_message?,
web_search~,
)
setup_agent(
agent,
cwd~,
home=home.unwrap_or(cwd),
restricted_workspace~,
web_search~,
command_approval_scope?,
)
{ logger, agent }
}
///|
/// Closes the moonclaw agent and releases associated resources.
/// Should be called when the agent is no longer needed (using `defer` ideally).
pub fn Moonclaw::close(self : Moonclaw) -> Unit {
self.agent.close()
}
///|
/// Starts the moonclaw agent and begins processing tasks.
///
/// Parameters:
///
/// - prompt: Optional initial prompt to send to the agent before starting
///
/// The agent will process messages from its queue and respond to user requests,
/// and stops when there are no more messages to process.
pub async fn Moonclaw::start(self : Moonclaw, prompt? : String) -> Unit {
if prompt is Some(prompt) {
let user_message = @ai.user_message(content=prompt)
self.agent.queue_message(user_message) |> ignore()
}
self.agent.start()
}
///|
/// Resumes a previously saved moonclaw agent session.
///
/// Parameters:
/// - logger: Optional logger for recording agent activities (defaults to file-based logger)
/// - model: The AI model to use for the agent
/// - cwd: Optional working directory (defaults to current working directory)
/// - id: The unique identifier of the conversation to resume
/// - user_message: Optional user message to add to the resumed conversation
/// - web_search: Whether to enable web search capabilities (defaults to false)
///
/// Returns a moonclaw instance with the conversation history restored.
/// Fails if the conversation with the given id cannot be found.
#as_free_fn
pub async fn Moonclaw::resume_(
logger? : @pino.Logger = @pino.logger(
"moonclaw",
try! @pino.Transport::parse("file:\{default_log_path()}"),
),
model~ : @model.Model,
home? : String,
id : @uuid.Uuid,
user_message? : String,
web_search? : Bool,
command_approval_scope? : String,
) -> Moonclaw {
let home = match home {
Some(home) => home
None => @os.home()
}
let uuid = @uuid.generator(@rand.chacha8())
let session_manager = @conversation.Manager::new(home~, uuid~)
guard session_manager.load(id) is Some(conversation) else {
fail("Conversation with id '\{id}' not found.")
}
// Use the conversation's web_search setting if not explicitly overridden.
let web_search = web_search.unwrap_or(conversation.web_search())
let agent = @agent.load(
model,
conversation,
logger~,
home~,
user_message?,
web_search~,
)
setup_agent(
agent,
cwd=conversation.cwd(),
home~,
restricted_workspace=false,
web_search~,
command_approval_scope?,
)
{ logger, agent }
}