anveesa 0.1.1 → 0.2.1

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.
@@ -0,0 +1,68 @@
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/Cargo.lock CHANGED
@@ -54,7 +54,7 @@ dependencies = [
54
54
 
55
55
  [[package]]
56
56
  name = "anveesa"
57
- version = "0.1.0"
57
+ version = "0.2.0"
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.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2024"
5
5
  default-run = "anveesa"
6
6
 
package/README.md CHANGED
@@ -3,6 +3,25 @@
3
3
  Anveesa is a Rust terminal wrapper for AI providers. It gives you one command,
4
4
  `anveesa`, while each provider is configured as either:
5
5
 
6
+ ## 📦 Publishing to npm
7
+
8
+ Anveesa can be published as an npm package via a Node.js wrapper that invokes the Rust binary.
9
+
10
+ ### Install from npm
11
+
12
+ ```bash
13
+ npm install -g anveesa-cli
14
+ ```
15
+
16
+ ### Build and publish
17
+
18
+ ```bash
19
+ npm run build
20
+ npm publish
21
+ ```
22
+
23
+ See `package.json` in the root directory for build scripts and npm configuration.
24
+
6
25
  - `openai-compatible`: HTTP chat completions providers such as OpenRouter and other compatible gateways.
7
26
  - `command`: local CLIs such as Codex, Copilot, and Claude Code, where Anveesa spawns a command and passes the prompt.
8
27
 
@@ -148,6 +167,28 @@ OpenAI-compatible API providers:
148
167
 
149
168
  - `openai`
150
169
  - `sumopod`
170
+
171
+ ### How to change the model
172
+
173
+ You can change the model using these commands:
174
+
175
+ **Via config file:**
176
+ ```bash
177
+ anveesa config set-model "your-model"
178
+ ```
179
+
180
+ **Via command line:**
181
+ ```bash
182
+ anveesa --model "your-model" "your prompt"
183
+ ```
184
+
185
+ **Interactive mode:**
186
+ ```bash
187
+ anveesa
188
+ # Then select/change model in the prompt
189
+ ```
190
+
191
+ The model can be set per provider (e.g., `sumopod`, `openai`, `openrouter`, etc.) and can be overridden per command with `--model`.
151
192
  - `openrouter`
152
193
  - `glm`
153
194
  - `glm-coding`
package/bin/anveesa ADDED
Binary file
package/bin/anveesa.js CHANGED
@@ -1,37 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execFile } = require('child_process');
3
+ const { spawn } = require('child_process');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
 
7
- // Cari binary di beberapa kemungkinan lokasi
8
7
  function findBinary() {
9
- const candidates = [
10
- // Installed via npm (global) - binary di samping script
11
- path.join(__dirname, 'target', 'release', 'anveesa'),
12
- path.join(__dirname, 'target', 'debug', 'anveesa'),
13
- // Installed via npm local (node_modules/.bin)
14
- path.join(__dirname, '..', '..', 'target', 'release', 'anveesa'),
15
- path.join(__dirname, '..', '..', 'target', 'debug', 'anveesa'),
16
- // Development mode (cargo build)
17
- path.join(process.cwd(), 'target', 'release', 'anveesa'),
18
- path.join(process.cwd(), 'target', 'debug', 'anveesa'),
19
- ];
8
+ const platform = process.platform;
9
+ const ext = platform === 'win32' ? '.exe' : '';
20
10
 
21
- for (const p of candidates) {
22
- if (fs.existsSync(p)) {
23
- return p;
24
- }
25
- }
11
+ // Binary is shipped in the package bin/ directory
12
+ const bundled = path.join(__dirname, 'anveesa' + ext);
13
+ if (fs.existsSync(bundled)) return bundled;
26
14
 
27
- // Cek PATH environment
28
- const paths = process.env.PATH.split(path.delimiter);
29
- for (const p of paths) {
30
- const full = path.join(p, 'anveesa');
31
- if (fs.existsSync(full)) {
32
- return full;
33
- }
34
- }
15
+ // Also check for target/release (dev mode)
16
+ const devPath = path.join(__dirname, '..', 'target', 'release', 'anveesa' + ext);
17
+ if (fs.existsSync(devPath)) return devPath;
18
+
19
+ // Check if there's a sibling directory with the binary
20
+ const sibling = path.join(__dirname, 'target', 'release', 'anveesa' + ext);
21
+ if (fs.existsSync(sibling)) return sibling;
35
22
 
36
23
  return null;
37
24
  }
