donna-komilion-bot 0.1.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/bin/donna.js +2 -0
- package/dist/agent.js +100 -0
- package/dist/hermione.js +113 -0
- package/dist/index.js +146 -0
- package/dist/memory.js +182 -0
- package/dist/tmux.js +127 -0
- package/dist/types.js +4 -0
- package/package.json +48 -0
package/bin/donna.js
ADDED
package/dist/agent.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Donna Agent — Komilion API Client
|
|
3
|
+
*
|
|
4
|
+
* Routes via komilion.com/api/v1 — no local scoring engine.
|
|
5
|
+
* Model selection is handled server-side by the Komilion Oracle.
|
|
6
|
+
* Policy maps directly to neo:frugal / neo:balanced / neo:premium.
|
|
7
|
+
*/
|
|
8
|
+
import { appendTurn, getTurns, createSession } from './memory.js';
|
|
9
|
+
const KOMILION_BASE = 'https://www.komilion.com/api/v1';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// KOMILION API CALL
|
|
12
|
+
// ============================================================================
|
|
13
|
+
async function callKomilion(policy, messages, maxTokens = 4000) {
|
|
14
|
+
const apiKey = process.env.KOMILION_API_KEY;
|
|
15
|
+
if (!apiKey)
|
|
16
|
+
throw new Error('KOMILION_API_KEY not set');
|
|
17
|
+
const startMs = Date.now();
|
|
18
|
+
const res = await fetch(`${KOMILION_BASE}/chat/completions`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
model: `komilion-${policy}`,
|
|
26
|
+
messages,
|
|
27
|
+
max_tokens: maxTokens,
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
const latencyMs = Date.now() - startMs;
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const txt = await res.text();
|
|
33
|
+
throw new Error(`Komilion API ${res.status}: ${txt.slice(0, 200)}`);
|
|
34
|
+
}
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
const content = data.choices?.[0]?.message?.content ?? '';
|
|
37
|
+
const usage = data.usage ?? {};
|
|
38
|
+
return {
|
|
39
|
+
content,
|
|
40
|
+
inputTokens: usage.prompt_tokens ?? 0,
|
|
41
|
+
outputTokens: usage.completion_tokens ?? 0,
|
|
42
|
+
latencyMs,
|
|
43
|
+
modelId: data.model ?? `neo:${policy}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function getWallet() {
|
|
47
|
+
const apiKey = process.env.KOMILION_API_KEY;
|
|
48
|
+
if (!apiKey)
|
|
49
|
+
throw new Error('KOMILION_API_KEY not set');
|
|
50
|
+
const res = await fetch('https://www.komilion.com/api/wallet/balance', {
|
|
51
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const txt = await res.text();
|
|
55
|
+
throw new Error(`Komilion wallet ${res.status}: ${txt.slice(0, 200)}`);
|
|
56
|
+
}
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
return {
|
|
59
|
+
balance: data.total_available ?? 0,
|
|
60
|
+
walletBalance: data.wallet_balance ?? 0,
|
|
61
|
+
trialCredits: data.trial_credits ?? 0,
|
|
62
|
+
currency: data.currency ?? 'USD',
|
|
63
|
+
isLowBalance: data.is_low_balance ?? false,
|
|
64
|
+
message: data.message ?? '',
|
|
65
|
+
topUpUrl: 'https://www.komilion.com/billing',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// DONNA AGENT TURN
|
|
70
|
+
// ============================================================================
|
|
71
|
+
export async function runTurn(sessionId, userMessage, policy = 'balanced') {
|
|
72
|
+
// Ensure session exists
|
|
73
|
+
createSession(sessionId, policy);
|
|
74
|
+
// Build messages from memory
|
|
75
|
+
const history = getTurns(sessionId, 20);
|
|
76
|
+
const messages = [
|
|
77
|
+
{ role: 'system', content: 'You are Donna, a highly capable AI assistant. You help with coding, analysis, research, and creative tasks.' },
|
|
78
|
+
...history.map(t => ({ role: t.role, content: t.content })),
|
|
79
|
+
{ role: 'user', content: userMessage },
|
|
80
|
+
];
|
|
81
|
+
// Route via Komilion Oracle
|
|
82
|
+
const result = await callKomilion(policy, messages, 4000);
|
|
83
|
+
// Store in memory
|
|
84
|
+
appendTurn(sessionId, 'user', userMessage);
|
|
85
|
+
appendTurn(sessionId, 'assistant', result.content, {
|
|
86
|
+
model: result.modelId,
|
|
87
|
+
costUsd: 0, // komilion.com tracks spend server-side
|
|
88
|
+
tokensIn: result.inputTokens,
|
|
89
|
+
tokensOut: result.outputTokens,
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
content: result.content,
|
|
93
|
+
modelId: result.modelId,
|
|
94
|
+
latencyMs: result.latencyMs,
|
|
95
|
+
inputTokens: result.inputTokens,
|
|
96
|
+
outputTokens: result.outputTokens,
|
|
97
|
+
costUsd: 0,
|
|
98
|
+
policy,
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/hermione.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermione — Benchmark Judge
|
|
3
|
+
*
|
|
4
|
+
* Ported from komilion/lib/hermione.ts
|
|
5
|
+
* Evaluates two AI responses side-by-side.
|
|
6
|
+
*/
|
|
7
|
+
const OPENROUTER_BASE = 'https://openrouter.ai/api/v1';
|
|
8
|
+
export const HERMIONE_MODEL = 'anthropic/claude-sonnet-4-6';
|
|
9
|
+
const SYSTEM_PROMPT = `You are Hermione, an expert AI benchmark judge.
|
|
10
|
+
Evaluate two AI responses to the same prompt and score them fairly.
|
|
11
|
+
|
|
12
|
+
Respond ONLY with JSON — no preamble, no markdown outside JSON.
|
|
13
|
+
|
|
14
|
+
Required format:
|
|
15
|
+
{
|
|
16
|
+
"a_score": <integer 1-10>,
|
|
17
|
+
"b_score": <integer 1-10>,
|
|
18
|
+
"winner": "a" | "b" | "tie",
|
|
19
|
+
"a_verdict": "<1-2 sentences>",
|
|
20
|
+
"b_verdict": "<1-2 sentences>",
|
|
21
|
+
"summary": "<1 sentence takeaway>"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Scoring rubric:
|
|
25
|
+
- 9-10: Exceptional — production-ready, handles edge cases
|
|
26
|
+
- 7-8: Good — correct and useful, minor improvements possible
|
|
27
|
+
- 5-6: Acceptable — works but misses nuance
|
|
28
|
+
- 3-4: Weak — partially correct or verbose without value
|
|
29
|
+
- 1-2: Poor — incorrect or unhelpful
|
|
30
|
+
|
|
31
|
+
Focus on correctness first, then completeness, then clarity.`;
|
|
32
|
+
export async function judgeWithHermione(prompt, a, b) {
|
|
33
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
34
|
+
if (!apiKey)
|
|
35
|
+
throw new Error('OPENROUTER_API_KEY not set');
|
|
36
|
+
const judgePrompt = `Evaluate these two responses to the same prompt.
|
|
37
|
+
|
|
38
|
+
USER PROMPT:
|
|
39
|
+
${prompt}
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
RESPONSE A (${a.modelId}):
|
|
44
|
+
${a.response}
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
RESPONSE B (${b.modelId}):
|
|
49
|
+
${b.response}
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
Performance context (for summary only — do NOT affect quality scores):
|
|
54
|
+
Latency: A=${a.latencyMs}ms, B=${b.latencyMs}ms
|
|
55
|
+
Cost: A=$${a.costUsd.toFixed(5)}, B=$${b.costUsd.toFixed(5)}
|
|
56
|
+
|
|
57
|
+
Score quality only (1-10 each). Declare winner based on quality alone.`;
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
'HTTP-Referer': 'https://www.komilion.com',
|
|
65
|
+
'X-Title': 'Donna Hermione Judge',
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
model: HERMIONE_MODEL,
|
|
69
|
+
messages: [
|
|
70
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
71
|
+
{ role: 'user', content: judgePrompt },
|
|
72
|
+
],
|
|
73
|
+
temperature: 0.1,
|
|
74
|
+
max_tokens: 500,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`Hermione API error: ${res.status}`);
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
const raw = data.choices?.[0]?.message?.content ?? '';
|
|
81
|
+
const jsonStr = raw.replace(/^```json\s*/i, '').replace(/\s*```$/, '').trim();
|
|
82
|
+
const parsed = JSON.parse(jsonStr);
|
|
83
|
+
const aScore = Math.min(10, Math.max(1, Math.round(Number(parsed.a_score) || 5)));
|
|
84
|
+
const bScore = Math.min(10, Math.max(1, Math.round(Number(parsed.b_score) || 5)));
|
|
85
|
+
let winner = 'tie';
|
|
86
|
+
const raw_winner = String(parsed.winner || '').toLowerCase();
|
|
87
|
+
if (raw_winner === 'a')
|
|
88
|
+
winner = 'a';
|
|
89
|
+
else if (raw_winner === 'b')
|
|
90
|
+
winner = 'b';
|
|
91
|
+
else if (aScore > bScore)
|
|
92
|
+
winner = 'a';
|
|
93
|
+
else if (bScore > aScore)
|
|
94
|
+
winner = 'b';
|
|
95
|
+
return {
|
|
96
|
+
aScore, bScore, winner,
|
|
97
|
+
aVerdict: String(parsed.a_verdict || 'No verdict.'),
|
|
98
|
+
bVerdict: String(parsed.b_verdict || 'No verdict.'),
|
|
99
|
+
summary: String(parsed.summary || ''),
|
|
100
|
+
judgeModel: HERMIONE_MODEL,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error('[Hermione] Judge failed, returning fallback:', err);
|
|
105
|
+
return {
|
|
106
|
+
aScore: 5, bScore: 5, winner: 'tie',
|
|
107
|
+
aVerdict: 'Hermione judgment unavailable.',
|
|
108
|
+
bVerdict: 'Hermione judgment unavailable.',
|
|
109
|
+
summary: 'Judgment failed. Compare responses manually.',
|
|
110
|
+
judgeModel: HERMIONE_MODEL,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Donna Komilion Bot — Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Interactive REPL: send prompt → komilion.com Oracle routes → SQLite memory
|
|
5
|
+
* Set KOMILION_API_KEY in .env to authenticate.
|
|
6
|
+
*/
|
|
7
|
+
import 'dotenv/config';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import { runTurn, getWallet } from './agent.js';
|
|
11
|
+
import { getTurns, searchMemory, listSessions, getSessionStats } from './memory.js';
|
|
12
|
+
import { spawnAll, killAll } from './tmux.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// CLI REPL
|
|
15
|
+
// ============================================================================
|
|
16
|
+
async function main() {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
// Commands
|
|
19
|
+
if (args[0] === 'spawn') {
|
|
20
|
+
spawnAll();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (args[0] === 'kill') {
|
|
24
|
+
killAll();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (args[0] === 'sessions') {
|
|
28
|
+
console.log(listSessions());
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (args[0] === 'search' && args[1]) {
|
|
32
|
+
const hits = searchMemory(args.slice(1).join(' '));
|
|
33
|
+
hits.forEach(h => console.log(`[${h.sessionId}/${h.role}] ${h.snippet}`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (args[0] === 'credits') {
|
|
37
|
+
const w = await getWallet();
|
|
38
|
+
console.log(` Balance: $${w.balance.toFixed(4)} ${w.currency}`);
|
|
39
|
+
console.log(` Wallet: $${w.walletBalance.toFixed(4)}`);
|
|
40
|
+
console.log(` Trial: $${w.trialCredits.toFixed(4)}`);
|
|
41
|
+
if (w.isLowBalance)
|
|
42
|
+
console.log(` ⚠️ Low balance — top up at: ${w.topUpUrl}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Interactive REPL
|
|
46
|
+
const sessionId = args[0] ?? randomUUID().slice(0, 8);
|
|
47
|
+
const policy = args[1] ?? 'balanced';
|
|
48
|
+
console.log(`\n🤖 Donna Komilion Bot`);
|
|
49
|
+
console.log(` Session: ${sessionId} | Policy: ${policy}`);
|
|
50
|
+
console.log(` Type /help for commands, Ctrl+C to exit\n`);
|
|
51
|
+
const stats = getSessionStats(sessionId);
|
|
52
|
+
if (stats) {
|
|
53
|
+
console.log(` Resuming: ${stats.turnCount} turns, last active ${stats.lastActive}\n`);
|
|
54
|
+
}
|
|
55
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
rl.on('close', () => { console.log('\nBye.'); process.exit(0); });
|
|
57
|
+
const prompt = () => {
|
|
58
|
+
rl.question('you> ', async (input) => {
|
|
59
|
+
const line = input.trim();
|
|
60
|
+
if (!line) {
|
|
61
|
+
prompt();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Slash commands
|
|
65
|
+
if (line === '/help') {
|
|
66
|
+
console.log(' /history [n] — show last n turns');
|
|
67
|
+
console.log(' /search <q> — FTS5 search memory');
|
|
68
|
+
console.log(' /stats — session stats');
|
|
69
|
+
console.log(' /sessions — list all sessions');
|
|
70
|
+
console.log(' /credits — show komilion.com wallet balance');
|
|
71
|
+
console.log(' /top-up — show billing URL to add credits');
|
|
72
|
+
console.log(' /clear — clear this session history');
|
|
73
|
+
console.log(' /exit — quit');
|
|
74
|
+
prompt();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (line === '/exit') {
|
|
78
|
+
rl.close();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (line.startsWith('/history')) {
|
|
82
|
+
const n = parseInt(line.split(' ')[1] ?? '10');
|
|
83
|
+
const turns = getTurns(sessionId, n);
|
|
84
|
+
turns.forEach(t => console.log(` [${t.role}] ${t.content.slice(0, 120)}`));
|
|
85
|
+
prompt();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (line.startsWith('/search ')) {
|
|
89
|
+
const q = line.slice(8);
|
|
90
|
+
const hits = searchMemory(q);
|
|
91
|
+
if (!hits.length)
|
|
92
|
+
console.log(' No results.');
|
|
93
|
+
hits.forEach(h => console.log(` [${h.createdAt}/${h.role}] ${h.snippet}`));
|
|
94
|
+
prompt();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (line === '/stats') {
|
|
98
|
+
const s = getSessionStats(sessionId);
|
|
99
|
+
if (s)
|
|
100
|
+
console.log(` turns=${s.turnCount} lastActive=${s.lastActive}`);
|
|
101
|
+
else
|
|
102
|
+
console.log(' No stats yet.');
|
|
103
|
+
prompt();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (line === '/sessions') {
|
|
107
|
+
listSessions().forEach(s => console.log(` ${s.id} | ${s.policy} | ${s.turnCount} turns`));
|
|
108
|
+
prompt();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (line === '/credits') {
|
|
112
|
+
try {
|
|
113
|
+
const w = await getWallet();
|
|
114
|
+
console.log(` Balance: $${w.balance.toFixed(4)} ${w.currency}`);
|
|
115
|
+
console.log(` Wallet: $${w.walletBalance.toFixed(4)}`);
|
|
116
|
+
console.log(` Trial: $${w.trialCredits.toFixed(4)}`);
|
|
117
|
+
if (w.isLowBalance)
|
|
118
|
+
console.log(` ⚠️ Low balance — top up: ${w.topUpUrl}`);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error(` ❌ ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
prompt();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (line === '/top-up') {
|
|
127
|
+
console.log(' Add credits at: https://www.komilion.com/billing');
|
|
128
|
+
prompt();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Agent turn
|
|
132
|
+
try {
|
|
133
|
+
process.stdout.write('donna> ');
|
|
134
|
+
const result = await runTurn(sessionId, line, policy);
|
|
135
|
+
console.log(result.content);
|
|
136
|
+
console.log(`\n ↳ ${result.modelId} | komilion-${result.policy} | ${result.latencyMs}ms\n`);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.error(` ❌ Error: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
prompt();
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
prompt();
|
|
145
|
+
}
|
|
146
|
+
main().catch(console.error);
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite + FTS5 Local Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Stores conversation turns, searchable via full-text index.
|
|
5
|
+
* Uses better-sqlite3 (synchronous, perfect for CLI bot).
|
|
6
|
+
*/
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { mkdirSync } from 'fs';
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const DB_DIR = process.env.DONNA_DB_DIR ?? join(__dirname, '..', 'data');
|
|
13
|
+
const DB_PATH = join(DB_DIR, 'donna.db');
|
|
14
|
+
let _db = null;
|
|
15
|
+
function getDb() {
|
|
16
|
+
if (_db)
|
|
17
|
+
return _db;
|
|
18
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
19
|
+
_db = new Database(DB_PATH);
|
|
20
|
+
_db.pragma('journal_mode = WAL');
|
|
21
|
+
_db.pragma('foreign_keys = ON');
|
|
22
|
+
initSchema(_db);
|
|
23
|
+
return _db;
|
|
24
|
+
}
|
|
25
|
+
function initSchema(db) {
|
|
26
|
+
// Turns table
|
|
27
|
+
db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS turns (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
session_id TEXT NOT NULL,
|
|
31
|
+
role TEXT NOT NULL CHECK(role IN ('user','assistant','system')),
|
|
32
|
+
content TEXT NOT NULL,
|
|
33
|
+
model TEXT,
|
|
34
|
+
cost_usd REAL,
|
|
35
|
+
tokens_in INTEGER,
|
|
36
|
+
tokens_out INTEGER,
|
|
37
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
38
|
+
);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id, id);
|
|
40
|
+
`);
|
|
41
|
+
// FTS5 virtual table for full-text search
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS turns_fts USING fts5(
|
|
44
|
+
content,
|
|
45
|
+
session_id UNINDEXED,
|
|
46
|
+
role UNINDEXED,
|
|
47
|
+
turn_id UNINDEXED,
|
|
48
|
+
tokenize = 'porter ascii'
|
|
49
|
+
);
|
|
50
|
+
`);
|
|
51
|
+
// Keep FTS in sync via triggers
|
|
52
|
+
db.exec(`
|
|
53
|
+
CREATE TRIGGER IF NOT EXISTS turns_ai AFTER INSERT ON turns BEGIN
|
|
54
|
+
INSERT INTO turns_fts(content, session_id, role, turn_id)
|
|
55
|
+
VALUES (new.content, new.session_id, new.role, new.id);
|
|
56
|
+
END;
|
|
57
|
+
CREATE TRIGGER IF NOT EXISTS turns_ad AFTER DELETE ON turns BEGIN
|
|
58
|
+
DELETE FROM turns_fts WHERE turn_id = old.id;
|
|
59
|
+
END;
|
|
60
|
+
`);
|
|
61
|
+
// Sessions table (metadata)
|
|
62
|
+
db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
policy TEXT NOT NULL DEFAULT 'balanced',
|
|
66
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
67
|
+
last_active TEXT NOT NULL DEFAULT (datetime('now')),
|
|
68
|
+
total_cost REAL NOT NULL DEFAULT 0
|
|
69
|
+
);
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// API
|
|
74
|
+
// ============================================================================
|
|
75
|
+
export function createSession(id, policy = 'balanced') {
|
|
76
|
+
const db = getDb();
|
|
77
|
+
db.prepare(`
|
|
78
|
+
INSERT OR IGNORE INTO sessions (id, policy) VALUES (?, ?)
|
|
79
|
+
`).run(id, policy);
|
|
80
|
+
}
|
|
81
|
+
export function appendTurn(sessionId, role, content, meta) {
|
|
82
|
+
const db = getDb();
|
|
83
|
+
// Ensure session exists
|
|
84
|
+
createSession(sessionId);
|
|
85
|
+
const result = db.prepare(`
|
|
86
|
+
INSERT INTO turns (session_id, role, content, model, cost_usd, tokens_in, tokens_out)
|
|
87
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
88
|
+
`).run(sessionId, role, content, meta?.model ?? null, meta?.costUsd ?? null, meta?.tokensIn ?? null, meta?.tokensOut ?? null);
|
|
89
|
+
// Update session last_active + total_cost
|
|
90
|
+
if (meta?.costUsd) {
|
|
91
|
+
db.prepare(`
|
|
92
|
+
UPDATE sessions SET last_active = datetime('now'), total_cost = total_cost + ?
|
|
93
|
+
WHERE id = ?
|
|
94
|
+
`).run(meta.costUsd, sessionId);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
db.prepare(`UPDATE sessions SET last_active = datetime('now') WHERE id = ?`).run(sessionId);
|
|
98
|
+
}
|
|
99
|
+
return result.lastInsertRowid;
|
|
100
|
+
}
|
|
101
|
+
export function getTurns(sessionId, limit = 50) {
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const rows = db.prepare(`
|
|
104
|
+
SELECT id, session_id, role, content, model, cost_usd, tokens_in, tokens_out, created_at
|
|
105
|
+
FROM turns WHERE session_id = ? ORDER BY id DESC LIMIT ?
|
|
106
|
+
`).all(sessionId, limit);
|
|
107
|
+
return rows.reverse().map(r => ({
|
|
108
|
+
id: r.id,
|
|
109
|
+
sessionId: r.session_id,
|
|
110
|
+
role: r.role,
|
|
111
|
+
content: r.content,
|
|
112
|
+
model: r.model ?? undefined,
|
|
113
|
+
costUsd: r.cost_usd ?? undefined,
|
|
114
|
+
tokensIn: r.tokens_in ?? undefined,
|
|
115
|
+
tokensOut: r.tokens_out ?? undefined,
|
|
116
|
+
createdAt: r.created_at,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
export function searchMemory(query, limit = 10) {
|
|
120
|
+
const db = getDb();
|
|
121
|
+
const rows = db.prepare(`
|
|
122
|
+
SELECT t.id, t.session_id, t.role, t.content, t.model, t.cost_usd, t.tokens_in, t.tokens_out, t.created_at,
|
|
123
|
+
snippet(turns_fts, 0, '**', '**', '…', 20) AS snippet
|
|
124
|
+
FROM turns_fts
|
|
125
|
+
JOIN turns t ON t.id = turns_fts.turn_id
|
|
126
|
+
WHERE turns_fts MATCH ?
|
|
127
|
+
ORDER BY turns_fts.rank
|
|
128
|
+
LIMIT ?
|
|
129
|
+
`).all(query, limit);
|
|
130
|
+
return rows.map(r => ({
|
|
131
|
+
id: r.id,
|
|
132
|
+
sessionId: r.session_id,
|
|
133
|
+
role: r.role,
|
|
134
|
+
content: r.content,
|
|
135
|
+
model: r.model ?? undefined,
|
|
136
|
+
costUsd: r.cost_usd ?? undefined,
|
|
137
|
+
tokensIn: r.tokens_in ?? undefined,
|
|
138
|
+
tokensOut: r.tokens_out ?? undefined,
|
|
139
|
+
createdAt: r.created_at,
|
|
140
|
+
snippet: r.snippet,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
export function getSessionStats(sessionId) {
|
|
144
|
+
const db = getDb();
|
|
145
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(sessionId);
|
|
146
|
+
if (!session)
|
|
147
|
+
return null;
|
|
148
|
+
const count = db.prepare(`SELECT COUNT(*) as n FROM turns WHERE session_id = ?`).get(sessionId);
|
|
149
|
+
return {
|
|
150
|
+
turnCount: count.n,
|
|
151
|
+
totalCost: session.total_cost,
|
|
152
|
+
lastActive: session.last_active,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function listSessions() {
|
|
156
|
+
const db = getDb();
|
|
157
|
+
const rows = db.prepare(`
|
|
158
|
+
SELECT s.id, s.policy, s.total_cost, s.last_active,
|
|
159
|
+
COUNT(t.id) as turn_count
|
|
160
|
+
FROM sessions s
|
|
161
|
+
LEFT JOIN turns t ON t.session_id = s.id
|
|
162
|
+
GROUP BY s.id
|
|
163
|
+
ORDER BY s.last_active DESC
|
|
164
|
+
`).all();
|
|
165
|
+
return rows.map(r => ({
|
|
166
|
+
id: r.id,
|
|
167
|
+
policy: r.policy,
|
|
168
|
+
turnCount: r.turn_count,
|
|
169
|
+
totalCost: r.total_cost,
|
|
170
|
+
lastActive: r.last_active,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
export function clearSession(sessionId) {
|
|
174
|
+
const db = getDb();
|
|
175
|
+
db.prepare(`DELETE FROM turns WHERE session_id = ?`).run(sessionId);
|
|
176
|
+
}
|
|
177
|
+
export function closeDb() {
|
|
178
|
+
if (_db) {
|
|
179
|
+
_db.close();
|
|
180
|
+
_db = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/tmux.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tmux Agent Spawning
|
|
3
|
+
*
|
|
4
|
+
* Spawns and manages Donna (Sonnet 4.6 orchestrator) and
|
|
5
|
+
* Hermione (judge) as separate tmux sessions.
|
|
6
|
+
*
|
|
7
|
+
* MCP server: PAUSED pending Harvey's explanation.
|
|
8
|
+
*/
|
|
9
|
+
import { execSync, exec } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
export const DONNA_SESSION = 'donna';
|
|
13
|
+
export const HERMIONE_SESSION = 'hermione';
|
|
14
|
+
function tmux(cmd) {
|
|
15
|
+
return execSync(`tmux ${cmd}`, { encoding: 'utf8' }).trim();
|
|
16
|
+
}
|
|
17
|
+
async function tmuxAsync(cmd) {
|
|
18
|
+
const { stdout } = await execAsync(`tmux ${cmd}`);
|
|
19
|
+
return stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
function sessionExists(name) {
|
|
22
|
+
try {
|
|
23
|
+
execSync(`tmux has-session -t ${name} 2>/dev/null`, { stdio: 'pipe' });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// SESSION MANAGEMENT
|
|
32
|
+
// ============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* Create a new tmux session (detached) if it doesn't exist.
|
|
35
|
+
* Returns true if created, false if already exists.
|
|
36
|
+
*/
|
|
37
|
+
export function ensureSession(name, startCmd) {
|
|
38
|
+
if (sessionExists(name)) {
|
|
39
|
+
console.log(`[tmux] Session '${name}' already exists`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const cmd = startCmd ? `-d -s ${name} ${startCmd}` : `-d -s ${name}`;
|
|
43
|
+
tmux(`new-session ${cmd}`);
|
|
44
|
+
console.log(`[tmux] Created session '${name}'`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Kill a tmux session.
|
|
49
|
+
*/
|
|
50
|
+
export function killSession(name) {
|
|
51
|
+
if (!sessionExists(name))
|
|
52
|
+
return;
|
|
53
|
+
tmux(`kill-session -t ${name}`);
|
|
54
|
+
console.log(`[tmux] Killed session '${name}'`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Send keys to a tmux session (e.g. a command to run).
|
|
58
|
+
*/
|
|
59
|
+
export function sendKeys(session, keys, enter = true) {
|
|
60
|
+
const escaped = keys.replace(/'/g, "'\\''");
|
|
61
|
+
tmux(`send-keys -t ${session} '${escaped}' ${enter ? 'Enter' : ''}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Capture the visible output of a tmux pane.
|
|
65
|
+
*/
|
|
66
|
+
export function capturePane(session, lines = 50) {
|
|
67
|
+
try {
|
|
68
|
+
return tmux(`capture-pane -t ${session} -p -S -${lines}`);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* List all active tmux sessions.
|
|
76
|
+
*/
|
|
77
|
+
export function listSessions() {
|
|
78
|
+
try {
|
|
79
|
+
const out = tmux(`list-sessions -F '#{session_name}'`);
|
|
80
|
+
return out.split('\n').filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// DONNA + HERMIONE LAUNCH
|
|
88
|
+
// ============================================================================
|
|
89
|
+
/**
|
|
90
|
+
* Spawn the Donna orchestrator session.
|
|
91
|
+
* Runs the donna CLI in an interactive tmux window.
|
|
92
|
+
*/
|
|
93
|
+
export function spawnDonna(donnaCmd = 'node dist/index.js') {
|
|
94
|
+
if (sessionExists(DONNA_SESSION)) {
|
|
95
|
+
console.log(`[tmux] Donna session already running. Attaching: tmux attach -t ${DONNA_SESSION}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
ensureSession(DONNA_SESSION, `-- bash -c '${donnaCmd}; exec bash'`);
|
|
99
|
+
console.log(`✅ [tmux] Donna spawned. Attach with: tmux attach -t ${DONNA_SESSION}`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Spawn the Hermione judge session.
|
|
103
|
+
* Separate window for judge runs — keeps Donna's output clean.
|
|
104
|
+
*/
|
|
105
|
+
export function spawnHermione(hermioneCmd = 'node dist/hermione-cli.js') {
|
|
106
|
+
if (sessionExists(HERMIONE_SESSION)) {
|
|
107
|
+
console.log(`[tmux] Hermione session already running.`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
ensureSession(HERMIONE_SESSION, `-- bash -c '${hermioneCmd}; exec bash'`);
|
|
111
|
+
console.log(`✅ [tmux] Hermione spawned. Attach with: tmux attach -t ${HERMIONE_SESSION}`);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Spawn both Donna and Hermione.
|
|
115
|
+
*/
|
|
116
|
+
export function spawnAll() {
|
|
117
|
+
spawnDonna();
|
|
118
|
+
spawnHermione();
|
|
119
|
+
console.log('\n[tmux] Active sessions:', listSessions().join(', '));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Kill all bot sessions.
|
|
123
|
+
*/
|
|
124
|
+
export function killAll() {
|
|
125
|
+
killSession(DONNA_SESSION);
|
|
126
|
+
killSession(HERMIONE_SESSION);
|
|
127
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "donna-komilion-bot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Donna orchestrator bot — routes via komilion.com, SQLite memory, tmux agent spawning",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"donna": "bin/donna.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"komilion",
|
|
16
|
+
"openrouter",
|
|
17
|
+
"ai",
|
|
18
|
+
"routing",
|
|
19
|
+
"claude",
|
|
20
|
+
"llm",
|
|
21
|
+
"donna"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://www.komilion.com",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/hshahrokni2/donna-komilion-bot"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"dev": "tsx src/index.ts",
|
|
35
|
+
"start": "node dist/index.js",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"better-sqlite3": "^9.4.3",
|
|
40
|
+
"dotenv": "^16.4.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/better-sqlite3": "^7.6.8",
|
|
44
|
+
"@types/node": "^20.14.0",
|
|
45
|
+
"tsx": "^4.15.6",
|
|
46
|
+
"typescript": "^5.5.2"
|
|
47
|
+
}
|
|
48
|
+
}
|