Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 1 addition & 30 deletions crates/cli/src/commands/launch/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ pub async fn run(opts: Options) -> Result<()> {
let mcp_config_path = write_mcp_config(&creds)?;
cmd.arg("--mcp-config").arg(&mcp_config_path);
let session_url = format!("{}/session/{}", crate::config::console_base_url(), session_id);
cmd.arg("--append-system-prompt").arg(system_prompt(&session_id, repo_origin.as_deref(), &session_url));
cmd.arg("--append-system-prompt").arg(crate::commands::launch::agent_session_prompt(&session_id, repo_origin.as_deref(), &session_url));
cmd.arg("--allowedTools").arg("mcp__edgee__setSessionName,mcp__edgee__addSessionPullRequest,mcp__edgee__addSessionCommit,mcp__edgee__setSessionGitHubRepo");
}

Expand Down Expand Up @@ -127,32 +127,3 @@ fn write_mcp_config(creds: &crate::config::Credentials) -> Result<std::path::Pat
std::fs::write(&path, serde_json::to_string_pretty(&mcp_config)?)?;
Ok(path)
}

fn system_prompt(session_id: &str, repo: Option<&str>, session_url: &str) -> String {
let mut prompt = format!(
r#"You are running inside the Edgee CLI and have access to the Edgee MCP server for tracking session metadata.

Your Edgee session ID is: {session_id}
Your Edgee public session page is: {session_url}

You MUST use the following Edgee MCP tools during this session:

1. `setSessionName` — call this immediately after the user's first message with a short descriptive name (3-6 words) summarizing what the user is asking for. Arguments:
- sessionId: "{session_id}"
- name: the descriptive name.
If at any later point during the session you come up with a clearly better name (e.g., the task's real scope becomes obvious only after exploring the code, or the user pivots the request), call `setSessionName` again with the improved name. Prefer calling it once, but do not hesitate to update when a materially better name emerges.

2. `addSessionPullRequest` — call this EVERY TIME you create OR edit a pull request (e.g., via `gh pr create`, `gh pr edit`, or any other tool). Immediately after the PR is created or modified, call this tool with:
- sessionId: "{session_id}"
- pullRequest: the full PR URL.
This is required for every PR you touch during this session, with no exceptions. Always call it on edits too — the PR may not yet be associated with this session, and the API handles duplicates safely, so redundant calls are harmless."#
);

if let Some(repo) = repo {
prompt.push_str(&format!(
"\n\n3. `setSessionGitHubRepo` — call this EXACTLY ONCE at the start of the session, together with (or right after) `setSessionName`. Arguments:\n - sessionId: \"{session_id}\"\n - repo: \"{repo}\"\n Do not call this tool again during the session."
));
}

prompt
}
36 changes: 34 additions & 2 deletions crates/cli/src/commands/launch/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@ pub async fn run(opts: Options) -> Result<()> {
crate::config::write(&creds)?;
}

// Step 3: launch codex with the correct env vars
// Step 3b: ensure MCP preference is set
crate::commands::auth::login::ensure_mcp_preference().await?;
creds = crate::config::read()?;

// Step 4: launch codex with the correct env vars
let codex = creds.codex.as_ref().unwrap();
let api_key = &codex.api_key;
let session_id = uuid::Uuid::new_v4().to_string();
crate::commands::launch::spawn_cli_version_report(&creds, &session_id);
let repo_entry = crate::git::detect_origin()
let repo_origin = crate::git::detect_origin();
let repo_entry = repo_origin
.as_ref()
.map(|url| format!(",\"x-edgee-repo\"=\"{}\"", url))
.unwrap_or_default();
let base_url = format!("{}/v1", crate::config::gateway_base_url());
Expand All @@ -51,6 +57,32 @@ pub async fn run(opts: Options) -> Result<()> {
"-c", &format!("model_providers.edgee-cli.http_headers={{\"x-edgee-api-key\"=\"{api_key}\",\"x-edgee-session-id\"=\"{session_id}\"{repo_entry}}}"),
"-c", "model_providers.edgee-cli.wire_api=\"responses\"",
]);

// Step 5: conditionally set up MCP integration
let use_mcp = creds.enable_mcp.unwrap_or(false);
let user_token = creds.user_token.as_deref().unwrap_or("");
if use_mcp && !user_token.is_empty() {
cmd.env("EDGEE_USER_TOKEN", user_token);
let session_url = format!("{}/session/{}", crate::config::console_base_url(), session_id);
let prompt = crate::commands::launch::agent_session_prompt(
&session_id,
repo_origin.as_deref(),
&session_url,
);
let escaped_prompt = crate::commands::launch::toml_escape_string(&prompt);
cmd.args([
"-c",
&format!(
"mcp_servers.edgee.url=\"{}\"",
crate::config::mcp_base_url()
),
"-c",
"mcp_servers.edgee.bearer_token_env_var=\"EDGEE_USER_TOKEN\"",
"-c",
&format!("developer_instructions={escaped_prompt}"),
]);
}

