@pheem49/mint 1.3.0 → 1.4.0
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/README.md +28 -24
- package/mint-cli.js +201 -26
- package/package.json +13 -2
- package/src/AI_Brain/Gemini_API.js +299 -46
- package/src/AI_Brain/agent_orchestrator.js +73 -0
- package/src/AI_Brain/autonomous_brain.js +2 -0
- package/src/AI_Brain/memory_store.js +318 -0
- package/src/CLI/chat_router.js +17 -2
- package/src/CLI/chat_ui.js +83 -1
- package/src/CLI/code_agent.js +143 -30
- package/src/CLI/onboarding.js +53 -6
- package/src/CLI/workspace_manager.js +81 -0
- package/src/Plugins/spotify.js +168 -40
- package/src/Plugins/system_monitor.js +72 -0
- package/src/System/config_manager.js +35 -2
- package/src/System/notifications.js +23 -0
- package/src/UI/settings.html +143 -65
- package/src/UI/settings.js +155 -41
- package/tests/agent_orchestrator.test.js +41 -0
- package/tests/config_manager.test.js +141 -0
- package/tests/memory_store.test.js +185 -0
- package/tests/spotify.test.js +201 -0
- package/tests/system_monitor.test.js +37 -0
- package/tests/workspace_manager.test.js +41 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint Long-Term Memory Store
|
|
3
|
+
* ---------------------------
|
|
4
|
+
* Persists user preferences, session summaries, and usage patterns
|
|
5
|
+
* across all Mint sessions using SQLite (same DB as knowledge_base).
|
|
6
|
+
*
|
|
7
|
+
* Auto-injects a "User Context" block into the system prompt so Mint
|
|
8
|
+
* remembers who it's talking to even after restart.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { readConfig } = require('../System/config_manager');
|
|
16
|
+
|
|
17
|
+
// ── Electron-safe app path ──────────────────────────────────────────────────
|
|
18
|
+
let app;
|
|
19
|
+
try {
|
|
20
|
+
const electron = require('electron');
|
|
21
|
+
app = electron.app;
|
|
22
|
+
} catch (_) {
|
|
23
|
+
app = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getDbPath() {
|
|
27
|
+
const fileName = 'mint-knowledge.sqlite'; // shared DB with knowledge_base
|
|
28
|
+
if (app && app.getPath) {
|
|
29
|
+
return path.join(app.getPath('userData'), fileName);
|
|
30
|
+
}
|
|
31
|
+
const mintDir = path.join(os.homedir(), '.mint');
|
|
32
|
+
if (!fs.existsSync(mintDir)) fs.mkdirSync(mintDir, { recursive: true });
|
|
33
|
+
return path.join(mintDir, fileName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Lazy DatabaseSync init ─────────────────────────────────────────────────
|
|
37
|
+
let DatabaseSync = null;
|
|
38
|
+
function getDatabaseSync() {
|
|
39
|
+
if (!DatabaseSync) ({ DatabaseSync } = require('node:sqlite'));
|
|
40
|
+
return DatabaseSync;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let dbInstance = null;
|
|
44
|
+
function getDb() {
|
|
45
|
+
if (dbInstance) return dbInstance;
|
|
46
|
+
const Database = getDatabaseSync();
|
|
47
|
+
dbInstance = new Database(getDbPath());
|
|
48
|
+
|
|
49
|
+
dbInstance.exec(`
|
|
50
|
+
-- User profile: arbitrary key-value pairs
|
|
51
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
52
|
+
key TEXT PRIMARY KEY,
|
|
53
|
+
value TEXT,
|
|
54
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Condensed summaries of past sessions
|
|
58
|
+
CREATE TABLE IF NOT EXISTS session_memories (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
summary TEXT NOT NULL,
|
|
61
|
+
tags TEXT DEFAULT '',
|
|
62
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- Frequently used topics / commands
|
|
66
|
+
CREATE TABLE IF NOT EXISTS usage_patterns (
|
|
67
|
+
pattern TEXT PRIMARY KEY,
|
|
68
|
+
count INTEGER DEFAULT 1,
|
|
69
|
+
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- Response Cache: For repetitive exact queries
|
|
73
|
+
CREATE TABLE IF NOT EXISTS response_cache (
|
|
74
|
+
query_hash TEXT PRIMARY KEY,
|
|
75
|
+
response TEXT NOT NULL,
|
|
76
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
return dbInstance;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Profile helpers ────────────────────────────────────────────────────────
|
|
84
|
+
function setProfile(key, value) {
|
|
85
|
+
try {
|
|
86
|
+
const db = getDb();
|
|
87
|
+
db.prepare(`
|
|
88
|
+
INSERT INTO user_profile (key, value, updated_at)
|
|
89
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
90
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
|
91
|
+
`).run(key, String(value));
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('[Memory] setProfile error:', err.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getProfile(key, defaultValue = null) {
|
|
98
|
+
try {
|
|
99
|
+
const row = getDb().prepare('SELECT value FROM user_profile WHERE key = ?').get(key);
|
|
100
|
+
return row ? row.value : defaultValue;
|
|
101
|
+
} catch (_) {
|
|
102
|
+
return defaultValue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getAllProfile() {
|
|
107
|
+
try {
|
|
108
|
+
const rows = getDb().prepare('SELECT key, value FROM user_profile').all();
|
|
109
|
+
return Object.fromEntries(rows.map(r => [r.key, r.value]));
|
|
110
|
+
} catch (_) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Session memory helpers ─────────────────────────────────────────────────
|
|
116
|
+
const MAX_SESSION_MEMORIES = 20; // keep last N summaries
|
|
117
|
+
|
|
118
|
+
function addSessionMemory(summary, tags = []) {
|
|
119
|
+
try {
|
|
120
|
+
const db = getDb();
|
|
121
|
+
db.prepare('INSERT INTO session_memories (summary, tags) VALUES (?, ?)').run(
|
|
122
|
+
summary.slice(0, 800), // cap length
|
|
123
|
+
tags.join(',')
|
|
124
|
+
);
|
|
125
|
+
// Prune oldest beyond limit
|
|
126
|
+
db.exec(`
|
|
127
|
+
DELETE FROM session_memories WHERE id NOT IN (
|
|
128
|
+
SELECT id FROM session_memories ORDER BY id DESC LIMIT ${MAX_SESSION_MEMORIES}
|
|
129
|
+
)
|
|
130
|
+
`);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[Memory] addSessionMemory error:', err.message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getRecentMemories(limit = 5) {
|
|
137
|
+
try {
|
|
138
|
+
return getDb()
|
|
139
|
+
.prepare('SELECT summary, tags, created_at FROM session_memories ORDER BY id DESC LIMIT ?')
|
|
140
|
+
.all(limit);
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Usage pattern helpers ──────────────────────────────────────────────────
|
|
147
|
+
function recordPattern(pattern) {
|
|
148
|
+
try {
|
|
149
|
+
const db = getDb();
|
|
150
|
+
db.prepare(`
|
|
151
|
+
INSERT INTO usage_patterns (pattern, count, last_used)
|
|
152
|
+
VALUES (?, 1, CURRENT_TIMESTAMP)
|
|
153
|
+
ON CONFLICT(pattern) DO UPDATE
|
|
154
|
+
SET count = count + 1, last_used = CURRENT_TIMESTAMP
|
|
155
|
+
`).run(pattern.slice(0, 120));
|
|
156
|
+
} catch (_) {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getTopPatterns(limit = 8) {
|
|
160
|
+
try {
|
|
161
|
+
return getDb()
|
|
162
|
+
.prepare('SELECT pattern, count FROM usage_patterns ORDER BY count DESC, last_used DESC LIMIT ?')
|
|
163
|
+
.all(limit);
|
|
164
|
+
} catch (_) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Simple keyword extractor (no external deps) ────────────────────────────
|
|
170
|
+
const STOP_WORDS = new Set([
|
|
171
|
+
'ที่', 'ให้', 'และ', 'ของ', 'กับ', 'ใน', 'บน', 'เป็น', 'อยู่', 'มี', 'ได้', 'the', 'a', 'an',
|
|
172
|
+
'is', 'are', 'was', 'were', 'it', 'in', 'on', 'at', 'for', 'to', 'of', 'with', 'and', 'or',
|
|
173
|
+
'this', 'that', 'i', 'you', 'me', 'my', 'your', 'can', 'do', 'be', 'will', 'please', 'how',
|
|
174
|
+
'what', 'which', 'when', 'where', 'why', 'help', 'want', 'need', 'make', 'create', 'get', 'run'
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
function extractKeywords(text) {
|
|
178
|
+
return text
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^\w\u0E00-\u0E7F\s]/g, ' ')
|
|
181
|
+
.split(/\s+/)
|
|
182
|
+
.filter(w => w.length > 2 && !STOP_WORDS.has(w))
|
|
183
|
+
.slice(0, 6);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Main public API ────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Called after every successful chat turn.
|
|
190
|
+
* Extracts patterns & infers preferences — runs async, non-blocking.
|
|
191
|
+
*/
|
|
192
|
+
function recordInteraction(userMessage, aiResponseText) {
|
|
193
|
+
try {
|
|
194
|
+
if (!userMessage || !aiResponseText) return;
|
|
195
|
+
|
|
196
|
+
// Extract keywords as usage patterns
|
|
197
|
+
const keywords = extractKeywords(userMessage);
|
|
198
|
+
keywords.forEach(kw => recordPattern(kw));
|
|
199
|
+
|
|
200
|
+
// Detect preferred language
|
|
201
|
+
const thaiRatio = (userMessage.match(/[\u0E00-\u0E7F]/g) || []).length / userMessage.length;
|
|
202
|
+
if (thaiRatio > 0.3) setProfile('preferred_language', 'thai');
|
|
203
|
+
else setProfile('preferred_language', 'english');
|
|
204
|
+
|
|
205
|
+
// Detect coding intent (update project activity)
|
|
206
|
+
const codingKeywords = ['code', 'fix', 'debug', 'function', 'class', 'import', 'script',
|
|
207
|
+
'แก้', 'เขียน', 'โค้ด', 'สคริปต์', 'ฟังก์ชัน'];
|
|
208
|
+
if (codingKeywords.some(k => userMessage.toLowerCase().includes(k))) {
|
|
209
|
+
const cwd = process.cwd();
|
|
210
|
+
if (cwd !== os.homedir()) {
|
|
211
|
+
setProfile('last_active_project', path.basename(cwd));
|
|
212
|
+
setProfile('last_active_project_path', cwd);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update interaction counter
|
|
217
|
+
const count = parseInt(getProfile('total_interactions', '0'), 10);
|
|
218
|
+
setProfile('total_interactions', String(count + 1));
|
|
219
|
+
setProfile('last_seen', new Date().toISOString());
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error('[Memory] recordInteraction error:', err.message);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Saves a condensed summary of a completed conversation.
|
|
227
|
+
* Call this when user clears history or after N turns.
|
|
228
|
+
*/
|
|
229
|
+
function saveSessionSummary(summary, tags = []) {
|
|
230
|
+
if (!summary || summary.trim().length < 10) return;
|
|
231
|
+
addSessionMemory(summary.trim(), tags);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns a formatted context string to inject into the AI system prompt.
|
|
236
|
+
* Lightweight — no async calls.
|
|
237
|
+
*/
|
|
238
|
+
function getUserContext() {
|
|
239
|
+
try {
|
|
240
|
+
const profile = getAllProfile();
|
|
241
|
+
const patterns = getTopPatterns(6);
|
|
242
|
+
const memories = getRecentMemories(3);
|
|
243
|
+
|
|
244
|
+
const lines = ['\n\n[LONG-TERM USER CONTEXT — use this to personalize responses]'];
|
|
245
|
+
|
|
246
|
+
// Profile info
|
|
247
|
+
if (Object.keys(profile).length > 0) {
|
|
248
|
+
if (profile.preferred_language)
|
|
249
|
+
lines.push(`• Preferred language: ${profile.preferred_language}`);
|
|
250
|
+
if (profile.last_active_project)
|
|
251
|
+
lines.push(`• Last active project: ${profile.last_active_project} (${profile.last_active_project_path || ''})`);
|
|
252
|
+
if (profile.total_interactions)
|
|
253
|
+
lines.push(`• Total interactions with Mint: ${profile.total_interactions}`);
|
|
254
|
+
if (profile.last_seen) {
|
|
255
|
+
const d = new Date(profile.last_seen);
|
|
256
|
+
lines.push(`• Last session: ${d.toLocaleDateString('th-TH')} ${d.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' })}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Usage patterns
|
|
261
|
+
if (patterns.length > 0) {
|
|
262
|
+
const topTopics = patterns.map(p => p.pattern).join(', ');
|
|
263
|
+
lines.push(`• Frequent topics/tools: ${topTopics}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Past session memories
|
|
267
|
+
if (memories.length > 0) {
|
|
268
|
+
lines.push('\nRecent session summaries:');
|
|
269
|
+
memories.forEach((m, i) => lines.push(` ${i + 1}. ${m.summary}`));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (lines.length === 1) return ''; // nothing to add
|
|
273
|
+
lines.push('[END USER CONTEXT]\n');
|
|
274
|
+
return lines.join('\n');
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('[Memory] getUserContext error:', err.message);
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Response Cache helpers ────────────────────────────────────────────────
|
|
282
|
+
function getCachedResponse(query) {
|
|
283
|
+
try {
|
|
284
|
+
const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
|
|
285
|
+
const row = getDb().prepare('SELECT response, created_at FROM response_cache WHERE query_hash = ?').get(hash);
|
|
286
|
+
if (row) {
|
|
287
|
+
// Optional: check TTL (e.g., 24 hours)
|
|
288
|
+
const age = Date.now() - new Date(row.created_at).getTime();
|
|
289
|
+
if (age < 24 * 60 * 60 * 1000) {
|
|
290
|
+
return JSON.parse(row.response);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch (_) {}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cacheResponse(query, responseObj) {
|
|
298
|
+
try {
|
|
299
|
+
const hash = crypto.createHash('md5').update(query.trim().toLowerCase()).digest('hex');
|
|
300
|
+
getDb().prepare(`
|
|
301
|
+
INSERT INTO response_cache (query_hash, response, created_at)
|
|
302
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
303
|
+
ON CONFLICT(query_hash) DO UPDATE SET response = excluded.response, created_at = CURRENT_TIMESTAMP
|
|
304
|
+
`).run(hash, JSON.stringify(responseObj));
|
|
305
|
+
} catch (_) {}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
recordInteraction,
|
|
310
|
+
saveSessionSummary,
|
|
311
|
+
getUserContext,
|
|
312
|
+
setProfile,
|
|
313
|
+
getProfile,
|
|
314
|
+
getTopPatterns,
|
|
315
|
+
getRecentMemories,
|
|
316
|
+
getCachedResponse,
|
|
317
|
+
cacheResponse
|
|
318
|
+
};
|
package/src/CLI/chat_router.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { GoogleGenAI } = require('@google/genai');
|
|
4
4
|
const { executeCodeTask } = require('./code_agent');
|
|
5
|
-
const { readConfig } = require('../System/config_manager');
|
|
5
|
+
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
8
8
|
|
|
@@ -128,7 +128,21 @@ async function runChatRoutedTask(input, context) {
|
|
|
128
128
|
const text = input.startsWith('/code ') ? input.slice('/code '.length).trim() : input;
|
|
129
129
|
const { appendMessage, setThinking, requestApproval, setMode } = context;
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
const config = readConfig();
|
|
132
|
+
const availableProviders = getAvailableProviders(config);
|
|
133
|
+
|
|
134
|
+
// Smart Routing Priority for Code Tasks
|
|
135
|
+
let preferredProvider = config.aiProvider || 'gemini';
|
|
136
|
+
|
|
137
|
+
// If preferred isn't actually available, try best available
|
|
138
|
+
if (!availableProviders.includes(preferredProvider)) {
|
|
139
|
+
if (availableProviders.includes('anthropic')) preferredProvider = 'anthropic';
|
|
140
|
+
else if (availableProviders.includes('openai')) preferredProvider = 'openai';
|
|
141
|
+
else if (availableProviders.includes('gemini')) preferredProvider = 'gemini';
|
|
142
|
+
else preferredProvider = availableProviders[0] || 'gemini';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
appendMessage('system', `Routing this request to Code Mode for workspace: ${process.cwd()} using [${preferredProvider}]`);
|
|
132
146
|
if (setMode) setMode('Code');
|
|
133
147
|
|
|
134
148
|
let seconds = 0;
|
|
@@ -142,6 +156,7 @@ async function runChatRoutedTask(input, context) {
|
|
|
142
156
|
const result = await executeCodeTask(text, {
|
|
143
157
|
cwd: process.cwd(),
|
|
144
158
|
requestApproval,
|
|
159
|
+
provider: preferredProvider,
|
|
145
160
|
onProgress: (message) => appendMessage('system', `[Code] ${message}`)
|
|
146
161
|
});
|
|
147
162
|
clearInterval(timer);
|
package/src/CLI/chat_ui.js
CHANGED
|
@@ -14,6 +14,10 @@ const SLASH_COMMANDS = [
|
|
|
14
14
|
{ name: '/copy', desc: 'Copy last response to clipboard' },
|
|
15
15
|
{ name: '/clear', desc: 'Clear conversation history' },
|
|
16
16
|
{ name: '/reset', desc: 'Reset conversation history' },
|
|
17
|
+
{ name: '/agent', desc: 'Switch AI personas (coder, researcher, etc)' },
|
|
18
|
+
{ name: '/workspace', desc: 'Manage project-specific contexts' },
|
|
19
|
+
{ name: '/review', desc: 'Request a second-pass review of the last response' },
|
|
20
|
+
{ name: '/stats', desc: 'Show system health stats (CPU/RAM/Disk)' },
|
|
17
21
|
{ name: '/help', desc: 'Show help information' },
|
|
18
22
|
{ name: '/exit', desc: 'Exit Mint' }
|
|
19
23
|
];
|
|
@@ -588,6 +592,84 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
588
592
|
screen.render();
|
|
589
593
|
}
|
|
590
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Opens a streaming message bubble for the assistant.
|
|
597
|
+
* Returns { appendChunk(text), finalize(timestamp) } for typewriter rendering.
|
|
598
|
+
* Usage:
|
|
599
|
+
* const stream = streamMessage('assistant');
|
|
600
|
+
* stream.appendChunk('Hello'); stream.appendChunk(' World');
|
|
601
|
+
* stream.finalize(timestamp);
|
|
602
|
+
*/
|
|
603
|
+
function streamMessage(role = 'assistant') {
|
|
604
|
+
const now = new Date();
|
|
605
|
+
const timeStr = now.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
606
|
+
const maxLineWidth = Math.max(screen.width - 20, 36);
|
|
607
|
+
|
|
608
|
+
// Print the header bubble once
|
|
609
|
+
chatBox.log('');
|
|
610
|
+
if (role === 'assistant') {
|
|
611
|
+
chatBox.log(` {bold}{#d4a8ff-fg}Mint{/} {gray-fg}${timeStr}{/}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let buffer = ''; // accumulates the full response text
|
|
615
|
+
let lineBuffer = ''; // current partial line being built
|
|
616
|
+
let lineRendered = false; // whether we already pushed the first line prefix
|
|
617
|
+
|
|
618
|
+
function flushLine(force = false) {
|
|
619
|
+
// Flush content that fits on one line-width or when forced
|
|
620
|
+
if (!lineBuffer && !force) return;
|
|
621
|
+
if (!lineRendered) {
|
|
622
|
+
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
623
|
+
lineRendered = true;
|
|
624
|
+
} else {
|
|
625
|
+
// Overwrite the last line by popping + re-pushing (blessed.log limitation)
|
|
626
|
+
// We can't truly overwrite, so we just keep appending new lines for each chunk.
|
|
627
|
+
// For large chunks, split on newline and emit per-line.
|
|
628
|
+
chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${lineBuffer}{/}`);
|
|
629
|
+
}
|
|
630
|
+
screen.render();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function appendChunk(text) {
|
|
634
|
+
if (!text) return;
|
|
635
|
+
buffer += text;
|
|
636
|
+
const segments = text.split('\n');
|
|
637
|
+
for (let i = 0; i < segments.length; i++) {
|
|
638
|
+
lineBuffer += segments[i];
|
|
639
|
+
if (i < segments.length - 1) {
|
|
640
|
+
// Newline boundary — emit current line
|
|
641
|
+
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
642
|
+
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
643
|
+
lineBuffer = '';
|
|
644
|
+
lineRendered = true;
|
|
645
|
+
screen.render();
|
|
646
|
+
} else if (lineBuffer.length >= maxLineWidth) {
|
|
647
|
+
// Line overflow — auto-wrap
|
|
648
|
+
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
649
|
+
lines.slice(0, -1).forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
650
|
+
lineBuffer = lines[lines.length - 1] || '';
|
|
651
|
+
lineRendered = true;
|
|
652
|
+
screen.render();
|
|
653
|
+
}
|
|
654
|
+
// Otherwise keep buffering the partial line
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function finalize(timestamp = null) {
|
|
659
|
+
// Flush remaining buffer
|
|
660
|
+
if (lineBuffer) {
|
|
661
|
+
const lines = wrapLineSmart(lineBuffer, maxLineWidth);
|
|
662
|
+
lines.forEach(l => chatBox.log(` {#5a456d-fg}▏{/} {#ffffff-fg}${l}{/}`));
|
|
663
|
+
lineBuffer = '';
|
|
664
|
+
}
|
|
665
|
+
// Track last response for clipboard
|
|
666
|
+
lastAssistantResponse = buffer;
|
|
667
|
+
screen.render();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return { appendChunk, finalize };
|
|
671
|
+
}
|
|
672
|
+
|
|
591
673
|
/** Show/hide thinking indicator in status bar */
|
|
592
674
|
function setThinking(active, secondsElapsed = 0) {
|
|
593
675
|
if (active) {
|
|
@@ -628,7 +710,7 @@ function createChatUI({ onSubmit, onExit }) {
|
|
|
628
710
|
});
|
|
629
711
|
}
|
|
630
712
|
|
|
631
|
-
return { screen, appendMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode };
|
|
713
|
+
return { screen, appendMessage, streamMessage, setThinking, updateStatusModel, copyLastResponse, requestApproval, setMode };
|
|
632
714
|
}
|
|
633
715
|
|
|
634
716
|
module.exports = { createChatUI };
|
package/src/CLI/code_agent.js
CHANGED
|
@@ -3,7 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const { execFile } = require('child_process');
|
|
4
4
|
const { promisify } = require('util');
|
|
5
5
|
const { GoogleGenAI } = require('@google/genai');
|
|
6
|
-
const
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
7
8
|
const { readWorkspaceSession, writeWorkspaceSession } = require('./code_session_memory');
|
|
8
9
|
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
@@ -244,16 +245,101 @@ function writeFile(workspaceRoot, targetPath, content) {
|
|
|
244
245
|
return `Wrote ${targetPath}`;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
248
|
+
class UnifiedAgentClient {
|
|
249
|
+
constructor(provider, config) {
|
|
250
|
+
this.provider = provider;
|
|
251
|
+
this.config = config;
|
|
252
|
+
this.history = [];
|
|
253
|
+
this.systemInstruction = CODE_AGENT_PROMPT;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async sendMessage(observation) {
|
|
257
|
+
this.history.push({ role: 'user', content: observation });
|
|
258
|
+
|
|
259
|
+
let responseText = '';
|
|
260
|
+
if (this.provider === 'anthropic') {
|
|
261
|
+
responseText = await this._callAnthropic();
|
|
262
|
+
} else if (this.provider === 'openai' || this.provider === 'local_openai') {
|
|
263
|
+
responseText = await this._callOpenAI();
|
|
264
|
+
} else {
|
|
265
|
+
responseText = await this._callGemini();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.history.push({ role: 'assistant', content: responseText });
|
|
269
|
+
return responseText;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async _callAnthropic() {
|
|
273
|
+
const apiKey = this.config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
274
|
+
const messages = this.history.map(m => ({
|
|
275
|
+
role: m.role,
|
|
276
|
+
content: m.content
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
const response = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
280
|
+
model: this.config.anthropicModel || 'claude-3-5-sonnet-latest',
|
|
281
|
+
max_tokens: 8192,
|
|
282
|
+
system: this.systemInstruction,
|
|
283
|
+
messages: messages
|
|
284
|
+
}, {
|
|
285
|
+
headers: {
|
|
286
|
+
'x-api-key': apiKey,
|
|
287
|
+
'anthropic-version': '2023-06-01',
|
|
288
|
+
'content-type': 'application/json'
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return response.data.content[0].text;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async _callOpenAI() {
|
|
295
|
+
const isLocal = this.provider === 'local_openai';
|
|
296
|
+
const apiKey = isLocal ? 'not-needed' : (this.config.openaiApiKey || process.env.OPENAI_API_KEY);
|
|
297
|
+
const baseUrl = isLocal ? (this.config.localApiBaseUrl || 'http://localhost:1234/v1') : 'https://api.openai.com/v1';
|
|
298
|
+
const model = isLocal ? (this.config.localModelName || 'local-model') : (this.config.openaiModel || 'gpt-4o');
|
|
299
|
+
|
|
300
|
+
const messages = [
|
|
301
|
+
{ role: 'system', content: this.systemInstruction },
|
|
302
|
+
...this.history
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
|
306
|
+
model: model,
|
|
307
|
+
messages: messages,
|
|
308
|
+
response_format: isLocal ? undefined : { type: "json_object" }
|
|
309
|
+
}, {
|
|
310
|
+
headers: {
|
|
311
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
312
|
+
'Content-Type': 'application/json'
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return response.data.choices[0].message.content;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async _callGemini() {
|
|
319
|
+
const apiKey = this.config.apiKey || process.env.GEMINI_API_KEY;
|
|
320
|
+
const model = this.config.geminiModel || DEFAULT_GEMINI_MODEL;
|
|
321
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
322
|
+
|
|
323
|
+
// Convert history for Gemini
|
|
324
|
+
const geminiHistory = this.history.slice(0, -1).map(m => ({
|
|
325
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
326
|
+
parts: [{ text: m.content }]
|
|
327
|
+
}));
|
|
328
|
+
|
|
329
|
+
const lastMessage = this.history[this.history.length - 1].content;
|
|
330
|
+
|
|
331
|
+
const chat = ai.chats.create({
|
|
332
|
+
model,
|
|
333
|
+
config: {
|
|
334
|
+
systemInstruction: this.systemInstruction,
|
|
335
|
+
responseMimeType: 'application/json'
|
|
336
|
+
},
|
|
337
|
+
history: geminiHistory
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const response = await chat.sendMessage({ message: [{ text: lastMessage }] });
|
|
341
|
+
return typeof response.text === 'function' ? response.text() : response.text;
|
|
252
342
|
}
|
|
253
|
-
return {
|
|
254
|
-
ai: new GoogleGenAI({ apiKey }),
|
|
255
|
-
model: (config.geminiModel || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL
|
|
256
|
-
};
|
|
257
343
|
}
|
|
258
344
|
|
|
259
345
|
function detectPackageManager(workspaceRoot) {
|
|
@@ -327,23 +413,19 @@ async function executeCodeTask(task, options = {}) {
|
|
|
327
413
|
const requestApproval = typeof options.requestApproval === 'function'
|
|
328
414
|
? options.requestApproval
|
|
329
415
|
: async () => true;
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
model,
|
|
334
|
-
config: {
|
|
335
|
-
systemInstruction: CODE_AGENT_PROMPT,
|
|
336
|
-
responseMimeType: 'application/json'
|
|
337
|
-
},
|
|
338
|
-
history: []
|
|
339
|
-
});
|
|
416
|
+
const config = readConfig();
|
|
417
|
+
const provider = options.provider || 'gemini';
|
|
418
|
+
const client = new UnifiedAgentClient(provider, config);
|
|
340
419
|
|
|
341
420
|
let observation = await buildInitialObservation(task, workspaceRoot);
|
|
342
421
|
|
|
422
|
+
let finalSummary = '';
|
|
423
|
+
let finalVerification = '';
|
|
424
|
+
let finalSessionSummary = '';
|
|
425
|
+
|
|
343
426
|
for (let step = 1; step <= MAX_AGENT_STEPS; step++) {
|
|
344
427
|
onProgress(`Step ${step}: thinking`);
|
|
345
|
-
const
|
|
346
|
-
const text = typeof response.text === 'function' ? response.text() : response.text;
|
|
428
|
+
const text = await client.sendMessage(observation);
|
|
347
429
|
const decision = extractJson(text);
|
|
348
430
|
const action = decision.action;
|
|
349
431
|
const input = decision.input || {};
|
|
@@ -351,17 +433,15 @@ async function executeCodeTask(task, options = {}) {
|
|
|
351
433
|
onProgress(`Step ${step}: ${action}${input.path ? ` ${input.path}` : input.command ? ` ${input.command}` : ''}`);
|
|
352
434
|
|
|
353
435
|
if (action === 'finish') {
|
|
354
|
-
|
|
436
|
+
finalSessionSummary = input.sessionSummary || input.summary || task;
|
|
437
|
+
finalSummary = input.summary || 'Task complete.';
|
|
438
|
+
finalVerification = input.verification || 'Not specified.';
|
|
355
439
|
writeWorkspaceSession(workspaceRoot, {
|
|
356
|
-
summary:
|
|
440
|
+
summary: finalSessionSummary,
|
|
357
441
|
lastTask: task,
|
|
358
|
-
lastVerification:
|
|
442
|
+
lastVerification: finalVerification
|
|
359
443
|
});
|
|
360
|
-
|
|
361
|
-
summary: input.summary || 'Task complete.',
|
|
362
|
-
verification: input.verification || 'Not specified.',
|
|
363
|
-
steps: step
|
|
364
|
-
};
|
|
444
|
+
break;
|
|
365
445
|
}
|
|
366
446
|
|
|
367
447
|
let toolResult = '';
|
|
@@ -427,6 +507,39 @@ async function executeCodeTask(task, options = {}) {
|
|
|
427
507
|
].join('\n');
|
|
428
508
|
}
|
|
429
509
|
|
|
510
|
+
// Check for Agent Collaboration (Review)
|
|
511
|
+
if (config.enableAgentCollaboration !== false) {
|
|
512
|
+
const availableProviders = getAvailableProviders(config);
|
|
513
|
+
const altProviders = availableProviders.filter(p => p !== provider && p !== 'ollama' && p !== 'huggingface');
|
|
514
|
+
if (altProviders.length > 0 && finalSummary) {
|
|
515
|
+
const reviewerProvider = altProviders[0];
|
|
516
|
+
onProgress(`Invoking Reviewer Agent (${reviewerProvider})...`);
|
|
517
|
+
|
|
518
|
+
const reviewerClient = new UnifiedAgentClient(reviewerProvider, config);
|
|
519
|
+
reviewerClient.systemInstruction = CODE_AGENT_PROMPT + "\n\nYou are the Reviewer Agent. Review the primary agent's changes, test output, and verification. If you spot a critical bug, point it out. Otherwise, confirm it looks good. Return JSON with action: 'finish' and your review in the 'summary' field.";
|
|
520
|
+
|
|
521
|
+
const reviewPrompt = `The primary agent (${provider}) just completed the task: "${task}".\nSummary: ${finalSummary}\nVerification: ${finalVerification}\nGit Status: ${(await getGitContext(workspaceRoot)).status}\n\nPlease review this. Return JSON with action: 'finish'.`;
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const reviewResponse = await reviewerClient.sendMessage(reviewPrompt);
|
|
525
|
+
const reviewDecision = extractJson(reviewResponse);
|
|
526
|
+
const reviewInput = reviewDecision.input || {};
|
|
527
|
+
|
|
528
|
+
finalSummary += `\n\n[Review by ${reviewerProvider}]\n${reviewInput.summary || reviewDecision.thought || 'Looks good.'}`;
|
|
529
|
+
} catch (e) {
|
|
530
|
+
onProgress(`Reviewer Agent failed: ${e.message}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (finalSummary) {
|
|
536
|
+
return {
|
|
537
|
+
summary: finalSummary,
|
|
538
|
+
verification: finalVerification,
|
|
539
|
+
steps: MAX_AGENT_STEPS
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
430
543
|
writeWorkspaceSession(workspaceRoot, {
|
|
431
544
|
summary: `Task stopped before completion: ${task}`,
|
|
432
545
|
lastTask: task,
|