@@ -41,24 +28,23 @@ const binaryPath = findBinary();
41
28
  if (!binaryPath) {
42
29
  console.error('❌ anveesa binary not found.');
43
30
  console.error('');
44
- console.error('Build the Rust binary first:');
45
- console.error(' cd ' + __dirname + ' && npm run build');
46
- console.error(' # or: cargo build --release');
47
- console.error('');
31
+ console.error('Try reinstalling: npm install -g anveesa');
32
+ console.error('Or build from source: cargo build --release');
48
33
  process.exit(1);
49
34
  }
50
35
 
51
- // Spawn the Rust binary
52
36
  const args = process.argv.slice(2);
53
- const options = {
37
+ const child = spawn(binaryPath, args, {
54
38
  cwd: process.cwd(),
55
- stdio: ['inherit', 'inherit', 'inherit'],
39
+ stdio: 'inherit',
56
40
  env: { ...process.env },
57
- };
41
+ });
58
42
 
59
- try {
60
- execFile(binaryPath, args, options);
61
- } catch (error) {
43
+ child.on('error', (error) => {
62
44
  console.error('Error running anveesa:', error.message);
63
45
  process.exit(1);
64
- }
46
+ });
47
+
48
+ child.on('exit', (code) => {
49
+ process.exit(code ?? 1);
50
+ });
package/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "anveesa",
3
- "version": "0.1.1",
4
- "description": "Anveesa terminal wrapper for AI providers",
3
+ "version": "0.2.1",
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": {
7
7
  "anveesa": "bin/anveesa.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "cargo build --release",
11
- "install": "node scripts/install.js",
12
- "prepublishOnly": "npm run build",
13
- "install:bin": "cargo build --release"
11
+ "postinstall": "node scripts/install.js",
12
+ "prepublishOnly": "npm run build"
14
13
  },
15
14
  "files": [
16
15
  "bin/",
@@ -18,15 +17,29 @@
18
17
  "Cargo.toml",
19
18
  "Cargo.lock",
20
19
  "src/",
21
- "README.md"
20
+ "README.md",
21
+ ".github/"
22
+ ],
23
+ "keywords": [
24
+ "ai",
25
+ "cli",
26
+ "terminal",
27
+ "rust",
28
+ "openai",
29
+ "agent",
30
+ "copilot"
22
31
  ],
23
- "keywords": ["ai", "cli", "terminal", "rust"],
24
32
  "license": "MIT",
25
33
  "engines": {
26
34
  "node": ">=16"
27
35
  },
28
36
  "repository": {
29
37
  "type": "git",
30
- "url": "https://github.com/pandhuwibowo/anveesa-cli.git"
31
- }
38
+ "url": "git+https://github.com/pandhuwibowo/anveesa-cli.git"
39
+ },
40
+ "homepage": "https://github.com/pandhuwibowo/anveesa-cli",
41
+ "bugs": {
42
+ "url": "https://github.com/pandhuwibowo/anveesa-cli/issues"
43
+ },
44
+ "author": "Anveesa"
32
45
  }
@@ -7,49 +7,141 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const https = require('https');
10
11
  const { execSync } = require('child_process');
11
12
 
