@@ -67,6 +67,7 @@ use crate::util::{
6767 self ,
6868 MCP_SERVER_TOOL_DELIMITER ,
6969 directories,
70+ file_uri,
7071} ;
7172
7273pub const DEFAULT_AGENT_NAME : & str = "q_cli_default" ;
@@ -88,6 +89,12 @@ pub enum AgentConfigError {
8889 Io ( #[ from] std:: io:: Error ) ,
8990 #[ error( "Failed to parse legacy mcp config: {0}" ) ]
9091 BadLegacyMcpConfig ( #[ from] eyre:: Report ) ,
92+ #[ error( "File URI not found: {uri} (resolved to {path})" ) ]
93+ FileUriNotFound { uri : String , path : PathBuf } ,
94+ #[ error( "Failed to read file URI: {uri} (resolved to {path}): {error}" ) ]
95+ FileUriReadError { uri : String , path : PathBuf , error : std:: io:: Error } ,
96+ #[ error( "Invalid file URI format: {uri}" ) ]
97+ InvalidFileUri { uri : String } ,
9198}
9299
93100/// An [Agent] is a declarative way of configuring a given instance of q chat. Currently, it is
@@ -221,10 +228,15 @@ impl Agent {
221228 legacy_mcp_config : Option < & McpServerConfig > ,
222229 output : & mut impl Write ,
223230 ) -> Result < ( ) , AgentConfigError > {
224- let Self { mcp_servers, .. } = self ;
225-
226231 self . path = Some ( path. to_path_buf ( ) ) ;
227232
233+ // Resolve file:// URIs in the prompt field
234+ if let Some ( resolved_prompt) = self . resolve_prompt ( ) ? {
235+ self . prompt = Some ( resolved_prompt) ;
236+ }
237+
238+ let Self { mcp_servers, .. } = self ;
239+
228240 if let ( true , Some ( legacy_mcp_config) ) = ( self . use_legacy_mcp_json , legacy_mcp_config) {
229241 for ( name, legacy_server) in & legacy_mcp_config. mcp_servers {
230242 if mcp_servers. mcp_servers . contains_key ( name) {
@@ -283,6 +295,48 @@ impl Agent {
283295 Ok ( serde_json:: to_string_pretty ( & agent_clone) ?)
284296 }
285297
298+ /// Resolves the prompt field, handling file:// URIs if present.
299+ /// Returns the prompt content as-is if it doesn't start with file://,
300+ /// or resolves the file URI and returns the file content.
301+ pub fn resolve_prompt ( & self ) -> Result < Option < String > , AgentConfigError > {
302+ match & self . prompt {
303+ None => Ok ( None ) ,
304+ Some ( prompt_str) => {
305+ if prompt_str. starts_with ( "file://" ) {
306+ // Get the base path from the agent config file path
307+ let base_path = match & self . path {
308+ Some ( path) => path. parent ( ) . unwrap_or ( Path :: new ( "." ) ) ,
309+ None => Path :: new ( "." ) ,
310+ } ;
311+
312+ // Resolve the file URI
313+ match file_uri:: resolve_file_uri ( prompt_str, base_path) {
314+ Ok ( content) => Ok ( Some ( content) ) ,
315+ Err ( file_uri:: FileUriError :: InvalidUri { uri } ) => {
316+ Err ( AgentConfigError :: InvalidFileUri { uri } )
317+ }
318+ Err ( file_uri:: FileUriError :: FileNotFound { path } ) => {
319+ Err ( AgentConfigError :: FileUriNotFound {
320+ uri : prompt_str. clone ( ) ,
321+ path
322+ } )
323+ }
324+ Err ( file_uri:: FileUriError :: ReadError { path, source } ) => {
325+ Err ( AgentConfigError :: FileUriReadError {
326+ uri : prompt_str. clone ( ) ,
327+ path,
328+ error : source
329+ } )
330+ }
331+ }
332+ } else {
333+ // Return the prompt as-is for backward compatibility
334+ Ok ( Some ( prompt_str. clone ( ) ) )
335+ }
336+ }
337+ }
338+ }
339+
286340 /// Retrieves an agent by name. It does so via first seeking the given agent under local dir,
287341 /// and falling back to global dir if it does not exist in local.
288342 pub async fn get_agent_by_name ( os : & Os , agent_name : & str ) -> eyre:: Result < ( Agent , PathBuf ) > {
@@ -937,6 +991,8 @@ fn validate_agent_name(name: &str) -> eyre::Result<()> {
937991#[ cfg( test) ]
938992mod tests {
939993 use serde_json:: json;
994+ use std:: fs;
995+ use tempfile:: TempDir ;
940996
941997 use super :: * ;
942998 use crate :: cli:: agent:: hook:: Source ;
@@ -1400,4 +1456,124 @@ mod tests {
14001456 }
14011457 }
14021458 }
1459+
1460+ #[ test]
1461+ fn test_resolve_prompt_file_uri_relative ( ) {
1462+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1463+
1464+ // Create a prompt file
1465+ let prompt_content = "You are a test agent with specific instructions." ;
1466+ let prompt_file = temp_dir. path ( ) . join ( "test-prompt.md" ) ;
1467+ fs:: write ( & prompt_file, prompt_content) . unwrap ( ) ;
1468+
1469+ // Create agent config file path
1470+ let config_file = temp_dir. path ( ) . join ( "test-agent.json" ) ;
1471+
1472+ // Create agent with file:// URI prompt
1473+ let agent = Agent {
1474+ name : "test-agent" . to_string ( ) ,
1475+ prompt : Some ( "file://./test-prompt.md" . to_string ( ) ) ,
1476+ path : Some ( config_file) ,
1477+ ..Default :: default ( )
1478+ } ;
1479+
1480+ // Test resolve_prompt
1481+ let resolved = agent. resolve_prompt ( ) . unwrap ( ) ;
1482+ assert_eq ! ( resolved, Some ( prompt_content. to_string( ) ) ) ;
1483+ }
1484+
1485+ #[ test]
1486+ fn test_resolve_prompt_file_uri_absolute ( ) {
1487+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1488+
1489+ // Create a prompt file
1490+ let prompt_content = "Absolute path prompt content." ;
1491+ let prompt_file = temp_dir. path ( ) . join ( "absolute-prompt.md" ) ;
1492+ fs:: write ( & prompt_file, prompt_content) . unwrap ( ) ;
1493+
1494+ // Create agent with absolute file:// URI
1495+ let agent = Agent {
1496+ name : "test-agent" . to_string ( ) ,
1497+ prompt : Some ( format ! ( "file://{}" , prompt_file. display( ) ) ) ,
1498+ path : Some ( temp_dir. path ( ) . join ( "test-agent.json" ) ) ,
1499+ ..Default :: default ( )
1500+ } ;
1501+
1502+ // Test resolve_prompt
1503+ let resolved = agent. resolve_prompt ( ) . unwrap ( ) ;
1504+ assert_eq ! ( resolved, Some ( prompt_content. to_string( ) ) ) ;
1505+ }
1506+
1507+ #[ test]
1508+ fn test_resolve_prompt_inline_unchanged ( ) {
1509+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1510+
1511+ // Create agent with inline prompt
1512+ let inline_prompt = "This is an inline prompt." ;
1513+ let agent = Agent {
1514+ name : "test-agent" . to_string ( ) ,
1515+ prompt : Some ( inline_prompt. to_string ( ) ) ,
1516+ path : Some ( temp_dir. path ( ) . join ( "test-agent.json" ) ) ,
1517+ ..Default :: default ( )
1518+ } ;
1519+
1520+ // Test resolve_prompt
1521+ let resolved = agent. resolve_prompt ( ) . unwrap ( ) ;
1522+ assert_eq ! ( resolved, Some ( inline_prompt. to_string( ) ) ) ;
1523+ }
1524+
1525+ #[ test]
1526+ fn test_resolve_prompt_file_not_found_error ( ) {
1527+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1528+
1529+ // Create agent with non-existent file URI
1530+ let agent = Agent {
1531+ name : "test-agent" . to_string ( ) ,
1532+ prompt : Some ( "file://./nonexistent.md" . to_string ( ) ) ,
1533+ path : Some ( temp_dir. path ( ) . join ( "test-agent.json" ) ) ,
1534+ ..Default :: default ( )
1535+ } ;
1536+
1537+ // Test resolve_prompt should fail
1538+ let result = agent. resolve_prompt ( ) ;
1539+ assert ! ( result. is_err( ) ) ;
1540+
1541+ if let Err ( AgentConfigError :: FileUriNotFound { uri, .. } ) = result {
1542+ assert_eq ! ( uri, "file://./nonexistent.md" ) ;
1543+ } else {
1544+ panic ! ( "Expected FileUriNotFound error, got: {:?}" , result) ;
1545+ }
1546+ }
1547+
1548+ #[ test]
1549+ fn test_resolve_prompt_no_prompt_field ( ) {
1550+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1551+
1552+ // Create agent without prompt field
1553+ let agent = Agent {
1554+ name : "test-agent" . to_string ( ) ,
1555+ prompt : None ,
1556+ path : Some ( temp_dir. path ( ) . join ( "test-agent.json" ) ) ,
1557+ ..Default :: default ( )
1558+ } ;
1559+
1560+ // Test resolve_prompt
1561+ let resolved = agent. resolve_prompt ( ) . unwrap ( ) ;
1562+ assert_eq ! ( resolved, None ) ;
1563+ }
1564+
1565+ #[ test]
1566+ fn test_resolve_prompt_no_path_set ( ) {
1567+ // Create agent without path set (should not happen in practice)
1568+ let agent = Agent {
1569+ name : "test-agent" . to_string ( ) ,
1570+ prompt : Some ( "file://./test.md" . to_string ( ) ) ,
1571+ path : None ,
1572+ ..Default :: default ( )
1573+ } ;
1574+
1575+ // Test resolve_prompt should fail gracefully
1576+ let result = agent. resolve_prompt ( ) ;
1577+ assert ! ( result. is_err( ) ) ;
1578+ }
14031579}
0 commit comments