anveesa 0.2.5 → 0.2.7
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +1 -15
- package/package.json +1 -1
- package/src/lib.rs +97 -2
- package/src/provider/command.rs +4 -0
- package/src/provider/mod.rs +9 -0
- package/src/provider/openai_compatible.rs +56 -2
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Anveesa
|
|
2
2
|
|
|
3
3
|
Anveesa is a Rust terminal wrapper for AI providers. It gives you one command,
|
|
4
|
-
`anveesa`,
|
|
4
|
+
`anveesa`, for interactive prompts and one-shot terminal requests.
|
|
5
5
|
|
|
6
6
|
## 📦 Publishing to npm
|
|
7
7
|
|
|
@@ -13,20 +13,6 @@ Anveesa can be published as an npm package via a Node.js wrapper that invokes th
|
|
|
13
13
|
npm install -g anveesa
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
### Build and publish
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
git tag v$(node -p "require('./package.json').version")
|
|
20
|
-
git push origin main --tags
|
|
21
|
-
npm publish
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Wait for the GitHub release binary workflow to finish before publishing to npm.
|
|
25
|
-
See `npm-publish.md` for the full release checklist.
|
|
26
|
-
|
|
27
|
-
- `openai-compatible`: HTTP chat completions providers such as OpenRouter and other compatible gateways.
|
|
28
|
-
- `command`: local CLIs such as Codex, Copilot, and Claude Code, where Anveesa spawns a command and passes the prompt.
|
|
29
|
-
|
|
30
16
|
## Install locally
|
|
31
17
|
|
|
32
18
|
```sh
|
package/package.json
CHANGED
package/src/lib.rs
CHANGED
|
@@ -326,6 +326,7 @@ async fn render_stream(
|
|
|
326
326
|
let mut usage: Option<Usage> = None;
|
|
327
327
|
let mut plan_tasks: Vec<String> = vec![];
|
|
328
328
|
let mut plan_done: Vec<bool> = vec![];
|
|
329
|
+
let mut status_message = "Waiting for provider response".to_string();
|
|
329
330
|
|
|
330
331
|
static TIPS: &[&str] = &[
|
|
331
332
|
"Tip: type /clear to reset context",
|
|
@@ -337,6 +338,18 @@ async fn render_stream(
|
|
|
337
338
|
loop {
|
|
338
339
|
tokio::select! {
|
|
339
340
|
maybe = rx.recv() => match maybe {
|
|
341
|
+
Some(StreamEvent::Status { message }) => {
|
|
342
|
+
clear_spinner(spinner, spinner_active);
|
|
343
|
+
spinner_active = false;
|
|
344
|
+
if line_open {
|
|
345
|
+
println!();
|
|
346
|
+
line_open = false;
|
|
347
|
+
}
|
|
348
|
+
status_message = message;
|
|
349
|
+
print_status(&status_message, spinner);
|
|
350
|
+
first_token = true;
|
|
351
|
+
frame = 0;
|
|
352
|
+
}
|
|
340
353
|
Some(StreamEvent::Token(text)) => {
|
|
341
354
|
if first_token {
|
|
342
355
|
clear_spinner(spinner, spinner_active);
|
|
@@ -359,10 +372,27 @@ async fn render_stream(
|
|
|
359
372
|
println!();
|
|
360
373
|
line_open = false;
|
|
361
374
|
}
|
|
375
|
+
status_message = format!("Running {summary}");
|
|
362
376
|
print_tool_call(&summary, spinner);
|
|
363
377
|
first_token = true;
|
|
364
378
|
frame = 0;
|
|
365
379
|
}
|
|
380
|
+
Some(StreamEvent::ToolResult { summary, ok, elapsed_ms, error }) => {
|
|
381
|
+
clear_spinner(spinner, spinner_active);
|
|
382
|
+
spinner_active = false;
|
|
383
|
+
if line_open {
|
|
384
|
+
println!();
|
|
385
|
+
line_open = false;
|
|
386
|
+
}
|
|
387
|
+
print_tool_result(&summary, ok, elapsed_ms, error.as_deref(), spinner);
|
|
388
|
+
status_message = if ok {
|
|
389
|
+
"Waiting for the model to continue".to_string()
|
|
390
|
+
} else {
|
|
391
|
+
"Waiting for the model to handle the tool failure".to_string()
|
|
392
|
+
};
|
|
393
|
+
first_token = true;
|
|
394
|
+
frame = 0;
|
|
395
|
+
}
|
|
366
396
|
Some(StreamEvent::Confirm { preview, reply }) => {
|
|
367
397
|
clear_spinner(spinner, spinner_active);
|
|
368
398
|
spinner_active = false;
|
|
@@ -374,6 +404,20 @@ async fn render_stream(
|
|
|
374
404
|
show_confirm_preview(&preview, spinner);
|
|
375
405
|
prompt_confirm_decision(spinner)
|
|
376
406
|
});
|
|
407
|
+
match decision {
|
|
408
|
+
ApprovalDecision::AllowOnce => {
|
|
409
|
+
print_status("Approved; applying action", spinner);
|
|
410
|
+
status_message = "Applying approved action".to_string();
|
|
411
|
+
}
|
|
412
|
+
ApprovalDecision::AllowForTurn => {
|
|
413
|
+
print_status("Approved all actions for this turn; applying action", spinner);
|
|
414
|
+
status_message = "Applying approved action".to_string();
|
|
415
|
+
}
|
|
416
|
+
ApprovalDecision::Deny => {
|
|
417
|
+
print_status("Declined action; returning decision to model", spinner);
|
|
418
|
+
status_message = "Waiting for the model to continue".to_string();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
377
421
|
let _ = reply.send(decision);
|
|
378
422
|
// Re-arm the spinner for the next API round.
|
|
379
423
|
first_token = true;
|
|
@@ -428,17 +472,18 @@ async fn render_stream(
|
|
|
428
472
|
let dots = ["", ".", "..", "…"][frame % 4];
|
|
429
473
|
// Tip rotates every 40 frames (~4 s)
|
|
430
474
|
let tip = TIPS[(frame / 40) % TIPS.len()];
|
|
475
|
+
let status = truncate_for_status(&status_message, 76);
|
|
431
476
|
|
|
432
477
|
if !spinner_active {
|
|
433
478
|
// First paint — just print 2 lines (no overwrite needed).
|
|
434
479
|
eprint!(
|
|
435
|
-
"\x1b[1;32m+\x1b[0m
|
|
480
|
+
"\x1b[1;32m+\x1b[0m {status}{dots} \x1b[2m({time_str})\x1b[0m\n \x1b[90m└\x1b[0m \x1b[2m{tip}\x1b[0m"
|
|
436
481
|
);
|
|
437
482
|
spinner_active = true;
|
|
438
483
|
} else {
|
|
439
484
|
// Overwrite: move up 1 line, clear both lines, reprint.
|
|
440
485
|
eprint!(
|
|
441
|
-
"\r\x1b[2K\x1b[1A\x1b[2K\r\x1b[1;32m+\x1b[0m
|
|
486
|
+
"\r\x1b[2K\x1b[1A\x1b[2K\r\x1b[1;32m+\x1b[0m {status}{dots} \x1b[2m({time_str})\x1b[0m\n \x1b[90m└\x1b[0m \x1b[2m{tip}\x1b[0m"
|
|
442
487
|
);
|
|
443
488
|
}
|
|
444
489
|
let _ = io::stderr().flush();
|
|
@@ -483,6 +528,33 @@ fn print_tool_call(summary: &str, is_tty: bool) {
|
|
|
483
528
|
}
|
|
484
529
|
}
|
|
485
530
|
|
|
531
|
+
fn print_status(message: &str, is_tty: bool) {
|
|
532
|
+
if is_tty {
|
|
533
|
+
eprintln!("\x1b[90m · {message}\x1b[0m");
|
|
534
|
+
} else {
|
|
535
|
+
eprintln!("status: {message}");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
fn print_tool_result(summary: &str, ok: bool, elapsed_ms: u128, error: Option<&str>, is_tty: bool) {
|
|
540
|
+
let elapsed = format_duration_ms(elapsed_ms);
|
|
541
|
+
if is_tty {
|
|
542
|
+
if ok {
|
|
543
|
+
eprintln!("\x1b[1;32m ✓\x1b[0m \x1b[90m{summary} completed in {elapsed}\x1b[0m");
|
|
544
|
+
} else if let Some(error) = error {
|
|
545
|
+
eprintln!("\x1b[1;31m ✗\x1b[0m \x1b[90m{summary} failed in {elapsed}: {error}\x1b[0m");
|
|
546
|
+
} else {
|
|
547
|
+
eprintln!("\x1b[1;31m ✗\x1b[0m \x1b[90m{summary} failed in {elapsed}\x1b[0m");
|
|
548
|
+
}
|
|
549
|
+
} else if ok {
|
|
550
|
+
eprintln!("tool ok: {summary} ({elapsed})");
|
|
551
|
+
} else if let Some(error) = error {
|
|
552
|
+
eprintln!("tool failed: {summary} ({elapsed}): {error}");
|
|
553
|
+
} else {
|
|
554
|
+
eprintln!("tool failed: {summary} ({elapsed})");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
486
558
|
fn print_file_op(
|
|
487
559
|
verb: &str,
|
|
488
560
|
path: &str,
|
|
@@ -590,6 +662,29 @@ fn format_elapsed(secs: f32) -> String {
|
|
|
590
662
|
}
|
|
591
663
|
}
|
|
592
664
|
|
|
665
|
+
fn format_duration_ms(ms: u128) -> String {
|
|
666
|
+
if ms >= 1000 {
|
|
667
|
+
format!("{:.1}s", ms as f64 / 1000.0)
|
|
668
|
+
} else {
|
|
669
|
+
format!("{ms}ms")
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
fn truncate_for_status(value: &str, max_chars: usize) -> String {
|
|
674
|
+
let mut chars = value.chars();
|
|
675
|
+
let mut output = String::new();
|
|
676
|
+
for _ in 0..max_chars {
|
|
677
|
+
let Some(ch) = chars.next() else {
|
|
678
|
+
return output;
|
|
679
|
+
};
|
|
680
|
+
output.push(ch);
|
|
681
|
+
}
|
|
682
|
+
if chars.next().is_some() {
|
|
683
|
+
output.push('…');
|
|
684
|
+
}
|
|
685
|
+
output
|
|
686
|
+
}
|
|
687
|
+
|
|
593
688
|
fn show_confirm_preview(preview: &ToolConfirmPreview, is_tty: bool) {
|
|
594
689
|
match preview {
|
|
595
690
|
ToolConfirmPreview::FileOp {
|
package/src/provider/command.rs
CHANGED
|
@@ -37,6 +37,10 @@ pub async fn ask(
|
|
|
37
37
|
command.stdin(Stdio::piped());
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
let _ = events.send(StreamEvent::Status {
|
|
41
|
+
message: format!("Running command provider `{}`", config.command),
|
|
42
|
+
});
|
|
43
|
+
|
|
40
44
|
let mut child = command.spawn().with_context(|| {
|
|
41
45
|
format!(
|
|
42
46
|
"failed to spawn command provider '{}' at {}",
|
package/src/provider/mod.rs
CHANGED
|
@@ -112,12 +112,21 @@ pub enum ToolConfirmPreview {
|
|
|
112
112
|
/// Events streamed from a provider back to the renderer, which owns the terminal.
|
|
113
113
|
#[derive(Debug)]
|
|
114
114
|
pub enum StreamEvent {
|
|
115
|
+
/// Durable progress/status message for long waits between model/tool phases.
|
|
116
|
+
Status { message: String },
|
|
115
117
|
/// A chunk of assistant text to display as it arrives.
|
|
116
118
|
Token(String),
|
|
117
119
|
/// Final token accounting for the turn.
|
|
118
120
|
Usage(Usage),
|
|
119
121
|
/// A read-only tool is running. Used to make multi-round inspection visible.
|
|
120
122
|
ToolCall { summary: String },
|
|
123
|
+
/// A tool finished running. Used to show explicit success/failure after approval.
|
|
124
|
+
ToolResult {
|
|
125
|
+
summary: String,
|
|
126
|
+
ok: bool,
|
|
127
|
+
elapsed_ms: u128,
|
|
128
|
+
error: Option<String>,
|
|
129
|
+
},
|
|
121
130
|
/// A write/run tool needs the user's approval. The renderer shows the
|
|
122
131
|
/// preview, prompts for a decision, and sends it back through the reply channel.
|
|
123
132
|
Confirm {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use std::time::Duration;
|
|
1
|
+
use std::time::{Duration, Instant};
|
|
2
2
|
|
|
3
3
|
use anyhow::{Context, Result, bail};
|
|
4
4
|
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
|
|
@@ -62,6 +62,14 @@ pub async fn ask(
|
|
|
62
62
|
let mut tool_intent_reprompts = 0usize;
|
|
63
63
|
|
|
64
64
|
loop {
|
|
65
|
+
let _ = events.send(StreamEvent::Status {
|
|
66
|
+
message: if tool_rounds == 0 {
|
|
67
|
+
format!("Waiting for {provider_name} response")
|
|
68
|
+
} else {
|
|
69
|
+
format!("Sending tool results to {provider_name}")
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
65
73
|
let mut body = json!({
|
|
66
74
|
"model": model,
|
|
67
75
|
"messages": messages,
|
|
@@ -134,6 +142,10 @@ pub async fn ask(
|
|
|
134
142
|
}));
|
|
135
143
|
}
|
|
136
144
|
|
|
145
|
+
let _ = events.send(StreamEvent::Status {
|
|
146
|
+
message: "Tool results sent; waiting for the next model response".to_string(),
|
|
147
|
+
});
|
|
148
|
+
|
|
137
149
|
if tool_rounds >= max_tool_rounds {
|
|
138
150
|
tools_enabled = false;
|
|
139
151
|
messages.push(tool_limit_message(max_tool_rounds));
|
|
@@ -163,6 +175,8 @@ async fn dispatch_tool(
|
|
|
163
175
|
approval_state: &mut ToolApprovalState,
|
|
164
176
|
events: &UnboundedSender<StreamEvent>,
|
|
165
177
|
) -> String {
|
|
178
|
+
let summary = tools::describe_call(&call.name, &call.arguments);
|
|
179
|
+
|
|
166
180
|
// Plan tools — display only, no approval or filesystem access needed.
|
|
167
181
|
if call.name == "set_plan" {
|
|
168
182
|
if let Ok(args) = serde_json::from_str::<serde_json::Value>(&call.arguments) {
|
|
@@ -213,7 +227,7 @@ async fn dispatch_tool(
|
|
|
213
227
|
}
|
|
214
228
|
} else {
|
|
215
229
|
let _ = events.send(StreamEvent::ToolCall {
|
|
216
|
-
summary:
|
|
230
|
+
summary: summary.clone(),
|
|
217
231
|
});
|
|
218
232
|
}
|
|
219
233
|
|
|
@@ -233,9 +247,24 @@ async fn dispatch_tool(
|
|
|
233
247
|
ApprovalDecision::AllowForTurn => approval_state.allow_for_turn = true,
|
|
234
248
|
ApprovalDecision::Deny => return denied_message("user declined this action"),
|
|
235
249
|
}
|
|
250
|
+
let _ = events.send(StreamEvent::Status {
|
|
251
|
+
message: format!("Applying approved action: {summary}"),
|
|
252
|
+
});
|
|
253
|
+
} else if tools::is_write_tool(&call.name) {
|
|
254
|
+
let _ = events.send(StreamEvent::ToolCall {
|
|
255
|
+
summary: summary.clone(),
|
|
256
|
+
});
|
|
236
257
|
}
|
|
237
258
|
|
|
259
|
+
let tool_started = Instant::now();
|
|
238
260
|
let result = tools::run(&call.name, &call.arguments).await;
|
|
261
|
+
let (ok, error) = parse_tool_result_status(&result);
|
|
262
|
+
let _ = events.send(StreamEvent::ToolResult {
|
|
263
|
+
summary: summary.clone(),
|
|
264
|
+
ok,
|
|
265
|
+
elapsed_ms: tool_started.elapsed().as_millis(),
|
|
266
|
+
error,
|
|
267
|
+
});
|
|
239
268
|
|
|
240
269
|
// When the user already reviewed the diff in the approval preview, skip the
|
|
241
270
|
// post-run FileOp so the same diff isn't printed twice.
|
|
@@ -252,6 +281,18 @@ async fn dispatch_tool(
|
|
|
252
281
|
result
|
|
253
282
|
}
|
|
254
283
|
|
|
284
|
+
fn parse_tool_result_status(result: &str) -> (bool, Option<String>) {
|
|
285
|
+
let Ok(json) = serde_json::from_str::<Value>(result) else {
|
|
286
|
+
return (true, None);
|
|
287
|
+
};
|
|
288
|
+
let ok = json.get("ok").and_then(Value::as_bool).unwrap_or(true);
|
|
289
|
+
let error = json
|
|
290
|
+
.get("error")
|
|
291
|
+
.and_then(Value::as_str)
|
|
292
|
+
.map(str::to_string);
|
|
293
|
+
(ok, error)
|
|
294
|
+
}
|
|
295
|
+
|
|
255
296
|
// ── File-op diff helpers ──────────────────────────────────────────────────────
|
|
256
297
|
|
|
257
298
|
enum FileOpSnapshot {
|
|
@@ -1048,4 +1089,17 @@ mod tests {
|
|
|
1048
1089
|
));
|
|
1049
1090
|
assert!(!looks_like_unfinished_tool_intent(""));
|
|
1050
1091
|
}
|
|
1092
|
+
|
|
1093
|
+
#[test]
|
|
1094
|
+
fn parses_tool_result_status() {
|
|
1095
|
+
assert_eq!(parse_tool_result_status(r#"{"ok":true}"#), (true, None));
|
|
1096
|
+
assert_eq!(
|
|
1097
|
+
parse_tool_result_status(r#"{"ok":false,"error":"boom"}"#),
|
|
1098
|
+
(false, Some("boom".to_string()))
|
|
1099
|
+
);
|
|
1100
|
+
assert_eq!(
|
|
1101
|
+
parse_tool_result_status(r#"{"content":"no explicit ok"}"#),
|
|
1102
|
+
(true, None)
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1051
1105
|
}
|