Skip to content

Commit c087803

Browse files
committed
integrate 'Morph Fast Apply' and ripgrep; limit recursion loop
1 parent e3d4b4b commit c087803

11 files changed

Lines changed: 610 additions & 32 deletions

File tree

README.md

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,22 @@ An interactive AI coding assistant powered by Claude that can write code, edit f
66

77
- **Interactive REPL** - Multi-turn conversations with Claude
88
- **File Operations** - Read, write, list, and create files/directories (sandboxed to current directory)
9+
- **Ultra-Fast Editing** - Optional Morph Apply integration (10,500+ tokens/sec, 96-98% accuracy)
10+
- **Code Search** - Fast regex-based code search using ripgrep (optional)
911
- **Web Search** - Real-time information via DuckDuckGo
1012
- **Secure** - All file operations restricted to workspace, prevents directory traversal
1113

1214
## Installation
1315

14-
**Requirements:** Rust 1.70+, Anthropic API key ([get one](https://console.anthropic.com/))
16+
**Requirements:**
17+
18+
- Rust 1.70+
19+
- Anthropic API key ([get one](https://console.anthropic.com/))
20+
21+
**Optional:**
22+
23+
- `ripgrep` for code search ([install guide](https://github.com/BurntSushi/ripgrep#installation))
24+
- Morph API key ([get one](https://morphllm.com/))
1525

1626
```bash
1727
git clone https://github.com/alexylon/sofos-code.git
@@ -21,15 +31,28 @@ cargo install --path .
2131

2232
## Usage
2333

34+
### Set API key
35+
2436
```bash
25-
# Set API key
2637
export ANTHROPIC_API_KEY='your-api-key'
38+
```
39+
40+
### Enable Ultra-Fast Editing (Optional). When enabled, Claude can use the `edit_file_fast` tool for lightning-fast, accurate code modifications.
41+
42+
```bash
43+
export MORPH_API_KEY='your-morph-key'
44+
```
2745

28-
# Start interactive mode
46+
### Start interactive mode
47+
48+
```bash
2949
sofos
50+
```
3051

31-
# One-shot mode
32-
sofos --prompt "Create a hello world Python script"
52+
### One-shot mode
53+
54+
```bash
55+
sofos --prompt "Create a hello world Rust program"
3356
```
3457

3558
### Commands
@@ -41,21 +64,28 @@ sofos --prompt "Create a hello world Python script"
4164
### Options
4265

4366
```
44-
--api-key <KEY> API key (overrides env var)
45-
-p, --prompt <TEXT> One-shot mode
46-
--model <MODEL> Claude model (default: claude-sonnet-4-5)
47-
--max-tokens <N> Max response tokens (default: 4096)
48-
-v, --verbose Verbose logging
67+
--api-key <KEY> Anthropic API key (overrides ANTHROPIC_API_KEY)
68+
--morph-api-key <KEY> Morph API key (overrides MORPH_API_KEY)
69+
-p, --prompt <TEXT> One-shot mode
70+
--model <MODEL> Claude model (default: claude-sonnet-4-5)
71+
--morph-model <MODEL> Morph model (default: morph-v3-fast)
72+
--max-tokens <N> Max response tokens (default: 4096)
73+
-v, --verbose Verbose logging
4974
```
5075

5176
## Available Tools
5277

5378
Claude can automatically use these tools:
5479

80+
**File Operations:**
5581
- `read_file` - Read file contents
5682
- `write_file` - Create or overwrite files
83+
- `edit_file_fast` - Ultra-fast code editing (requires MORPH_API_KEY)
5784
- `list_directory` - List directory contents
5885
- `create_directory` - Create directories
86+
87+
**Code & Search:**
88+
- `search_code` - Fast regex-based code search (requires `ripgrep`)
5989
- `web_search` - Search the internet
6090

6191
## Security
@@ -94,9 +124,18 @@ RUST_LOG=debug sofos
94124

95125
MIT License
96126

127+
## Morph Integration Status
128+
129+
Sofos integrates with Morph's APIs for enhanced performance:
130+
131+
**Morph Apply** - Ultra-fast code editing (10,500+ tokens/sec, 96-98% accuracy)
132+
- Direct REST API integration
133+
- Works with `edit_file_fast` tool
134+
- Optional - enable with `MORPH_API_KEY`
135+
97136
## Acknowledgments
98137

99-
Built with Rust and powered by Anthropic's Claude. Inspired by Aider and similar tools.
138+
Built with Rust and powered by Anthropic's Claude. Morph Apply integration for fast edits. Inspired by Aider and similar tools.
100139

101140
---
102141

src/api/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod client;
2+
pub mod morph;
23
pub mod types;
34

45
pub use client::AnthropicClient;
6+
pub use morph::MorphClient;
57
pub use types::*;

src/api/morph.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use crate::error::{Result, SofosError};
2+
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
3+
use serde::{Deserialize, Serialize};
4+
5+
const MORPH_BASE_URL: &str = "https://api.morphllm.com/v1";
6+
7+
#[derive(Debug, Serialize)]
8+
struct MorphMessage {
9+
role: String,
10+
content: String,
11+
}
12+
13+
#[derive(Debug, Serialize)]
14+
struct MorphRequest {
15+
model: String,
16+
messages: Vec<MorphMessage>,
17+
}
18+
19+
#[derive(Debug, Deserialize)]
20+
struct MorphResponse {
21+
choices: Vec<MorphChoice>,
22+
}
23+
24+
#[derive(Debug, Deserialize)]
25+
struct MorphChoice {
26+
message: MorphMessageResponse,
27+
}
28+
29+
#[derive(Debug, Deserialize)]
30+
struct MorphMessageResponse {
31+
content: String,
32+
}
33+
34+
pub struct MorphClient {
35+
client: reqwest::Client,
36+
model: String,
37+
}
38+
39+
impl MorphClient {
40+
pub fn new(api_key: String, model: Option<String>) -> Result<Self> {
41+
let mut headers = HeaderMap::new();
42+
headers.insert(
43+
"Authorization",
44+
HeaderValue::from_str(&format!("Bearer {}", api_key))
45+
.map_err(|e| SofosError::Config(format!("Invalid Morph API key: {}", e)))?,
46+
);
47+
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
48+
49+
let client = reqwest::Client::builder()
50+
.default_headers(headers)
51+
.build()
52+
.map_err(|e| SofosError::Config(format!("Failed to create HTTP client: {}", e)))?;
53+
54+
Ok(Self {
55+
client,
56+
model: model.unwrap_or_else(|| "morph-v3-fast".to_string()),
57+
})
58+
}
59+
60+
/// Apply code edits using Morph's fast apply API
61+
///
62+
/// Format requirements:
63+
/// - instruction: Brief first-person description of changes
64+
/// - original_code: Complete original code
65+
/// - code_edit: Updated code with "// ... existing code ..." markers for unchanged sections
66+
pub async fn apply_edit(
67+
&self,
68+
instruction: &str,
69+
original_code: &str,
70+
code_edit: &str,
71+
) -> Result<String> {
72+
let content = format!(
73+
"<instruction>{}</instruction>\n<code>{}</code>\n<update>{}</update>",
74+
instruction, original_code, code_edit
75+
);
76+
77+
let request = MorphRequest {
78+
model: self.model.clone(),
79+
messages: vec![MorphMessage {
80+
role: "user".to_string(),
81+
content,
82+
}],
83+
};
84+
85+
let url = format!("{}/chat/completions", MORPH_BASE_URL);
86+
87+
let response = self
88+
.client
89+
.post(&url)
90+
.json(&request)
91+
.send()
92+
.await?;
93+
94+
if !response.status().is_success() {
95+
let status = response.status();
96+
let error_text = response.text().await.unwrap_or_default();
97+
return Err(SofosError::Api(format!(
98+
"Morph API request failed with status {}: {}",
99+
status, error_text
100+
)));
101+
}
102+
103+
let result: MorphResponse = response.json().await?;
104+
105+
Ok(result
106+
.choices
107+
.first()
108+
.map(|c| c.message.content.clone())
109+
.ok_or_else(|| SofosError::Api("No response from Morph API".to_string()))?)
110+
}
111+
}
112+
113+
#[cfg(test)]
114+
mod tests {
115+
use super::*;
116+
117+
#[test]
118+
fn test_morph_client_creation() {
119+
let client = MorphClient::new("test-key".to_string(), None);
120+
assert!(client.is_ok());
121+
}
122+
}

src/cli.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ pub struct Cli {
1111
#[arg(long, env = "ANTHROPIC_API_KEY")]
1212
pub api_key: Option<String>,
1313

14+
#[arg(long, env = "MORPH_API_KEY")]
15+
pub morph_api_key: Option<String>,
16+
1417
/// Initial prompt to send (if not provided, starts interactive REPL)
1518
#[arg(short, long)]
1619
pub prompt: Option<String>,
1720

1821
#[arg(long, default_value = "claude-sonnet-4-5")]
1922
pub model: String,
2023

24+
#[arg(long, default_value = "morph-v3-fast")]
25+
pub morph_model: String,
26+
2127
#[arg(long, default_value = "4096")]
2228
pub max_tokens: u32,
2329

src/conversation.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,49 @@ pub struct ConversationHistory {
88

99
impl ConversationHistory {
1010
pub fn new() -> Self {
11-
let system_prompt = r#"You are Sofos, an AI coding assistant. You have access to tools that allow you to:
12-
1. Read files in the current project directory
13-
2. Write/create files in the current project directory
14-
3. List directory contents
15-
4. Create directories
16-
5. Search the web for information
11+
Self::with_features(false, false)
12+
}
13+
14+
pub fn with_features(has_morph: bool, has_code_search: bool) -> Self {
15+
let mut features = vec![
16+
"1. Read files in the current project directory",
17+
"2. Write/create files in the current project directory",
18+
"3. List directory contents",
19+
"4. Create directories",
20+
"5. Search the web for information",
21+
];
22+
23+
if has_code_search {
24+
features.push("6. Search code using ripgrep");
25+
}
26+
27+
let edit_instruction = if has_morph {
28+
"- When creating new files, use the write_file tool\n- When editing existing files, ALWAYS use the edit_file_fast tool (ultra-fast, 10,500+ tokens/sec)"
29+
} else {
30+
"- When creating or editing code, use the write_file tool"
31+
};
32+
33+
let system_prompt = format!(
34+
r#"You are Sofos, an AI coding assistant. You have access to tools that allow you to:
35+
{}
1736
1837
IMPORTANT: All file operations are restricted to the current working directory for security. You cannot access files outside this directory.
1938
2039
When helping users:
2140
- Be concise and practical
2241
- Use your tools to read files before suggesting changes
23-
- When creating or editing code, use the write_file tool
42+
{}
2443
- Search the web when you need current information or documentation
2544
- Explain your reasoning when using tools
2645
27-
Your goal is to help users with coding tasks efficiently and accurately."#;
46+
Your goal is to help users with coding tasks efficiently and accurately."#,
47+
features.join("\n"),
48+
edit_instruction
49+
);
2850

2951
Self {
3052
messages: Vec::new(),
31-
system_prompt: system_prompt.to_string(),
53+
system_prompt,
3254
}
3355
}
3456

src/main.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod error;
55
mod repl;
66
mod tools;
77

8+
use api::MorphClient;
89
use clap::Parser;
910
use cli::Cli;
1011
use colored::Colorize;
@@ -43,9 +44,23 @@ fn main() -> Result<()> {
4344
"Workspace:".bright_cyan(),
4445
workspace.display().to_string().dimmed()
4546
);
47+
48+
let morph_client = cli.morph_api_key.as_ref().and_then(|key| {
49+
match MorphClient::new(key.clone(), Some(cli.morph_model.clone())) {
50+
Ok(client) => {
51+
println!("{}", "Morph Fast Apply: Enabled".bright_green());
52+
Some(client)
53+
}
54+
Err(e) => {
55+
eprintln!("{} Failed to initialize Morph client: {}", "Warning:".bright_yellow(), e);
56+
None
57+
}
58+
}
59+
});
60+
4661
println!();
4762

48-
let mut repl = Repl::new(api_key, cli.model, cli.max_tokens, workspace)?;
63+
let mut repl = Repl::new(api_key, cli.model, cli.max_tokens, workspace, morph_client)?;
4964

5065
if let Some(prompt) = cli.prompt {
5166
repl.process_single_prompt(&prompt)?;

0 commit comments

Comments
 (0)