12
- function getBinaryPath() {
13
- const pkgDir = __dirname;
14
- const possiblePaths = [
15
- path.join(pkgDir, '..', 'target', 'debug', 'anveesa'),
16
- path.join(pkgDir, '..', 'target', 'release', 'anveesa'),
17
- path.join(pkgDir, 'target', 'debug', 'anveesa'),
18
- path.join(pkgDir, 'target', 'release', 'anveesa'),
19
- ];
20
-
21
- for (const p of possiblePaths) {
22
- if (fs.existsSync(p)) {
23
- return p;
24
- }
25
- }
26
-
13
+ const PACKAGE = require(path.join(__dirname, '..', 'package.json'));
14
+ const REPO = 'pandhuwibowo/anveesa-cli';
15
+ const BIN_DIR = path.join(__dirname, 'bin');
16
+
17
+ function getPlatformInfo() {
18
+ const platform = process.platform;
19
+ const arch = process.arch;
20
+
21
+ if (platform === 'darwin' && arch === 'arm64')
22
+ return { platform: 'macos', target: 'aarch64-apple-darwin', ext: '' };
23
+ if (platform === 'darwin' && arch === 'x64')
24
+ return { platform: 'macos', target: 'x86_64-apple-darwin', ext: '' };
25
+ if (platform === 'linux' && arch === 'x64')
26
+ return { platform: 'linux', target: 'x86_64-unknown-linux-gnu', ext: '' };
27
+ if (platform === 'linux' && arch === 'arm64')
28
+ return { platform: 'linux', target: 'aarch64-unknown-linux-gnu', ext: '' };
29
+ if (platform === 'win32' && arch === 'x64')
30
+ return { platform: 'windows', target: 'x86_64-pc-windows-msvc', ext: '.exe' };
31
+
27
32
  return null;
28
33
  }
29
34
 
30
- function checkAndInstall() {
31
- const binaryPath = getBinaryPath();
32
-
33
- if (binaryPath) {
34
- console.log('✓ Anveesa binary found at:', binaryPath);
35
- console.log('✓ Installation complete!');
35
+ function getBinaryUrl() {
36
+ const info = getPlatformInfo();
37
+ if (!info) return null;
38
+ const version = PACKAGE.version;
39
+ return `https://github.com/${REPO}/releases/download/v${version}/anveesa-${version}-${info.target}.tar.gz`;
40
+ }
41
+
42
+ function download(url, dest) {
43
+ 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}`));
62
+ }
63
+ }).on('error', (err) => {
64
+ fs.unlink(dest, () => {});
65
+ reject(err);
66
+ });
67
+ });
68
+ }
69
+
70
+ async function tryDownloadBinary() {
71
+ const url = getBinaryUrl();
72
+ if (!url) {
73
+ console.log('⚠ Unsupported platform:', process.platform, process.arch);
74
+ return false;
75
+ }
76
+
77
+ console.log('⬇ Downloading prebuilt binary for', process.platform, process.arch);
78
+
79
+ try {
80
+ const tarPath = path.join(__dirname, 'anveesa-bin.tar.gz');
81
+ await download(url, tarPath);
82
+
83
+ // Extract
84
+ execSync(`tar xzf "${tarPath}" -C "${BIN_DIR}"`, { stdio: 'inherit' });
85
+ fs.unlinkSync(tarPath);
86
+
87
+ // Make executable
88
+ const binary = path.join(BIN_DIR, 'anveesa');
89
+ if (fs.existsSync(binary)) {
90
+ fs.chmodSync(binary, 0o755);
91
+ }
92
+
93
+ console.log('✓ Binary downloaded successfully');
36
94
  return true;
37
- } else {
38
- console.error('✗ Anveesa binary not found.');
39
- console.error('');
40
- console.error('Build the Rust binary first:');
41
- console.error(' npm run install:bin');
42
- console.error('');
95
+ } catch (e) {
96
+ if (e.message === '404') {
97
+ console.log('ℹ No prebuilt binary for this version yet');
98
+ }
43
99
  return false;
44
100
  }
45
101
  }
46
102
 
47
- // If run directly
48
- if (require.main === module) {
49
- checkAndInstall();
103
+ function tryBuildFromSource() {
104
+ try {
105
+ console.log('⚙ Building from source (requires Rust)...');
106
+ execSync('cargo build --release', { cwd: path.join(__dirname, '..'), stdio: 'inherit' });
107
+
108
+ const src = path.join(__dirname, '..', 'target', 'release', 'anveesa');
109
+ const dest = path.join(BIN_DIR, 'anveesa');
110
+
111
+ if (fs.existsSync(src)) {
112
+ if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
113
+ fs.copyFileSync(src, dest);
114
+ fs.chmodSync(dest, 0o755);
115
+ console.log('✓ Built from source successfully');
116
+ return true;
117
+ }
118
+ } catch (e) {
119
+ console.log('⚠ Build from source failed (Rust not installed?)');
120
+ }
121
+ return false;
122
+ }
123
+
124
+ async function install() {
125
+ // 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)) {
129
+ console.log('✓ anveesa binary already installed');
130
+ return;
131
+ }
132
+
133
+ // Try download first
134
+ if (await tryDownloadBinary()) return;
135
+
136
+ // Fallback to build from source
137
+ if (tryBuildFromSource()) return;
138
+
139
+ console.error('');
140
+ console.error('✗ Could not install anveesa binary.');
141
+ console.error('');
142
+ console.error('Install Rust: https://rustup.rs/');
143
+ console.error('Then run: npm install -g anveesa');
144
+ process.exit(1);
50
145
  }
51
146
 
52
- module.exports = {
53
- getBinaryPath,
54
- checkAndInstall
55
- };
147
+ install();
@@ -46,6 +46,7 @@ pub async fn ask(
46
46
 
47
47
  let client = reqwest::Client::builder()
48
48
  .connect_timeout(CONNECT_TIMEOUT)
49
+ .read_timeout(Duration::from_secs(300)) // 5-minute read timeout for long streams
49
50
  .build()
50
51
  .context("failed to build HTTP client")?;
51
52
  let url = format!("{}/chat/completions", config.base_url.trim_end_matches('/'));
@@ -662,22 +663,42 @@ async fn stream_response(
662
663
  events: &UnboundedSender<StreamEvent>,
663
664
  ) -> Result<()> {
664
665
  let mut buffer = String::new();
666
+ let mut consecutive_errors: usize = 0;
667
+ const MAX_CONSECUTIVE_ERRORS: usize = 3;
665
668
 
666
- while let Some(chunk) = response
667
- .chunk()
668
- .await
669
- .context("failed to read streamed response chunk")?
670
- {
671
- buffer.push_str(&String::from_utf8_lossy(&chunk));
672
-
673
- while let Some(newline) = buffer.find('\n') {
674
- let line: String = buffer.drain(..=newline).collect();
675
- if let Some(token) = state.ingest_line(line.trim_end_matches(['\r', '\n'])) {
676
- let _ = events.send(StreamEvent::Token(token));
669
+ loop {
670
+ let chunk_result = response.chunk().await;
671
+
672
+ match chunk_result {
673
+ Ok(Some(chunk)) => {
674
+ consecutive_errors = 0; // Reset error counter on successful read
675
+ buffer.push_str(&String::from_utf8_lossy(&chunk));
676
+
677
+ while let Some(newline) = buffer.find('\n') {
678
+ let line: String = buffer.drain(..=newline).collect();
679
+ if let Some(token) = state.ingest_line(line.trim_end_matches(['\r', '\n'])) {
680
+ let _ = events.send(StreamEvent::Token(token));
681
+ }
682
+ }
683
+ }
684
+ Ok(None) => {
685
+ // Stream ended normally
686
+ break;
687
+ }
688
+ Err(_e) => {
689
+ consecutive_errors += 1;
690
+ 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;
694
+ }
695
+ // Try to continue reading - transient network hiccups happen
696
+ continue;
677
697
  }
678
698
  }
679
699
  }
680
700
 
701
+ // Process any remaining data in the buffer
681
702
  if !buffer.is_empty()
682
703
  && let Some(token) = state.ingest_line(buffer.trim())
683
704
  {
@@ -716,7 +737,10 @@ impl StreamState {
716
737
  self.done = true;
717
738
  return None;
718
739
  }
719
- let chunk: Value = serde_json::from_str(data).ok()?;
740
+ let chunk: Value = match serde_json::from_str(data) {
741
+ Ok(v) => v,
742
+ Err(_) => return None, // Skip malformed lines instead of bailing
743
+ };
720
744
  self.apply_chunk(&chunk)
721
745
  }
722
746
 
@@ -725,7 +749,15 @@ impl StreamState {
725
749
  self.usage = parse_usage(usage);
726
750
  }
727
751
 
728
- let delta = chunk.get("choices")?.get(0)?.get("delta")?;
752
+ let Some(choices) = chunk.get("choices") else {
753
+ return None;
754
+ };
755
+ let Some(first_choice) = choices.get(0) else {
756
+ return None;
757
+ };
758
+ let Some(delta) = first_choice.get("delta") else {
759
+ return None;
760
+ };
729
761
 
730
762
  if let Some(tool_calls) = delta.get("tool_calls").and_then(Value::as_array) {
731
763
  for call in tool_calls {
@@ -733,7 +765,8 @@ impl StreamState {
733
765
  }
734
766
  }
735
767
 
736
- let text = delta.get("content").and_then(Value::as_str)?;
768
+ // Content may be absent (e.g., tool-call-only chunks) — don't bail.
769
+ let text = delta.get("content").and_then(Value::as_str).unwrap_or("");
737
770
  if text.is_empty() {
738
771
  return None;
739
772
  }