cmd.args(&opts.args);

let status = cmd.status().map_err(|e| {
Expand Down
106 changes: 106 additions & 0 deletions crates/cli/src/commands/launch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,67 @@ async fn fetch_stats(
client.get_session_stats(org_id, session_id).await
}

/// Build the per-session "agent instructions" prompt that tells the model to
/// call the Edgee MCP tools (`setSessionName`, `addSessionPullRequest`,
/// optionally `setSessionGitHubRepo`).
///
/// Shared by every `edgee launch <agent>` that wires Edgee MCP. The text
/// references only the bare MCP tool names, so it works regardless of how the
/// host agent namespaces them.
pub fn agent_session_prompt(session_id: &str, repo: Option<&str>, session_url: &str) -> String {
let mut prompt = format!(
r#"You are running inside the Edgee CLI and have access to the Edgee MCP server for tracking session metadata.

Your Edgee session ID is: {session_id}
Your Edgee public session page is: {session_url}

You MUST use the following Edgee MCP tools during this session:

1. `setSessionName` — call this immediately after the user's first message with a short descriptive name (3-6 words) summarizing what the user is asking for. Arguments:
- sessionId: "{session_id}"
- name: the descriptive name.
If at any later point during the session you come up with a clearly better name (e.g., the task's real scope becomes obvious only after exploring the code, or the user pivots the request), call `setSessionName` again with the improved name. Prefer calling it once, but do not hesitate to update when a materially better name emerges.

2. `addSessionPullRequest` — call this EVERY TIME you create OR edit a pull request (e.g., via `gh pr create`, `gh pr edit`, or any other tool). Immediately after the PR is created or modified, call this tool with:
- sessionId: "{session_id}"
- pullRequest: the full PR URL.
This is required for every PR you touch during this session, with no exceptions. Always call it on edits too — the PR may not yet be associated with this session, and the API handles duplicates safely, so redundant calls are harmless."#
);

if let Some(repo) = repo {
prompt.push_str(&format!(
"\n\n3. `setSessionGitHubRepo` — call this EXACTLY ONCE at the start of the session, together with (or right after) `setSessionName`. Arguments:\n - sessionId: \"{session_id}\"\n - repo: \"{repo}\"\n Do not call this tool again during the session."
));
}

prompt
}

/// Wrap `s` as a TOML basic string literal (including surrounding `"`), escaping
/// the characters TOML requires per the spec. Used to embed multi-line prompts
/// into Codex's `-c key=value` overrides without TOML parse errors.
pub fn toml_escape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str(r"\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str(r"\n"),
'\r' => out.push_str(r"\r"),
'\t' => out.push_str(r"\t"),
'\x08' => out.push_str(r"\b"),
'\x0c' => out.push_str(r"\f"),
c if (c as u32) < 0x20 || c as u32 == 0x7F => {
out.push_str(&format!("\\u{:04X}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}

/// Fire-and-forget: record the running CLI version on the session metadata.
///
/// No-op when the active profile has no user token or no selected org. All
Expand Down Expand Up @@ -439,3 +500,48 @@ pub fn spawn_cli_version_report(creds: &crate::config::Credentials, session_id:
}
});
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn toml_escape_round_trip_through_parser() {
let prompt = agent_session_prompt(
"abc-123",
Some("git@github.com:owner/repo.git"),
"https://www.edgee.ai/session/abc-123",
);
let escaped = toml_escape_string(&prompt);
let doc = format!("value = {escaped}");
let parsed: toml::Value =
toml::from_str(&doc).expect("escaped TOML literal must parse cleanly");
let round_tripped = parsed
.get("value")
.and_then(|v| v.as_str())
.expect("value must be a string");
assert_eq!(round_tripped, prompt);
}

#[test]
fn toml_escape_handles_quotes_backslashes_and_control_chars() {
let raw = "He said \"hi\"\nback\\slash\ttab\x07bell";
let escaped = toml_escape_string(raw);
let doc = format!("value = {escaped}");
let parsed: toml::Value = toml::from_str(&doc).expect("must parse");
assert_eq!(parsed.get("value").and_then(|v| v.as_str()).unwrap(), raw);
}

#[test]
fn agent_prompt_omits_repo_block_when_absent() {
let p = agent_session_prompt("sid", None, "https://example/sid");
assert!(!p.contains("setSessionGitHubRepo"));
}

#[test]
fn agent_prompt_includes_repo_block_when_present() {
let p = agent_session_prompt("sid", Some("owner/repo"), "https://example/sid");
assert!(p.contains("setSessionGitHubRepo"));
assert!(p.contains("owner/repo"));
}
}
Loading