anveesa 0.2.2 → 0.2.4

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 CHANGED
@@ -54,7 +54,7 @@ dependencies = [
54
54
 
55
55
  [[package]]
56
56
  name = "anveesa"
57
- version = "0.2.0"
57
+ version = "0.2.4"
58
58
  dependencies = [
59
59
  "anyhow",
60
60
  "base64",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "anveesa"
3
- version = "0.2.0"
3
+ version = "0.2.4"
4
4
  edition = "2024"
5
5
  default-run = "anveesa"
6
6
 
package/README.md CHANGED
@@ -10,17 +10,19 @@ Anveesa can be published as an npm package via a Node.js wrapper that invokes th
10
10
  ### Install from npm
11
11
 
12
12
  ```bash
13
- npm install -g anveesa-cli
13
+ npm install -g anveesa
14
14
  ```
15
15
 
16
16
  ### Build and publish
17
17
 
18
18
  ```bash
19
- npm run build
19
+ git tag v$(node -p "require('./package.json').version")
20
+ git push origin main --tags
20
21
  npm publish
21
22
  ```
22
23
 
23
- See `package.json` in the root directory for build scripts and npm configuration.
24
+ Wait for the GitHub release binary workflow to finish before publishing to npm.
25
+ See `npm-publish.md` for the full release checklist.
24
26
 
25
27
  - `openai-compatible`: HTTP chat completions providers such as OpenRouter and other compatible gateways.
26
28
  - `command`: local CLIs such as Codex, Copilot, and Claude Code, where Anveesa spawns a command and passes the prompt.
package/bin/anveesa.js CHANGED
@@ -8,14 +8,14 @@ function findBinary() {
8
8
  const platform = process.platform;
9
9
  const ext = platform === 'win32' ? '.exe' : '';
10
10
 
11
- // Binary is shipped in the package bin/ directory
12
- const bundled = path.join(__dirname, 'anveesa' + ext);
13
- if (fs.existsSync(bundled)) return bundled;
14
-
15
- // Also check for target/release (dev mode)
11
+ // Prefer the latest local release build in a development checkout.
16
12
  const devPath = path.join(__dirname, '..', 'target', 'release', 'anveesa' + ext);
17
13
  if (fs.existsSync(devPath)) return devPath;
18
14
 
15
+ // Installed npm packages keep the downloaded binary in the package bin/ directory.
16
+ const bundled = path.join(__dirname, 'anveesa' + ext);
17
+ if (fs.existsSync(bundled)) return bundled;
18
+
19
19
  // Check if there's a sibling directory with the binary
20
20
  const sibling = path.join(__dirname, 'target', 'release', 'anveesa' + ext);
21
21
  if (fs.existsSync(sibling)) return sibling;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anveesa",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A terminal CLI that wraps AI providers (OpenAI-compatible APIs and local CLIs) into a single unified command",
5
5
  "main": "bin/anveesa.js",
6
6
  "bin": {
@@ -12,13 +12,12 @@
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
14
  "files": [
15
- "bin/",
16
- "scripts/",
15
+ "bin/anveesa.js",
16
+ "scripts/install.js",
17
17
  "Cargo.toml",
18
18
  "Cargo.lock",
19
19
  "src/",
20
- "README.md",
21
- ".github/"
20
+ "README.md"
22
21
  ],
23
22
  "keywords": [
24
23
  "ai",
@@ -8,11 +8,12 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const https = require('https');
11
- const { execSync } = require('child_process');
11
+ const { execFileSync } = require('child_process');
12
12
 
13
13
  const PACKAGE = require(path.join(__dirname, '..', 'package.json'));
14
14
  const REPO = 'pandhuwibowo/anveesa-cli';
15
15
  const BIN_DIR = path.join(__dirname, '..', 'bin');
16
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
16
17
 
17
18
  function getPlatformInfo() {
18
19
  const platform = process.platform;
@@ -32,6 +33,14 @@ function getPlatformInfo() {
32
33
  return null;
33
34
  }
34
35
 
36
+ function getExecutableName() {
37
+ return process.platform === 'win32' ? 'anveesa.exe' : 'anveesa';
38
+ }
39
+
40
+ function getBinaryPath() {
41
+ return path.join(BIN_DIR, getExecutableName());
42
+ }
43
+
35
44
  function getBinaryUrl() {
36
45
  const info = getPlatformInfo();
37
46
  if (!info) return null;
@@ -39,31 +48,47 @@ function getBinaryUrl() {
39
48
  return `https://github.com/${REPO}/releases/download/v${version}/anveesa-${version}-${info.target}.tar.gz`;
40
49
  }
41
50
 
42
- function download(url, dest) {
51
+ function download(url, dest, redirects = 0) {
43
52
  return new Promise((resolve, reject) => {
44
- const file = fs.createWriteStream(dest);
45
- https.get(url, { followAllRedirects: true }, (res) => {
46
- if (res.statusCode === 302 || res.statusCode === 301) {
47
- https.get(res.headers.location, (res2) => {
48
- res2.pipe(file);
49
- file.on('finish', () => { file.close(); resolve(); });
50
- }).on('error', reject);
51
- } else if (res.statusCode === 200) {
52
- res.pipe(file);
53
- file.on('finish', () => { file.close(); resolve(); });
54
- } else if (res.statusCode === 404) {
55
- file.close();
56
- fs.unlink(dest, () => {});
57
- reject(new Error('404'));
58
- } else {
59
- file.close();
60
- fs.unlink(dest, () => {});
61
- reject(new Error(`HTTP ${res.statusCode}`));
53
+ const request = https.get(url, {
54
+ headers: { 'User-Agent': `anveesa-install/${PACKAGE.version}` },
55
+ }, (res) => {
56
+ if (REDIRECT_STATUSES.has(res.statusCode)) {
57
+ res.resume();
58
+ if (!res.headers.location) {
59
+ reject(new Error(`HTTP ${res.statusCode} without Location header`));
60
+ return;
61
+ }
62
+ if (redirects >= 5) {
63
+ reject(new Error('too many redirects'));
64
+ return;
65
+ }
66
+
67
+ const nextUrl = new URL(res.headers.location, url).toString();
68
+ resolve(download(nextUrl, dest, redirects + 1));
69
+ return;
62
70
  }
71
+
72
+ if (res.statusCode !== 200) {
73
+ res.resume();
74
+ reject(new Error(res.statusCode === 404 ? '404' : `HTTP ${res.statusCode}`));
75
+ return;
76
+ }
77
+
78
+ const file = fs.createWriteStream(dest);
79
+ file.on('finish', () => { file.close(resolve); });
80
+ file.on('error', (err) => {
81
+ fs.unlink(dest, () => {});
82
+ reject(err);
83
+ });
84
+ res.pipe(file);
63
85
  }).on('error', (err) => {
64
86
  fs.unlink(dest, () => {});
65
87
  reject(err);
66
88
  });
89
+ request.setTimeout(30000, () => {
90
+ request.destroy(new Error('download timed out'));
91
+ });
67
92
  });
68
93
  }
69
94
 
@@ -76,18 +101,23 @@ async function tryDownloadBinary() {
76
101
 
77
102
  console.log('⬇ Downloading prebuilt binary for', process.platform, process.arch);
78
103
 
104
+ const tarPath = path.join(__dirname, 'anveesa-bin.tar.gz');
79
105
  try {
80
- const tarPath = path.join(__dirname, 'anveesa-bin.tar.gz');
106
+ if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
107
+
81
108
  await download(url, tarPath);
82
109
 
83
110
  // Extract
84
- execSync(`tar xzf "${tarPath}" -C "${BIN_DIR}"`, { stdio: 'inherit' });
111
+ execFileSync('tar', ['xzf', tarPath, '-C', BIN_DIR], { stdio: 'inherit' });
85
112
  fs.unlinkSync(tarPath);
86
113
 
87
114
  // Make executable
88
- const binary = path.join(BIN_DIR, 'anveesa');
115
+ const binary = getBinaryPath();
89
116
  if (fs.existsSync(binary)) {
90
- fs.chmodSync(binary, 0o755);
117
+ if (process.platform !== 'win32') fs.chmodSync(binary, 0o755);
118
+ } else {
119
+ console.log('⚠ Downloaded archive did not contain', getExecutableName());
120
+ return false;
91
121
  }
92
122
 
93
123
  console.log('✓ Binary downloaded successfully');
@@ -96,6 +126,7 @@ async function tryDownloadBinary() {
96
126
  if (e.message === '404') {
97
127
  console.log('ℹ No prebuilt binary for this version yet');
98
128
  }
129
+ fs.unlink(tarPath, () => {});
99
130
  return false;
100
131
  }
101
132
  }
@@ -103,29 +134,45 @@ async function tryDownloadBinary() {
103
134
  function tryBuildFromSource() {
104
135
  try {
105
136
  console.log('⚙ Building from source (requires Rust)...');
106
- execSync('cargo build --release', { cwd: path.join(__dirname, '..'), stdio: 'inherit' });
137
+ execFileSync('cargo', ['build', '--release'], { cwd: path.join(__dirname, '..'), stdio: 'inherit' });
107
138
 
108
- const src = path.join(__dirname, '..', 'target', 'release', 'anveesa');
109
- const dest = path.join(BIN_DIR, 'anveesa');
139
+ const src = path.join(__dirname, '..', 'target', 'release', getExecutableName());
140
+ const dest = getBinaryPath();
110
141
 
111
142
  if (fs.existsSync(src)) {
112
143
  if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
113
144
  fs.copyFileSync(src, dest);
114
- fs.chmodSync(dest, 0o755);
145
+ if (process.platform !== 'win32') fs.chmodSync(dest, 0o755);
115
146
  console.log('✓ Built from source successfully');
116
147
  return true;
117
148
  }
118
149
  } catch (e) {
119
- console.log('⚠ Build from source failed (Rust not installed?)');
150
+ if (e.code === 'ENOENT') {
151
+ console.log('⚠ Build from source failed: cargo was not found');
152
+ } else {
153
+ console.log('⚠ Build from source failed');
154
+ }
120
155
  }
121
156
  return false;
122
157
  }
123
158
 
159
+ function hasUsableExistingBinary() {
160
+ const existing = getBinaryPath();
161
+ if (!fs.existsSync(existing)) return false;
162
+
163
+ try {
164
+ if (process.platform !== 'win32') fs.chmodSync(existing, 0o755);
165
+ execFileSync(existing, ['--help'], { stdio: 'ignore', timeout: 5000 });
166
+ return true;
167
+ } catch (e) {
168
+ console.log('ℹ Existing anveesa binary is not usable on this platform; replacing it');
169
+ return false;
170
+ }
171
+ }
172
+
124
173
  async function install() {
125
174
  // Check if binary already exists
126
- const existing = path.join(BIN_DIR, 'anveesa');
127
- const existingExe = path.join(BIN_DIR, 'anveesa.exe');
128
- if (fs.existsSync(existing) || fs.existsSync(existingExe)) {
175
+ if (hasUsableExistingBinary()) {
129
176
  console.log('✓ anveesa binary already installed');
130
177
  return;
131
178
  }
@@ -139,6 +186,12 @@ async function install() {
139
186
  console.error('');
140
187
  console.error('✗ Could not install anveesa binary.');
141
188
  console.error('');
189
+ const url = getBinaryUrl();
190
+ if (url) {
191
+ console.error(`No prebuilt binary was available for anveesa v${PACKAGE.version}:`);
192
+ console.error(` ${url}`);
193
+ console.error('');
194
+ }
142
195
  console.error('Install Rust: https://rustup.rs/');
143
196
  console.error('Then run: npm install -g anveesa');
144
197
  console.error('');
@@ -147,4 +200,4 @@ async function install() {
147
200
  process.exit(1);
148
201
  }
149
202
 
150
- install();
203
+ install();
package/src/lib.rs CHANGED
@@ -16,6 +16,7 @@ use clap::{CommandFactory, Parser};
16
16
  use serde::{Deserialize, Serialize};
17
17
  use tokio::sync::mpsc;
18
18
 
19
+ #[cfg(target_os = "macos")]
19
20
  use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
20
21
 
21
22
  use crate::{
@@ -301,6 +302,7 @@ async fn render_stream(
301
302
  let mut spinner_active = false;
302
303
  let mut first_token = true;
303
304
  let mut produced = false;
305
+ let mut line_open = false;
304
306
  let mut usage: Option<Usage> = None;
305
307
  let mut plan_tasks: Vec<String> = vec![];
306
308
  let mut plan_done: Vec<bool> = vec![];
@@ -325,13 +327,29 @@ async fn render_stream(
325
327
  first_token = false;
326
328
  }
327
329
  produced = true;
330
+ line_open = true;
328
331
  print!("{text}");
329
332
  let _ = io::stdout().flush();
330
333
  }
331
334
  Some(StreamEvent::Usage(value)) => usage = Some(value),
335
+ Some(StreamEvent::ToolCall { summary }) => {
336
+ clear_spinner(spinner, spinner_active);
337
+ spinner_active = false;
338
+ if line_open {
339
+ println!();
340
+ line_open = false;
341
+ }
342
+ print_tool_call(&summary, spinner);
343
+ first_token = true;
344
+ frame = 0;
345
+ }
332
346
  Some(StreamEvent::Confirm { preview, reply }) => {
333
347
  clear_spinner(spinner, spinner_active);
334
348
  spinner_active = false;
349
+ if line_open {
350
+ println!();
351
+ line_open = false;
352
+ }
335
353
  let decision = tokio::task::block_in_place(|| {
336
354
  show_confirm_preview(&preview, spinner);
337
355
  prompt_confirm_decision(spinner)
@@ -344,6 +362,10 @@ async fn render_stream(
344
362
  Some(StreamEvent::FileOp { verb, path, added, removed, preview, truncated }) => {
345
363
  clear_spinner(spinner, spinner_active);
346
364
  spinner_active = false;
365
+ if line_open {
366
+ println!();
367
+ line_open = false;
368
+ }
347
369
  print_file_op(&verb, &path, added, removed, &preview, truncated, spinner);
348
370
  // Re-arm the spinner for the next API round.
349
371
  first_token = true;
@@ -352,6 +374,10 @@ async fn render_stream(
352
374
  Some(StreamEvent::PlanSet { tasks }) => {
353
375
  clear_spinner(spinner, spinner_active);
354
376
  spinner_active = false;
377
+ if line_open {
378
+ println!();
379
+ line_open = false;
380
+ }
355
381
  plan_done = vec![false; tasks.len()];
356
382
  plan_tasks = tasks;
357
383
  print_plan_list(&plan_tasks, &plan_done, spinner);
@@ -361,6 +387,10 @@ async fn render_stream(
361
387
  Some(StreamEvent::PlanTaskDone { index }) => {
362
388
  clear_spinner(spinner, spinner_active);
363
389
  spinner_active = false;
390
+ if line_open {
391
+ println!();
392
+ line_open = false;
393
+ }
364
394
  if index < plan_done.len() {
365
395
  plan_done[index] = true;
366
396
  }
@@ -397,7 +427,7 @@ async fn render_stream(
397
427
  }
398
428
  }
399
429
 
400
- if produced {
430
+ if produced && line_open {
401
431
  println!();
402
432
  } else {
403
433
  clear_spinner(spinner, spinner_active);
@@ -425,6 +455,14 @@ async fn render_stream(
425
455
  }
426
456
  }
427
457
 
458
+ fn print_tool_call(summary: &str, is_tty: bool) {
459
+ if is_tty {
460
+ eprintln!("\x1b[90m └─ {summary}\x1b[0m");
461
+ } else {
462
+ eprintln!("tool: {summary}");
463
+ }
464
+ }
465
+
428
466
  fn print_file_op(
429
467
  verb: &str,
430
468
  path: &str,
@@ -968,11 +1006,13 @@ impl PromptBuffer {
968
1006
  }
969
1007
  }
970
1008
 
1009
+ #[cfg(unix)]
971
1010
  struct RawPromptMode {
972
1011
  fd: i32,
973
1012
  saved: libc::termios,
974
1013
  }
975
1014
 
1015
+ #[cfg(unix)]
976
1016
  impl RawPromptMode {
977
1017
  fn enter() -> Result<Self> {
978
1018
  let fd = libc::STDIN_FILENO;
@@ -1001,6 +1041,7 @@ impl RawPromptMode {
1001
1041
  }
1002
1042
  }
1003
1043
 
1044
+ #[cfg(unix)]
1004
1045
  impl Drop for RawPromptMode {
1005
1046
  fn drop(&mut self) {
1006
1047
  print!("\x1b[?2004l");
@@ -1012,6 +1053,16 @@ impl Drop for RawPromptMode {
1012
1053
  }
1013
1054
  }
1014
1055
 
1056
+ #[cfg(not(unix))]
1057
+ struct RawPromptMode;
1058
+
1059
+ #[cfg(not(unix))]
1060
+ impl RawPromptMode {
1061
+ fn enter() -> Result<Self> {
1062
+ Ok(Self)
1063
+ }
1064
+ }
1065
+
1015
1066
  fn read_prompt_line(label: &str, width: usize, paste_count: &mut usize) -> Result<PromptRead> {
1016
1067
  let _raw_mode = RawPromptMode::enter()?;
1017
1068
  let mut input = io::stdin().lock();
@@ -116,6 +116,8 @@ pub enum StreamEvent {
116
116
  Token(String),
117
117
  /// Final token accounting for the turn.
118
118
  Usage(Usage),
119
+ /// A read-only tool is running. Used to make multi-round inspection visible.
120
+ ToolCall { summary: String },
119
121
  /// A write/run tool needs the user's approval. The renderer shows the
120
122
  /// preview, prompts for a decision, and sends it back through the reply channel.
121
123
  Confirm {
@@ -21,6 +21,7 @@ const MAX_RETRIES: usize = 2;
21
21
  const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
22
22
  /// How many times the model may call the exact same (tool, arguments) pair before we refuse.
23
23
  const MAX_IDENTICAL_CALLS: usize = 3;
24
+ const MAX_TOOL_INTENT_REPROMPTS: usize = 2;
24
25
 
25
26
  pub async fn ask(
26
27
  provider_name: &str,
@@ -58,6 +59,7 @@ pub async fn ask(
58
59
  let mut approval_state = ToolApprovalState::default();
59
60
  let mut full_text = String::new();
60
61
  let mut last_usage: Option<Usage> = None;
62
+ let mut tool_intent_reprompts = 0usize;
61
63
 
62
64
  loop {
63
65
  let mut body = json!({
@@ -94,12 +96,25 @@ pub async fn ask(
94
96
  let mut state = StreamState::default();
95
97
  stream_response(response, &mut state, events).await?;
96
98
 
97
- full_text.push_str(&state.content);
98
99
  if let Some(usage) = state.usage {
99
100
  last_usage = Some(usage);
100
101
  }
101
102
 
102
103
  if state.tool_calls.is_empty() {
104
+ if tools_enabled
105
+ && tool_intent_reprompts < MAX_TOOL_INTENT_REPROMPTS
106
+ && looks_like_unfinished_tool_intent(&state.content)
107
+ {
108
+ tool_intent_reprompts += 1;
109
+ messages.push(json!({
110
+ "role": "assistant",
111
+ "content": state.content,
112
+ }));
113
+ messages.push(tool_intent_reprompt_message());
114
+ continue;
115
+ }
116
+
117
+ full_text.push_str(&state.content);
103
118
  break;
104
119
  }
105
120
 
@@ -196,6 +211,10 @@ async fn dispatch_tool(
196
211
  if !policy.allows_write_tools() {
197
212
  return denied_message("write tools are disabled (pass --yes or run interactively)");
198
213
  }
214
+ } else {
215
+ let _ = events.send(StreamEvent::ToolCall {
216
+ summary: tools::describe_call(&call.name, &call.arguments),
217
+ });
199
218
  }
200
219
 
201
220
  // Snapshot BEFORE the tool runs — needed both for preview and for post-run diff.
@@ -397,6 +416,50 @@ fn tool_limit_message(max_tool_rounds: usize) -> Value {
397
416
  })
398
417
  }
399
418
 
419
+ fn tool_intent_reprompt_message() -> Value {
420
+ json!({
421
+ "role": "system",
422
+ "content": "Your previous message said you would inspect/read/check the workspace, but it did not call any tool or provide a final answer. Do not narrate future tool use. If you need information, call the relevant Anveesa tools now. Otherwise, answer the user directly."
423
+ })
424
+ }
425
+
426
+ fn looks_like_unfinished_tool_intent(text: &str) -> bool {
427
+ let lower = text.trim().to_lowercase();
428
+ if lower.is_empty() || lower.len() > 600 {
429
+ return false;
430
+ }
431
+
432
+ let has_intent = [
433
+ "let me inspect",
434
+ "let me check",
435
+ "let me look",
436
+ "let me read",
437
+ "let me search",
438
+ "let me peek",
439
+ "let me also peek",
440
+ "i'll inspect",
441
+ "i'll check",
442
+ "i'll look",
443
+ "i'll read",
444
+ "i'll search",
445
+ "i will inspect",
446
+ "i will check",
447
+ "i will look",
448
+ "i will read",
449
+ "i will search",
450
+ "i'm going to inspect",
451
+ "i'm going to check",
452
+ "i'm going to look",
453
+ "i'm going to read",
454
+ "i need to inspect",
455
+ "i need to check",
456
+ ]
457
+ .iter()
458
+ .any(|needle| lower.contains(needle));
459
+
460
+ has_intent && (lower.ends_with(':') || lower.ends_with('.') || lower.ends_with("first"))
461
+ }
462
+
400
463
  fn denied_message(reason: &str) -> String {
401
464
  json!({ "ok": false, "error": reason }).to_string()
402
465
  }
@@ -668,7 +731,7 @@ async fn stream_response(
668
731
 
669
732
  loop {
670
733
  let chunk_result = response.chunk().await;
671
-
734
+
672
735
  match chunk_result {
673
736
  Ok(Some(chunk)) => {
674
737
  consecutive_errors = 0; // Reset error counter on successful read
@@ -685,12 +748,14 @@ async fn stream_response(
685
748
  // Stream ended normally
686
749
  break;
687
750
  }
688
- Err(_e) => {
751
+ Err(error) => {
689
752
  consecutive_errors += 1;
690
753
  if consecutive_errors >= MAX_CONSECUTIVE_ERRORS {
691
- // Log the error but don't fail the whole request
692
- eprintln!("\n[warning: stream interrupted after {} consecutive errors]", consecutive_errors);
693
- break;
754
+ bail!(
755
+ "stream interrupted while reading provider response after {} consecutive errors: {}",
756
+ consecutive_errors,
757
+ error
758
+ );
694
759
  }
695
760
  // Try to continue reading - transient network hiccups happen
696
761
  continue;
@@ -969,4 +1034,18 @@ mod tests {
969
1034
  .contains("Do not call tools again")
970
1035
  );
971
1036
  }
1037
+
1038
+ #[test]
1039
+ fn detects_unfinished_tool_intent() {
1040
+ assert!(looks_like_unfinished_tool_intent(
1041
+ "Let me inspect the workspace structure more thoroughly."
1042
+ ));
1043
+ assert!(looks_like_unfinished_tool_intent(
1044
+ "Let me also peek at the key files to understand the project:"
1045
+ ));
1046
+ assert!(!looks_like_unfinished_tool_intent(
1047
+ "The project is a Rust CLI with an npm wrapper."
1048
+ ));
1049
+ assert!(!looks_like_unfinished_tool_intent(""));
1050
+ }
972
1051
  }
package/src/tools.rs CHANGED
@@ -24,7 +24,9 @@ const MAX_COMMAND_TIMEOUT_SECS: u64 = 300;
24
24
  pub fn guidance(include_write: bool) -> String {
25
25
  let mut text = String::from(
26
26
  "You can use Anveesa tools to inspect the workspace: list directories, find files by name, \
27
- search text, read capped file snippets, and do a basic public web lookup. Prefer tools over guessing.",
27
+ search text, read capped file snippets, and do a basic public web lookup. Prefer tools over guessing. \
28
+ If you need to inspect, read, list, search, or check something, call the relevant tool immediately; \
29
+ do not end a response by saying you will inspect something later.",
28
30
  );
29
31
  if include_write {
30
32
  text.push_str(
@@ -58,6 +60,19 @@ pub fn describe_call(name: &str, arguments: &str) -> String {
58
60
  let args: Value = serde_json::from_str(arguments).unwrap_or(Value::Null);
59
61
  let field = |key: &str| args.get(key).and_then(Value::as_str).unwrap_or("");
60
62
  match name {
63
+ "list_dir" => format!("list directory {}", field("path").if_empty(".")),
64
+ "find_files" => format!(
65
+ "find files matching `{}` under {}",
66
+ field("query"),
67
+ field("root").if_empty(".")
68
+ ),
69
+ "search_text" => format!(
70
+ "search text `{}` under {}",
71
+ field("query"),
72
+ field("root").if_empty(".")
73
+ ),
74
+ "read_file" => format!("read file {}", field("path")),
75
+ "web_search" => format!("web search `{}`", field("query")),
61
76
  "create_dir" => format!("create directory {}", field("path")),
62
77
  "write_file" => format!("write file {}", field("path")),
63
78
  "edit_file" => format!("edit file {}", field("path")),
@@ -66,6 +81,16 @@ pub fn describe_call(name: &str, arguments: &str) -> String {
66
81
  }
67
82
  }
68
83
 
84
+ trait EmptyStrExt {
85
+ fn if_empty(self, fallback: &'static str) -> Self;
86
+ }
87
+
88
+ impl<'a> EmptyStrExt for &'a str {
89
+ fn if_empty(self, fallback: &'static str) -> Self {
90
+ if self.is_empty() { fallback } else { self }
91
+ }
92
+ }
93
+
69
94
  pub fn definitions(include_write: bool) -> Vec<Value> {
70
95
  let mut definitions = vec![
71
96
  json!({
@@ -846,6 +871,23 @@ mod tests {
846
871
 
847
872
  #[test]
848
873
  fn describes_calls_for_confirmation() {
874
+ assert_eq!(describe_call("list_dir", r#"{}"#), "list directory .");
875
+ assert_eq!(
876
+ describe_call("find_files", r#"{"query":"Cargo","root":"src"}"#),
877
+ "find files matching `Cargo` under src"
878
+ );
879
+ assert_eq!(
880
+ describe_call("search_text", r#"{"query":"TODO"}"#),
881
+ "search text `TODO` under ."
882
+ );
883
+ assert_eq!(
884
+ describe_call("read_file", r#"{"path":"README.md"}"#),
885
+ "read file README.md"
886
+ );
887
+ assert_eq!(
888
+ describe_call("web_search", r#"{"query":"rust termios"}"#),
889
+ "web search `rust termios`"
890
+ );
849
891
  assert_eq!(
850
892
  describe_call("create_dir", r#"{"path":"hello"}"#),
851
893
  "create directory hello"
@@ -863,6 +905,7 @@ mod tests {
863
905
  #[test]
864
906
  fn guidance_mentions_writes_only_when_enabled() {
865
907
  assert!(!guidance(false).contains("write_file"));
908
+ assert!(guidance(false).contains("call the relevant tool immediately"));
866
909
  assert!(guidance(true).contains("create_dir"));
867
910
  assert!(guidance(true).contains("write_file"));
868
911
  }
@@ -1,68 +0,0 @@
1
- name: Release Binaries
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
-
8
- permissions:
9
- contents: write
10
-
11
- jobs:
12
- build:
13
- strategy:
14
- matrix:
15
- include:
16
- - os: ubuntu-latest
17
- target: x86_64-unknown-linux-gnu
18
- - os: ubuntu-latest
19
- target: aarch64-unknown-linux-gnu
20
- - os: macos-latest
21
- target: x86_64-apple-darwin
22
- - os: macos-latest
23
- target: aarch64-apple-darwin
24
- - os: windows-latest
25
- target: x86_64-pc-windows-msvc
26
-
27
- runs-on: ${{ matrix.os }}
28
-
29
- steps:
30
- - uses: actions/checkout@v4
31
-
32
- - name: Install Rust
33
- uses: dtolnay/rust-action@stable
34
- with:
35
- targets: ${{ matrix.target }}
36
-
37
- - name: Install cross-compilation tools (Linux ARM64)
38
- if: matrix.target == 'aarch64-unknown-linux-gnu'
39
- run: |
40
- sudo apt-get update
41
- sudo apt-get install -y gcc-aarch64-linux-gnu
42
- echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc' >> $GITHUB_ENV
43
-
44
- - name: Build
45
- run: cargo build --release --target ${{ matrix.target }}
46
-
47
- - name: Package binary
48
- shell: bash
49
- run: |
50
- VERSION=${GITHUB_REF_NAME#v}
51
- BIN_DIR=release-artifacts
52
- mkdir -p $BIN_DIR
53
-
54
- if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
55
- cp target/${{ matrix.target }}/release/anveesa.exe $BIN_DIR/anveesa.exe
56
- cd $BIN_DIR && tar czf ../anveesa-$VERSION-${{ matrix.target }}.tar.gz anveesa.exe
57
- else
58
- cp target/${{ matrix.target }}/release/anveesa $BIN_DIR/anveesa
59
- chmod +x $BIN_DIR/anveesa
60
- cd $BIN_DIR && tar czf ../anveesa-$VERSION-${{ matrix.target }}.tar.gz anveesa
61
- fi
62
-
63
- - name: Upload to Release
64
- uses: softprops/action-gh-release@v2
65
- with:
66
- files: anveesa-*.tar.gz
67
- env:
68
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/bin/anveesa DELETED
Binary file