Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Install Linux Tauri dependencies
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev

- name: Rust cache
uses: swatinem/rust-cache@v2
with:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Friday/

Prerequisites: Node.js and Rust (for Tauri). See [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/).

For **Local CLI** mode, install the Cursor Agent CLI and ensure `cursor-agent` is on your `PATH` (or set the path in Settings → Advanced: Cursor CLI). Onboarding probes for it automatically.

For **Cursor API / Cloud** mode, add a Cursor dashboard API key (`crsr_…`) in onboarding or Settings.

```bash
npm install

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use serde_json::Value;

use crate::adapters::cursor_cli_local::cursor_parser::{CursorIntermediateEvent, CursorIntermediateKind};
use crate::adapters::cursor_cli_local::cursor_parser::{
extract_tool_name, CursorIntermediateEvent, CursorIntermediateKind,
};
use crate::core::event::{
AgentEvent, FileAction, FridaySessionStatus, MessageRole, RiskLevel, now_iso,
};
Expand Down Expand Up @@ -54,22 +56,17 @@ impl CursorEventMapper {
vec![]
}
}
CursorIntermediateKind::ToolUse => {
let tool_name = event
.payload
.get("tool")
.or_else(|| event.payload.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("tool")
.to_string();
CursorIntermediateKind::ToolUse | CursorIntermediateKind::ToolCompleted => {
let tool_name = extract_tool_name(&event.payload)
.unwrap_or_else(|| "tool".into());
let title = event
.payload
.get("title")
.and_then(|v| v.as_str())
.unwrap_or(&tool_name)
.to_string();
let status = crate::core::state_machine::status_from_tool_name(&tool_name);
vec![
let mut events = vec![
AgentEvent::ToolCall {
session_id: session_id.to_string(),
tool_name: tool_name.clone(),
Expand All @@ -83,20 +80,62 @@ impl CursorEventMapper {
message: Some(format!("Using {tool_name}")),
timestamp: now_iso(),
},
]
];
if matches!(event.kind, CursorIntermediateKind::ToolCompleted) {
if let Some(path) = extract_path(&event.payload) {
let action = if tool_name.to_lowercase().contains("delete") {
FileAction::Deleted
} else if tool_name.to_lowercase().contains("write") {
FileAction::Created
} else {
FileAction::Edited
};
events.push(AgentEvent::FileChanged {
session_id: session_id.to_string(),
path,
action,
timestamp: now_iso(),
});
}
}
events
}
CursorIntermediateKind::FileRead => {
let msg = extract_text_or_raw(&event);
vec![AgentEvent::AgentStatus {
let path = extract_path(&event.payload);
let msg = path
.clone()
.unwrap_or_else(|| extract_text_or_raw(&event));
let mut events = vec![AgentEvent::AgentStatus {
session_id: session_id.to_string(),
status: FridaySessionStatus::Reading,
message: Some(msg),
timestamp: now_iso(),
}]
}];
if let Some(path) = path {
events.insert(
0,
AgentEvent::ToolCall {
session_id: session_id.to_string(),
tool_name: "read".into(),
title: format!("Read {path}"),
args: Some(event.payload.clone()),
timestamp: now_iso(),
},
);
}
events
}
CursorIntermediateKind::FileEdit => {
let path = extract_path(&event.payload).unwrap_or_else(|| "unknown".into());
let tool_name = extract_tool_name(&event.payload).unwrap_or_else(|| "edit".into());
vec![
AgentEvent::ToolCall {
session_id: session_id.to_string(),
tool_name,
title: format!("Edit {path}"),
args: Some(event.payload.clone()),
timestamp: now_iso(),
},
AgentEvent::FileChanged {
session_id: session_id.to_string(),
path,
Expand All @@ -112,15 +151,40 @@ impl CursorEventMapper {
]
}
CursorIntermediateKind::CommandRun => {
let command = extract_text_or_raw(&event);
let command = extract_command(&event);
let risk = classify_command_risk(&command);
vec![AgentEvent::CommandStarted {
session_id: session_id.to_string(),
command: command.clone(),
cwd: None,
risk,
timestamp: now_iso(),
}]
vec![
AgentEvent::CommandStarted {
session_id: session_id.to_string(),
command: command.clone(),
cwd: extract_cwd(&event.payload),
risk,
timestamp: now_iso(),
},
AgentEvent::AgentStatus {
session_id: session_id.to_string(),
status: FridaySessionStatus::RunningCommand,
message: Some(command),
timestamp: now_iso(),
},
]
}
CursorIntermediateKind::CommandCompleted => {
let command = extract_command(&event);
vec![
AgentEvent::CommandCompleted {
session_id: session_id.to_string(),
command,
exit_code: extract_exit_code(&event.payload),
timestamp: now_iso(),
},
AgentEvent::AgentStatus {
session_id: session_id.to_string(),
status: FridaySessionStatus::Thinking,
message: Some("Command finished".into()),
timestamp: now_iso(),
},
]
}
CursorIntermediateKind::Approval => vec![AgentEvent::ApprovalRequired {
session_id: session_id.to_string(),
Expand Down Expand Up @@ -150,19 +214,27 @@ impl CursorEventMapper {
},
]
}
CursorIntermediateKind::Error => vec![
AgentEvent::SessionError {
session_id: session_id.to_string(),
error: extract_text_or_raw(&event),
timestamp: now_iso(),
},
AgentEvent::AgentStatus {
session_id: session_id.to_string(),
status: FridaySessionStatus::Error,
message: None,
timestamp: now_iso(),
},
],
CursorIntermediateKind::Error => {
let error = event
.payload
.get("result")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| extract_text_or_raw(&event));
vec![
AgentEvent::SessionError {
session_id: session_id.to_string(),
error,
timestamp: now_iso(),
},
AgentEvent::AgentStatus {
session_id: session_id.to_string(),
status: FridaySessionStatus::Error,
message: None,
timestamp: now_iso(),
},
]
}
}
}
}
Expand Down Expand Up @@ -200,6 +272,49 @@ fn extract_path(payload: &Value) -> Option<String> {
.and_then(|v| v.as_str())
.map(String::from)
})
.or_else(|| {
payload
.get("tool_call")
.and_then(|tc| tc.as_object())
.and_then(|obj| obj.values().next())
.and_then(|tool| {
tool.pointer("/args/path")
.or_else(|| tool.pointer("/result/success/path"))
})
.and_then(|v| v.as_str())
.map(String::from)
})
}

fn extract_command(event: &CursorIntermediateEvent) -> String {
if let Some(cmd) = event
.payload
.pointer("/tool_call/shellToolCall/args/command")
.or_else(|| event.payload.pointer("/tool_call/bashToolCall/args/command"))
.or_else(|| event.payload.pointer("/args/command"))
.and_then(|v| v.as_str())
{
return cmd.to_string();
}
extract_text_or_raw(event)
}

fn extract_cwd(payload: &Value) -> Option<String> {
payload
.pointer("/tool_call/shellToolCall/args/working_directory")
.or_else(|| payload.pointer("/tool_call/shellToolCall/args/cwd"))
.or_else(|| payload.pointer("/args/cwd"))
.and_then(|v| v.as_str())
.map(String::from)
}

fn extract_exit_code(payload: &Value) -> Option<i32> {
payload
.pointer("/tool_call/shellToolCall/result/success/exitCode")
.or_else(|| payload.pointer("/tool_call/shellToolCall/result/exitCode"))
.or_else(|| payload.get("exit_code"))
.and_then(|v| v.as_i64())
.map(|n| n as i32)
}

fn extract_text_or_raw(event: &CursorIntermediateEvent) -> String {
Expand Down
Loading
Loading