@zemerik/gemini-assist 1.1.1-beta

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,159 @@
1
+ use anyhow::{Context, Result};
2
+ use serde::{Deserialize, Serialize};
3
+
4
+ #[derive(Debug, Clone, Serialize, Deserialize)]
5
+ pub struct ChatMessage {
6
+ pub role: String,
7
+ pub parts: Vec<Part>,
8
+ }
9
+
10
+ #[derive(Debug, Clone, Serialize, Deserialize)]
11
+ pub struct Part {
12
+ pub text: String,
13
+ }
14
+
15
+ #[derive(Debug, Clone, Serialize, Deserialize)]
16
+ struct GenerateContentRequest {
17
+ contents: Vec<Content>,
18
+ generation_config: Option<GenerationConfig>,
19
+ }
20
+
21
+ #[derive(Debug, Clone, Serialize, Deserialize)]
22
+ struct Content {
23
+ role: String,
24
+ parts: Vec<Part>,
25
+ }
26
+
27
+ #[derive(Debug, Clone, Serialize, Deserialize)]
28
+ struct GenerationConfig {
29
+ temperature: f64,
30
+ }
31
+
32
+ #[derive(Debug, Clone, Serialize, Deserialize)]
33
+ struct GenerateContentResponse {
34
+ candidates: Vec<Candidate>,
35
+ }
36
+
37
+ #[derive(Debug, Clone, Serialize, Deserialize)]
38
+ struct Candidate {
39
+ content: Content,
40
+ }
41
+
42
+ pub struct GeminiClient {
43
+ api_key: String,
44
+ model_name: String,
45
+ temperature: f64,
46
+ base_url: String,
47
+ chat_history: Vec<ChatMessage>,
48
+ }
49
+
50
+ impl GeminiClient {
51
+ pub fn new(api_key: String, model_name: String, temperature: f64) -> Self {
52
+ let base_url = format!(
53
+ "https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent",
54
+ model_name
55
+ );
56
+
57
+ GeminiClient {
58
+ api_key,
59
+ model_name,
60
+ temperature,
61
+ base_url,
62
+ chat_history: Vec::new(),
63
+ }
64
+ }
65
+
66
+ pub async fn chat(&mut self, prompt: &str) -> Result<String> {
67
+ // Add user message to history
68
+ self.chat_history.push(ChatMessage {
69
+ role: "user".to_string(),
70
+ parts: vec![Part {
71
+ text: prompt.to_string(),
72
+ }],
73
+ });
74
+
75
+ // Build request with chat history
76
+ let contents: Vec<Content> = self
77
+ .chat_history
78
+ .iter()
79
+ .map(|msg| Content {
80
+ role: msg.role.clone(),
81
+ parts: msg.parts.clone(),
82
+ })
83
+ .collect();
84
+
85
+ let request = GenerateContentRequest {
86
+ contents,
87
+ generation_config: Some(GenerationConfig {
88
+ temperature: self.temperature,
89
+ }),
90
+ };
91
+
92
+ // Make API call
93
+ let client = reqwest::Client::new();
94
+ let url = format!("{}?key={}", self.base_url, self.api_key);
95
+
96
+ let response = client
97
+ .post(&url)
98
+ .json(&request)
99
+ .send()
100
+ .await
101
+ .context("Failed to send request to Gemini API")?;
102
+
103
+ // Save status before consuming response body
104
+ let status = response.status();
105
+
106
+ // Handle errors
107
+ if status.is_client_error() {
108
+ let error_text = response.text().await.unwrap_or_default();
109
+ if error_text.contains("API_KEY") || status == 401 {
110
+ anyhow::bail!("Invalid API key. Please check your GEMINI_API_KEY.");
111
+ } else if error_text.contains("quota") || error_text.contains("rate limit") {
112
+ anyhow::bail!("API quota exceeded or rate limit reached. Please try again later.");
113
+ } else if error_text.contains("safety") {
114
+ anyhow::bail!("Content was blocked by safety filters.");
115
+ } else {
116
+ anyhow::bail!("Gemini API error: {}", error_text);
117
+ }
118
+ }
119
+
120
+ if !status.is_success() {
121
+ let error_text = response.text().await.unwrap_or_default();
122
+ anyhow::bail!("HTTP error {}: {}", status, error_text);
123
+ }
124
+
125
+ let api_response: GenerateContentResponse = response
126
+ .json()
127
+ .await
128
+ .context("Failed to parse Gemini API response")?;
129
+
130
+ if api_response.candidates.is_empty() {
131
+ anyhow::bail!("No response candidates from Gemini API");
132
+ }
133
+
134
+ let response_text = api_response.candidates[0]
135
+ .content
136
+ .parts
137
+ .first()
138
+ .map(|p| p.text.clone())
139
+ .unwrap_or_default();
140
+
141
+ // Add assistant response to history
142
+ self.chat_history.push(ChatMessage {
143
+ role: "model".to_string(),
144
+ parts: vec![Part {
145
+ text: response_text.clone(),
146
+ }],
147
+ });
148
+
149
+ Ok(response_text)
150
+ }
151
+
152
+ pub fn clear_history(&mut self) {
153
+ self.chat_history.clear();
154
+ }
155
+
156
+ pub fn get_history_count(&self) -> usize {
157
+ self.chat_history.len()
158
+ }
159
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,58 @@
1
+ pub mod gemini_client;
2
+
3
+ use gemini_client::GeminiClient;
4
+ use napi::bindgen_prelude::*;
5
+ use napi_derive::napi;
6
+ use std::sync::Arc;
7
+ use tokio::sync::Mutex;
8
+
9
+ #[napi]
10
+ pub struct RustGeminiClient {
11
+ client: Arc<Mutex<GeminiClient>>,
12
+ }
13
+
14
+ #[napi]
15
+ impl RustGeminiClient {
16
+ #[napi(constructor)]
17
+ pub fn new(api_key: String, model_name: Option<String>, temperature: Option<f64>) -> Self {
18
+ let model = model_name.unwrap_or_else(|| "gemini-2.5-flash".to_string());
19
+ let temp = temperature.unwrap_or(0.7);
20
+ let client = GeminiClient::new(api_key, model, temp);
21
+
22
+ RustGeminiClient {
23
+ client: Arc::new(Mutex::new(client)),
24
+ }
25
+ }
26
+
27
+ #[napi]
28
+ pub async fn chat(&self, prompt: String) -> Result<String> {
29
+ let mut client = self.client.lock().await;
30
+ client
31
+ .chat(&prompt)
32
+ .await
33
+ .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))
34
+ }
35
+
36
+ #[napi]
37
+ pub async fn clear_history(&self) -> Result<()> {
38
+ let mut client = self.client.lock().await;
39
+ client.clear_history();
40
+ Ok(())
41
+ }
42
+
43
+ #[napi]
44
+ pub async fn get_history_count(&self) -> Result<u32> {
45
+ let client = self.client.lock().await;
46
+ Ok(client.get_history_count() as u32)
47
+ }
48
+ }
49
+
50
+ #[napi]
51
+ pub fn validate_api_key_rust(api_key: String) -> bool {
52
+ api_key.len() > 10 && api_key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
53
+ }
54
+
55
+ #[napi]
56
+ pub fn rust_version() -> String {
57
+ format!("Rust-powered Gemini Client v1.1.1-Beta")
58
+ }
@@ -0,0 +1,157 @@
1
+ use anyhow::{Context, Result};
2
+ use serde::{Deserialize, Serialize};
3
+ use std::collections::HashMap;
4
+
5
+ #[derive(Debug, Clone, Serialize, Deserialize)]
6
+ pub struct ChatMessage {
7
+ pub role: String,
8
+ pub parts: Vec<Part>,
9
+ }
10
+
11
+ #[derive(Debug, Clone, Serialize, Deserialize)]
12
+ pub struct Part {
13
+ pub text: String,
14
+ }
15
+
16
+ #[derive(Debug, Clone, Serialize, Deserialize)]
17
+ struct GenerateContentRequest {
18
+ contents: Vec<Content>,
19
+ generation_config: Option<GenerationConfig>,
20
+ }
21
+
22
+ #[derive(Debug, Clone, Serialize, Deserialize)]
23
+ struct Content {
24
+ role: String,
25
+ parts: Vec<Part>,
26
+ }
27
+
28
+ #[derive(Debug, Clone, Serialize, Deserialize)]
29
+ struct GenerationConfig {
30
+ temperature: f64,
31
+ }
32
+
33
+ #[derive(Debug, Clone, Serialize, Deserialize)]
34
+ struct GenerateContentResponse {
35
+ candidates: Vec<Candidate>,
36
+ }
37
+
38
+ #[derive(Debug, Clone, Serialize, Deserialize)]
39
+ struct Candidate {
40
+ content: Content,
41
+ }
42
+
43
+ pub struct GeminiClient {
44
+ api_key: String,
45
+ model_name: String,
46
+ temperature: f64,
47
+ base_url: String,
48
+ chat_history: Vec<ChatMessage>,
49
+ }
50
+
51
+ impl GeminiClient {
52
+ pub fn new(api_key: String, model_name: String, temperature: f64) -> Self {
53
+ let base_url = format!(
54
+ "https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent",
55
+ model_name
56
+ );
57
+
58
+ GeminiClient {
59
+ api_key,
60
+ model_name,
61
+ temperature,
62
+ base_url,
63
+ chat_history: Vec::new(),
64
+ }
65
+ }
66
+
67
+ pub async fn chat(&mut self, prompt: &str) -> Result<String> {
68
+ // Add user message to history
69
+ self.chat_history.push(ChatMessage {
70
+ role: "user".to_string(),
71
+ parts: vec![Part {
72
+ text: prompt.to_string(),
73
+ }],
74
+ });
75
+
76
+ // Build request with chat history
77
+ let contents: Vec<Content> = self
78
+ .chat_history
79
+ .iter()
80
+ .map(|msg| Content {
81
+ role: msg.role.clone(),
82
+ parts: msg.parts.clone(),
83
+ })
84
+ .collect();
85
+
86
+ let request = GenerateContentRequest {
87
+ contents,
88
+ generation_config: Some(GenerationConfig {
89
+ temperature: self.temperature,
90
+ }),
91
+ };
92
+
93
+ // Make API call
94
+ let client = reqwest::Client::new();
95
+ let url = format!("{}?key={}", self.base_url, self.api_key);
96
+
97
+ let response = client
98
+ .post(&url)
99
+ .json(&request)
100
+ .send()
101
+ .await
102
+ .context("Failed to send request to Gemini API")?;
103
+
104
+ // Handle errors
105
+ if response.status().is_client_error() {
106
+ let error_text = response.text().await.unwrap_or_default();
107
+ if error_text.contains("API_KEY") || response.status() == 401 {
108
+ anyhow::bail!("Invalid API key. Please check your GEMINI_API_KEY.");
109
+ } else if error_text.contains("quota") || error_text.contains("rate limit") {
110
+ anyhow::bail!("API quota exceeded or rate limit reached. Please try again later.");
111
+ } else if error_text.contains("safety") {
112
+ anyhow::bail!("Content was blocked by safety filters.");
113
+ } else {
114
+ anyhow::bail!("Gemini API error: {}", error_text);
115
+ }
116
+ }
117
+
118
+ if !response.status().is_success() {
119
+ let error_text = response.text().await.unwrap_or_default();
120
+ anyhow::bail!("HTTP error {}: {}", response.status(), error_text);
121
+ }
122
+
123
+ let api_response: GenerateContentResponse = response
124
+ .json()
125
+ .await
126
+ .context("Failed to parse Gemini API response")?;
127
+
128
+ if api_response.candidates.is_empty() {
129
+ anyhow::bail!("No response candidates from Gemini API");
130
+ }
131
+
132
+ let response_text = api_response.candidates[0]
133
+ .content
134
+ .parts
135
+ .first()
136
+ .map(|p| p.text.clone())
137
+ .unwrap_or_default();
138
+
139
+ // Add assistant response to history
140
+ self.chat_history.push(ChatMessage {
141
+ role: "model".to_string(),
142
+ parts: vec![Part {
143
+ text: response_text.clone(),
144
+ }],
145
+ });
146
+
147
+ Ok(response_text)
148
+ }
149
+
150
+ pub fn clear_history(&mut self) {
151
+ self.chat_history.clear();
152
+ }
153
+
154
+ pub fn get_history_count(&self) -> usize {
155
+ self.chat_history.len()
156
+ }
157
+ }
@@ -0,0 +1,57 @@
1
+ mod gemini_client;
2
+
3
+ use gemini_client::GeminiClient;
4
+ use napi::bindgen_prelude::*;
5
+ use napi_derive::napi;
6
+ use std::sync::Arc;
7
+ use tokio::sync::Mutex;
8
+
9
+ #[napi]
10
+ pub struct RustGeminiClient {
11
+ client: Arc<Mutex<GeminiClient>>,
12
+ }
13
+
14
+ #[napi]
15
+ impl RustGeminiClient {
16
+ #[napi(constructor)]
17
+ pub fn new(api_key: String, model_name: Option<String>, temperature: Option<f64>) -> Self {
18
+ let model = model_name.unwrap_or_else(|| "gemini-pro".to_string());
19
+ let temp = temperature.unwrap_or(0.7);
20
+ let client = GeminiClient::new(api_key, model, temp);
21
+
22
+ RustGeminiClient {
23
+ client: Arc::new(Mutex::new(client)),
24
+ }
25
+ }
26
+
27
+ #[napi]
28
+ pub async fn chat(&self, prompt: String) -> Result<String> {
29
+ let mut client = self.client.lock().await;
30
+ client
31
+ .chat(&prompt)
32
+ .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))
33
+ }
34
+
35
+ #[napi]
36
+ pub async fn clear_history(&self) -> Result<()> {
37
+ let mut client = self.client.lock().await;
38
+ client.clear_history();
39
+ Ok(())
40
+ }
41
+
42
+ #[napi]
43
+ pub async fn get_history_count(&self) -> Result<u32> {
44
+ let client = self.client.lock().await;
45
+ Ok(client.get_history_count() as u32)
46
+ }
47
+ }
48
+
49
+ #[napi]
50
+ pub fn validate_api_key_rust(api_key: String) -> bool {
51
+ api_key.len() > 10 && api_key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
52
+ }
53
+
54
+ #[napi]
55
+ pub fn rust_version() -> String {
56
+ format!("Rust-powered Gemini Client v1.1.1-Beta")
57
+ }
package/src/utils.js ADDED
@@ -0,0 +1,62 @@
1
+ const chalk = require('chalk');
2
+
3
+ // Try to use Rust validation, fallback to JS
4
+ let validateApiKeyRust;
5
+ try {
6
+ const native = require('../../index.node');
7
+ validateApiKeyRust = native.validate_api_key_rust;
8
+ } catch (error) {
9
+ validateApiKeyRust = null;
10
+ }
11
+
12
+ function validateApiKey(apiKey) {
13
+ if (!apiKey || typeof apiKey !== 'string') {
14
+ return false;
15
+ }
16
+
17
+ // Use Rust validation if available, otherwise JS
18
+ if (validateApiKeyRust) {
19
+ return validateApiKeyRust(apiKey);
20
+ }
21
+
22
+ // JS fallback
23
+ return apiKey.length > 10 && /^[A-Za-z0-9_-]+$/.test(apiKey);
24
+ }
25
+
26
+ function printWelcome() {
27
+ // Check if Rust is available
28
+ let rustInfo = '';
29
+ try {
30
+ const native = require('../../index.node');
31
+ const rustVersion = native.rust_version();
32
+ rustInfo = chalk.green(' (Rust-powered)');
33
+ } catch (error) {
34
+ rustInfo = chalk.yellow(' (JS mode)');
35
+ }
36
+
37
+ console.clear();
38
+ console.log(chalk.bold.cyan('╔═══════════════════════════════════════╗'));
39
+ console.log(chalk.bold.cyan('║ ') + chalk.bold.white('Gemini Assist v1.1.1-Beta') + rustInfo + chalk.bold.cyan(' ║'));
40
+ console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝'));
41
+ console.log(chalk.gray('AI Assistant powered by Google Gemini\n'));
42
+ }
43
+
44
+ function printError(message) {
45
+ console.error(chalk.red.bold('✗ Error: ') + chalk.red(message));
46
+ }
47
+
48
+ function printSuccess(message) {
49
+ console.log(chalk.green.bold('✓ ') + chalk.green(message));
50
+ }
51
+
52
+ function printInfo(message) {
53
+ console.log(chalk.blue.bold('ℹ ') + chalk.blue(message));
54
+ }
55
+
56
+ module.exports = {
57
+ validateApiKey,
58
+ printWelcome,
59
+ printError,
60
+ printSuccess,
61
+ printInfo
62
+ };