@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.
- package/.env.example +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +35 -0
- package/.github/ISSUE_TEMPLATE/question.md +29 -0
- package/.github/dependabot.yml +49 -0
- package/.github/labels.yml +129 -0
- package/.github/pull_request_template.md +46 -0
- package/.github/stale.yml +25 -0
- package/.github/workflows/ci.yml +192 -0
- package/.github/workflows/release.yml +71 -0
- package/.rustignore +5 -0
- package/Cargo.toml +19 -0
- package/LICENSE +0 -0
- package/README.md +220 -0
- package/README_RUST.md +134 -0
- package/RELEASE_NOTES.md +124 -0
- package/bin/gemini-assist.js +136 -0
- package/build.rs +3 -0
- package/gitignore +1 -0
- package/index.d.ts +13 -0
- package/index.js +317 -0
- package/napi.config.json +33 -0
- package/package.json +63 -0
- package/scripts/postinstall.js +11 -0
- package/src/gemini.js +127 -0
- package/src/gemini_client.rs +159 -0
- package/src/lib.rs +58 -0
- package/src/rust/gemini_client.rs +157 -0
- package/src/rust/lib.rs +57 -0
- package/src/utils.js +62 -0
|
@@ -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
|
+
}
|
package/src/rust/lib.rs
ADDED
|
@@ -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
|
+
};
|