@masyv/relay 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -90
- package/core/Cargo.toml +5 -2
- package/core/src/diff.rs +118 -0
- package/core/src/handoff/mod.rs +2 -0
- package/core/src/handoff/templates.rs +74 -0
- package/core/src/history.rs +107 -0
- package/core/src/lib.rs +4 -0
- package/core/src/main.rs +258 -209
- package/core/src/resume.rs +202 -0
- package/core/src/tui.rs +324 -0
- package/package.json +1 -1
package/core/src/main.rs
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
2
|
use clap::{Parser, Subcommand};
|
|
3
|
-
use colored::Colorize;
|
|
4
3
|
use std::path::PathBuf;
|
|
5
4
|
|
|
6
|
-
use relay::{agents, capture, handoff, Config};
|
|
5
|
+
use relay::{agents, capture, handoff, tui, Config};
|
|
7
6
|
|
|
8
7
|
#[derive(Parser)]
|
|
9
8
|
#[command(
|
|
10
9
|
name = "relay",
|
|
11
10
|
about = "Relay — When Claude's rate limit hits, another agent picks up where you left off.",
|
|
12
|
-
long_about = "Captures your Claude Code session state (task, todos, git diff, decisions,\nerrors) and hands it off to Codex, Gemini, Ollama, or GPT-4 — so your\nwork never stops.",
|
|
13
11
|
version
|
|
14
12
|
)]
|
|
15
13
|
struct Cli {
|
|
16
14
|
#[command(subcommand)]
|
|
17
15
|
command: Commands,
|
|
18
16
|
|
|
19
|
-
/// Output as JSON
|
|
17
|
+
/// Output as JSON (no TUI)
|
|
20
18
|
#[arg(long, global = true)]
|
|
21
19
|
json: bool,
|
|
22
20
|
|
|
@@ -31,17 +29,17 @@ struct Cli {
|
|
|
31
29
|
|
|
32
30
|
#[derive(Subcommand)]
|
|
33
31
|
enum Commands {
|
|
34
|
-
/// Hand off current session to a fallback agent
|
|
32
|
+
/// Hand off current session to a fallback agent
|
|
35
33
|
Handoff {
|
|
36
|
-
///
|
|
34
|
+
/// Target agent (codex, claude, aider, gemini, copilot, opencode, ollama, openai)
|
|
37
35
|
#[arg(long)]
|
|
38
36
|
to: Option<String>,
|
|
39
37
|
|
|
40
|
-
/// Set deadline urgency (e.g. "7pm", "
|
|
38
|
+
/// Set deadline urgency (e.g. "7pm", "30min")
|
|
41
39
|
#[arg(long)]
|
|
42
40
|
deadline: Option<String>,
|
|
43
41
|
|
|
44
|
-
///
|
|
42
|
+
/// Just print the handoff — don't launch agent
|
|
45
43
|
#[arg(long)]
|
|
46
44
|
dry_run: bool,
|
|
47
45
|
|
|
@@ -52,20 +50,40 @@ enum Commands {
|
|
|
52
50
|
/// What to include: all, conversation, git, todos (comma-separated)
|
|
53
51
|
#[arg(long, default_value = "all")]
|
|
54
52
|
include: String,
|
|
53
|
+
|
|
54
|
+
/// Copy handoff to clipboard instead of launching agent
|
|
55
|
+
#[arg(long)]
|
|
56
|
+
clipboard: bool,
|
|
57
|
+
|
|
58
|
+
/// Handoff template: full (default), minimal, raw
|
|
59
|
+
#[arg(long, default_value = "full")]
|
|
60
|
+
template: String,
|
|
55
61
|
},
|
|
56
62
|
|
|
57
|
-
/// Show current session snapshot
|
|
63
|
+
/// Show current session snapshot
|
|
58
64
|
Status,
|
|
59
65
|
|
|
60
|
-
/// List configured agents and
|
|
66
|
+
/// List configured agents and availability
|
|
61
67
|
Agents,
|
|
62
68
|
|
|
63
|
-
///
|
|
69
|
+
/// Resume after rate limit resets — show what happened during handoff
|
|
70
|
+
Resume,
|
|
71
|
+
|
|
72
|
+
/// List past handoffs
|
|
73
|
+
History {
|
|
74
|
+
/// Number of entries to show
|
|
75
|
+
#[arg(long, default_value = "10")]
|
|
76
|
+
limit: usize,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/// Show what changed since the last handoff
|
|
80
|
+
Diff,
|
|
81
|
+
|
|
82
|
+
/// Generate default config at ~/.relay/config.toml
|
|
64
83
|
Init,
|
|
65
84
|
|
|
66
|
-
/// PostToolUse hook
|
|
85
|
+
/// PostToolUse hook (auto-detect rate limits)
|
|
67
86
|
Hook {
|
|
68
|
-
/// Session ID
|
|
69
87
|
#[arg(long, default_value = "unknown")]
|
|
70
88
|
session: String,
|
|
71
89
|
},
|
|
@@ -92,286 +110,317 @@ fn main() -> Result<()> {
|
|
|
92
110
|
});
|
|
93
111
|
|
|
94
112
|
match cli.command {
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════
|
|
114
|
+
// HANDOFF
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════
|
|
116
|
+
Commands::Handoff { to, deadline, dry_run, turns, include, clipboard, template } => {
|
|
117
|
+
if !cli.json {
|
|
118
|
+
tui::print_banner();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Step 1: Capture
|
|
122
|
+
let sp = if !cli.json { Some(tui::step(1, 3, "Capturing session state...")) } else { None };
|
|
97
123
|
|
|
98
|
-
// Set conversation turn limit before capture
|
|
99
124
|
relay::capture::session::MAX_CONVERSATION_TURNS
|
|
100
125
|
.store(turns, std::sync::atomic::Ordering::Relaxed);
|
|
101
126
|
|
|
102
|
-
let mut snapshot = capture::capture_snapshot(
|
|
103
|
-
&project_dir,
|
|
104
|
-
deadline.as_deref(),
|
|
105
|
-
)?;
|
|
127
|
+
let mut snapshot = capture::capture_snapshot(&project_dir, deadline.as_deref())?;
|
|
106
128
|
|
|
107
|
-
//
|
|
129
|
+
// Apply include filter
|
|
108
130
|
let includes: Vec<&str> = include.split(',').map(|s| s.trim()).collect();
|
|
109
131
|
if !includes.contains(&"all") {
|
|
110
|
-
if !includes.contains(&"conversation") {
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
if !includes.contains(&"git") {
|
|
114
|
-
snapshot.git_state = None;
|
|
115
|
-
snapshot.recent_files.clear();
|
|
116
|
-
}
|
|
117
|
-
if !includes.contains(&"todos") {
|
|
118
|
-
snapshot.todos.clear();
|
|
119
|
-
}
|
|
132
|
+
if !includes.contains(&"conversation") { snapshot.conversation.clear(); }
|
|
133
|
+
if !includes.contains(&"git") { snapshot.git_state = None; snapshot.recent_files.clear(); }
|
|
134
|
+
if !includes.contains(&"todos") { snapshot.todos.clear(); }
|
|
120
135
|
}
|
|
121
136
|
|
|
122
|
-
let
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
if let Some(sp) = sp { sp.finish_with_message("Session captured"); }
|
|
138
|
+
|
|
139
|
+
// Step 2: Build handoff
|
|
140
|
+
let sp = if !cli.json { Some(tui::step(2, 3, "Building handoff package...")) } else { None };
|
|
141
|
+
|
|
142
|
+
// Resolve target agent
|
|
143
|
+
let target_name = if let Some(ref name) = to {
|
|
144
|
+
name.clone()
|
|
145
|
+
} else if !cli.json && !dry_run {
|
|
146
|
+
// Interactive agent selection
|
|
147
|
+
if let Some(sp) = sp.as_ref() { sp.finish_with_message("Handoff built"); }
|
|
148
|
+
|
|
149
|
+
let statuses = agents::check_all_agents(&config);
|
|
150
|
+
let agent_list: Vec<(String, bool, String)> = statuses
|
|
151
|
+
.iter()
|
|
152
|
+
.map(|s| (s.name.clone(), s.available, s.reason.clone()))
|
|
153
|
+
.collect();
|
|
154
|
+
|
|
155
|
+
match tui::select_agent(&agent_list) {
|
|
156
|
+
Some(name) => name,
|
|
157
|
+
None => {
|
|
158
|
+
eprintln!(" No agent selected.");
|
|
159
|
+
return Ok(());
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
"auto".into()
|
|
164
|
+
};
|
|
128
165
|
|
|
129
|
-
//
|
|
166
|
+
// Build handoff using selected template
|
|
167
|
+
let handoff_text = match handoff::templates::Template::from_str(&template) {
|
|
168
|
+
handoff::templates::Template::Minimal => {
|
|
169
|
+
handoff::templates::build_minimal(&snapshot, &target_name)
|
|
170
|
+
}
|
|
171
|
+
handoff::templates::Template::Raw => {
|
|
172
|
+
handoff::templates::build_raw(&snapshot)
|
|
173
|
+
}
|
|
174
|
+
handoff::templates::Template::Full => {
|
|
175
|
+
handoff::build_handoff(&snapshot, &target_name, config.general.max_context_tokens)?
|
|
176
|
+
}
|
|
177
|
+
};
|
|
130
178
|
let handoff_path = handoff::save_handoff(&handoff_text, &project_dir)?;
|
|
131
179
|
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
180
|
+
if let Some(sp) = sp { sp.finish_with_message("Handoff built"); }
|
|
181
|
+
|
|
182
|
+
// JSON / dry-run output
|
|
183
|
+
if cli.json {
|
|
184
|
+
println!("{}", serde_json::to_string_pretty(&serde_json::json!({
|
|
185
|
+
"snapshot": snapshot,
|
|
186
|
+
"handoff_text": handoff_text,
|
|
187
|
+
"handoff_file": handoff_path.to_string_lossy(),
|
|
188
|
+
"target_agent": target_name,
|
|
189
|
+
}))?);
|
|
190
|
+
return Ok(());
|
|
191
|
+
}
|
|
192
|
+
// Clipboard mode
|
|
193
|
+
if clipboard {
|
|
194
|
+
#[cfg(target_os = "macos")]
|
|
195
|
+
{
|
|
196
|
+
use std::process::{Command, Stdio};
|
|
197
|
+
let mut child = Command::new("pbcopy")
|
|
198
|
+
.stdin(Stdio::piped())
|
|
199
|
+
.spawn()?;
|
|
200
|
+
if let Some(mut stdin) = child.stdin.take() {
|
|
201
|
+
use std::io::Write;
|
|
202
|
+
stdin.write_all(handoff_text.as_bytes())?;
|
|
203
|
+
}
|
|
204
|
+
child.wait()?;
|
|
205
|
+
eprintln!(" 📋 Handoff copied to clipboard!");
|
|
206
|
+
eprintln!(" 📄 Also saved: {}", handoff_path.display());
|
|
207
|
+
}
|
|
208
|
+
#[cfg(not(target_os = "macos"))]
|
|
209
|
+
{
|
|
210
|
+
eprintln!(" Clipboard not supported on this platform.");
|
|
211
|
+
eprintln!(" 📄 Saved to: {}", handoff_path.display());
|
|
145
212
|
}
|
|
146
213
|
return Ok(());
|
|
147
214
|
}
|
|
215
|
+
if dry_run {
|
|
216
|
+
println!("{handoff_text}");
|
|
217
|
+
eprintln!();
|
|
218
|
+
eprintln!(" 📄 Saved: {}", handoff_path.display());
|
|
219
|
+
return Ok(());
|
|
220
|
+
}
|
|
148
221
|
|
|
149
|
-
|
|
150
|
-
|
|
222
|
+
// Step 3: Launch agent
|
|
223
|
+
let sp = tui::step(3, 3, &format!("Launching {}...", target_name));
|
|
151
224
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
agents::handoff_to_named(&config, agent_name, &handoff_text, &project_dir.to_string_lossy())
|
|
225
|
+
let result = if to.is_some() {
|
|
226
|
+
agents::handoff_to_named(&config, &target_name, &handoff_text, &project_dir.to_string_lossy())
|
|
155
227
|
} else {
|
|
156
228
|
agents::handoff_to_first_available(&config, &handoff_text, &project_dir.to_string_lossy())
|
|
157
229
|
}?;
|
|
158
230
|
|
|
231
|
+
sp.finish_with_message(if result.success {
|
|
232
|
+
format!("{} launched", target_name)
|
|
233
|
+
} else {
|
|
234
|
+
"Failed".into()
|
|
235
|
+
});
|
|
236
|
+
|
|
159
237
|
if result.success {
|
|
160
|
-
|
|
161
|
-
eprintln!(" {}", result.message);
|
|
238
|
+
tui::print_handoff_success(&result.agent, &handoff_path.to_string_lossy());
|
|
162
239
|
} else {
|
|
163
|
-
|
|
164
|
-
eprintln!();
|
|
165
|
-
eprintln!("💡 The handoff context was saved to:");
|
|
166
|
-
eprintln!(" {}", handoff_path.display());
|
|
167
|
-
eprintln!(" You can copy-paste it into any AI assistant manually.");
|
|
240
|
+
tui::print_handoff_fail(&result.message, &handoff_path.to_string_lossy());
|
|
168
241
|
}
|
|
169
242
|
}
|
|
170
243
|
|
|
244
|
+
// ═══════════════════════════════════════════════════════════════
|
|
245
|
+
// STATUS
|
|
246
|
+
// ═══════════════════════════════════════════════════════════════
|
|
171
247
|
Commands::Status => {
|
|
248
|
+
let sp = if !cli.json { Some(tui::spinner("Reading session state...")) } else { None };
|
|
172
249
|
let snapshot = capture::capture_snapshot(&project_dir, None)?;
|
|
250
|
+
if let Some(sp) = sp { sp.finish_and_clear(); }
|
|
173
251
|
|
|
174
252
|
if cli.json {
|
|
175
253
|
println!("{}", serde_json::to_string_pretty(&snapshot)?);
|
|
176
|
-
|
|
254
|
+
} else {
|
|
255
|
+
tui::print_snapshot(&snapshot);
|
|
177
256
|
}
|
|
257
|
+
}
|
|
178
258
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
println!(" {}", snapshot.current_task);
|
|
187
|
-
println!();
|
|
188
|
-
|
|
189
|
-
if !snapshot.todos.is_empty() {
|
|
190
|
-
println!("{}", "── Todos ──".cyan());
|
|
191
|
-
for t in &snapshot.todos {
|
|
192
|
-
let icon = match t.status.as_str() {
|
|
193
|
-
"completed" => "✅",
|
|
194
|
-
"in_progress" => "🔄",
|
|
195
|
-
_ => "⏳",
|
|
196
|
-
};
|
|
197
|
-
println!(" {icon} [{}] {}", t.status, t.content);
|
|
198
|
-
}
|
|
199
|
-
println!();
|
|
200
|
-
}
|
|
259
|
+
// ═══════════════════════════════════════════════════════════════
|
|
260
|
+
// AGENTS
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════
|
|
262
|
+
Commands::Agents => {
|
|
263
|
+
let sp = if !cli.json { Some(tui::spinner("Checking agents...")) } else { None };
|
|
264
|
+
let statuses = agents::check_all_agents(&config);
|
|
265
|
+
if let Some(sp) = sp { sp.finish_and_clear(); }
|
|
201
266
|
|
|
202
|
-
if
|
|
203
|
-
println!("{}",
|
|
204
|
-
|
|
205
|
-
|
|
267
|
+
if cli.json {
|
|
268
|
+
println!("{}", serde_json::to_string_pretty(&statuses)?);
|
|
269
|
+
} else {
|
|
270
|
+
tui::print_agents(&config.general.priority, &statuses);
|
|
206
271
|
}
|
|
272
|
+
}
|
|
207
273
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
println!();
|
|
214
|
-
}
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════
|
|
275
|
+
// RESUME
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════
|
|
277
|
+
Commands::Resume => {
|
|
278
|
+
let report = relay::resume::build_resume(&project_dir)?;
|
|
215
279
|
|
|
216
|
-
if
|
|
217
|
-
println!("{}",
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
280
|
+
if cli.json {
|
|
281
|
+
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
282
|
+
} else {
|
|
283
|
+
tui::print_section("🔄", "Resume — What Happened During Handoff");
|
|
284
|
+
eprintln!(" Handoff at: {}", report.handoff_time);
|
|
285
|
+
eprintln!(" Task: {}", report.original_task);
|
|
286
|
+
eprintln!();
|
|
287
|
+
|
|
288
|
+
if !report.new_commits.is_empty() {
|
|
289
|
+
tui::print_section("📝", &format!("New Commits ({})", report.new_commits.len()));
|
|
290
|
+
for c in &report.new_commits {
|
|
291
|
+
eprintln!(" {c}");
|
|
224
292
|
}
|
|
225
293
|
}
|
|
226
|
-
println!();
|
|
227
|
-
}
|
|
228
294
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
295
|
+
if !report.changes_since.is_empty() {
|
|
296
|
+
tui::print_section("📄", &format!("Changed Files ({})", report.changes_since.len()));
|
|
297
|
+
for f in &report.changes_since {
|
|
298
|
+
eprintln!(" {f}");
|
|
299
|
+
}
|
|
233
300
|
}
|
|
234
|
-
println!();
|
|
235
|
-
}
|
|
236
301
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
let prefix = match turn.role.as_str() {
|
|
243
|
-
"user" => "👤 USER".to_string(),
|
|
244
|
-
"assistant" => "🤖 CLAUDE".to_string(),
|
|
245
|
-
"assistant_tool" => "🔧 TOOL".to_string(),
|
|
246
|
-
"tool_result" => "📤 RESULT".to_string(),
|
|
247
|
-
_ => turn.role.clone(),
|
|
248
|
-
};
|
|
249
|
-
let content = if turn.content.len() > 120 {
|
|
250
|
-
format!("{}...", &turn.content[..117])
|
|
251
|
-
} else {
|
|
252
|
-
turn.content.clone()
|
|
253
|
-
};
|
|
254
|
-
println!(" {}: {}", prefix, content);
|
|
302
|
+
if !report.diff_stat.is_empty() {
|
|
303
|
+
tui::print_section("📊", "Diff Summary");
|
|
304
|
+
for line in report.diff_stat.lines() {
|
|
305
|
+
eprintln!(" {line}");
|
|
306
|
+
}
|
|
255
307
|
}
|
|
256
|
-
|
|
308
|
+
|
|
309
|
+
eprintln!();
|
|
310
|
+
eprintln!(" 📋 Resume prompt ready. Use --json to get the full prompt.");
|
|
257
311
|
}
|
|
258
312
|
}
|
|
259
313
|
|
|
260
|
-
|
|
261
|
-
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════
|
|
315
|
+
// HISTORY
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════
|
|
317
|
+
Commands::History { limit } => {
|
|
318
|
+
let entries = relay::history::list_handoffs(&project_dir, limit)?;
|
|
262
319
|
|
|
263
320
|
if cli.json {
|
|
264
|
-
println!("{}", serde_json::to_string_pretty(&
|
|
321
|
+
println!("{}", serde_json::to_string_pretty(&entries)?);
|
|
322
|
+
return Ok(());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if entries.is_empty() {
|
|
326
|
+
eprintln!(" No handoffs recorded yet.");
|
|
265
327
|
return Ok(());
|
|
266
328
|
}
|
|
267
329
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
let icon = if s.available { "✅" } else { "❌" };
|
|
275
|
-
let name = if s.available {
|
|
276
|
-
s.name.green().bold().to_string()
|
|
277
|
-
} else {
|
|
278
|
-
s.name.dimmed().to_string()
|
|
279
|
-
};
|
|
280
|
-
println!(
|
|
281
|
-
" {icon} {:<10} {}",
|
|
282
|
-
name,
|
|
283
|
-
s.reason
|
|
330
|
+
tui::print_section("📜", &format!("Handoff History ({} entries)", entries.len()));
|
|
331
|
+
eprintln!();
|
|
332
|
+
for e in &entries {
|
|
333
|
+
eprintln!(
|
|
334
|
+
" {} → {:<10} {}",
|
|
335
|
+
e.timestamp, e.agent, e.task
|
|
284
336
|
);
|
|
285
|
-
|
|
286
|
-
|
|
337
|
+
}
|
|
338
|
+
eprintln!();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════
|
|
342
|
+
// DIFF
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════
|
|
344
|
+
Commands::Diff => {
|
|
345
|
+
let report = relay::diff::diff_since_handoff(&project_dir)?;
|
|
346
|
+
|
|
347
|
+
if cli.json {
|
|
348
|
+
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
349
|
+
return Ok(());
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
tui::print_section("📊", "Changes Since Last Handoff");
|
|
353
|
+
eprintln!(" Handoff at: {}", report.handoff_time);
|
|
354
|
+
eprintln!(
|
|
355
|
+
" {} added, {} modified, {} deleted",
|
|
356
|
+
report.files_added, report.files_modified, report.files_deleted
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if !report.new_commits.is_empty() {
|
|
360
|
+
eprintln!();
|
|
361
|
+
eprintln!(" Commits:");
|
|
362
|
+
for c in &report.new_commits {
|
|
363
|
+
eprintln!(" {c}");
|
|
287
364
|
}
|
|
288
365
|
}
|
|
289
|
-
println!();
|
|
290
366
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
" {} agent{} ready for handoff.",
|
|
297
|
-
available,
|
|
298
|
-
if available == 1 { "" } else { "s" }
|
|
299
|
-
);
|
|
367
|
+
if !report.diff_stat.is_empty() {
|
|
368
|
+
eprintln!();
|
|
369
|
+
for line in report.diff_stat.lines() {
|
|
370
|
+
eprintln!(" {line}");
|
|
371
|
+
}
|
|
300
372
|
}
|
|
373
|
+
eprintln!();
|
|
301
374
|
}
|
|
302
375
|
|
|
376
|
+
// ═══════════════════════════════════════════════════════════════
|
|
377
|
+
// INIT
|
|
378
|
+
// ═══════════════════════════════════════════════════════════════
|
|
303
379
|
Commands::Init => {
|
|
304
380
|
let path = relay::config_path();
|
|
305
381
|
if path.exists() {
|
|
306
|
-
|
|
307
|
-
|
|
382
|
+
eprintln!(" Config exists: {}", path.display());
|
|
383
|
+
eprintln!(" Edit to add API keys and customize priority.");
|
|
308
384
|
} else {
|
|
309
385
|
Config::save_default(&path)?;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
println!(" api_key = \"your-openai-key\"");
|
|
386
|
+
eprintln!(" ✅ Config created: {}", path.display());
|
|
387
|
+
eprintln!();
|
|
388
|
+
eprintln!(" Add API keys:");
|
|
389
|
+
eprintln!(" [agents.gemini]");
|
|
390
|
+
eprintln!(" api_key = \"your-key\"");
|
|
391
|
+
eprintln!();
|
|
392
|
+
eprintln!(" [agents.openai]");
|
|
393
|
+
eprintln!(" api_key = \"your-key\"");
|
|
319
394
|
}
|
|
320
395
|
}
|
|
321
396
|
|
|
397
|
+
// ═══════════════════════════════════════════════════════════════
|
|
398
|
+
// HOOK
|
|
399
|
+
// ═══════════════════════════════════════════════════════════════
|
|
322
400
|
Commands::Hook { session: _ } => {
|
|
323
401
|
use std::io::Read;
|
|
324
402
|
let mut raw = String::new();
|
|
325
403
|
std::io::stdin().read_to_string(&mut raw)?;
|
|
326
404
|
|
|
327
|
-
// Check for rate limit signals
|
|
328
405
|
if let Some(detection) = relay::detect::check_hook_output(&raw) {
|
|
329
406
|
eprintln!(
|
|
330
|
-
"{}",
|
|
331
|
-
|
|
332
|
-
"🚨 [relay] Rate limit detected in {} output (signal: {})",
|
|
333
|
-
detection.tool_name, detection.signal
|
|
334
|
-
).red().bold()
|
|
407
|
+
" 🚨 Rate limit detected in {} (signal: {})",
|
|
408
|
+
detection.tool_name, detection.signal
|
|
335
409
|
);
|
|
336
|
-
|
|
337
410
|
if config.general.auto_handoff {
|
|
338
|
-
// Auto-handoff
|
|
339
411
|
let snapshot = capture::capture_snapshot(&project_dir, None)?;
|
|
340
|
-
let handoff_text = handoff::build_handoff(
|
|
341
|
-
&snapshot,
|
|
342
|
-
"auto",
|
|
343
|
-
config.general.max_context_tokens,
|
|
344
|
-
)?;
|
|
345
|
-
|
|
412
|
+
let handoff_text = handoff::build_handoff(&snapshot, "auto", config.general.max_context_tokens)?;
|
|
346
413
|
let handoff_path = handoff::save_handoff(&handoff_text, &project_dir)?;
|
|
347
|
-
eprintln!(
|
|
348
|
-
"📄 Handoff saved: {}",
|
|
349
|
-
handoff_path.display()
|
|
350
|
-
);
|
|
351
|
-
|
|
352
414
|
let result = agents::handoff_to_first_available(
|
|
353
|
-
&config,
|
|
354
|
-
&handoff_text,
|
|
355
|
-
&project_dir.to_string_lossy(),
|
|
415
|
+
&config, &handoff_text, &project_dir.to_string_lossy(),
|
|
356
416
|
)?;
|
|
357
|
-
|
|
358
417
|
if result.success {
|
|
359
|
-
eprintln!(
|
|
360
|
-
"{}",
|
|
361
|
-
format!("✅ Auto-handed off to {}", result.agent).green()
|
|
362
|
-
);
|
|
418
|
+
eprintln!(" ✅ Auto-handed off to {}", result.agent);
|
|
363
419
|
} else {
|
|
364
|
-
eprintln!(
|
|
365
|
-
"{}",
|
|
366
|
-
format!("⚠️ No agents available. Handoff saved to: {}",
|
|
367
|
-
handoff_path.display()
|
|
368
|
-
).yellow()
|
|
369
|
-
);
|
|
420
|
+
eprintln!(" 📄 Saved: {}", handoff_path.display());
|
|
370
421
|
}
|
|
371
422
|
}
|
|
372
423
|
}
|
|
373
|
-
|
|
374
|
-
// Always pass through the original output
|
|
375
424
|
print!("{raw}");
|
|
376
425
|
}
|
|
377
426
|
}
|