agentgui 1.0.4 → 1.0.6
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/database.js +103 -32
- package/package.json +2 -1
- package/server.js +22 -2
- package/static/app.js +77 -11
- package/static/index.html +7 -0
package/database.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { createRequire } from 'module';
|
|
4
5
|
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
5
7
|
const dbDir = path.join(os.homedir(), '.gmgui');
|
|
6
8
|
const dbFilePath = path.join(dbDir, 'data.db');
|
|
7
9
|
const oldJsonPath = path.join(dbDir, 'data.json');
|
|
@@ -28,7 +30,7 @@ try {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
function initSchema() {
|
|
31
|
-
db.
|
|
33
|
+
db.exec(`
|
|
32
34
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
33
35
|
id TEXT PRIMARY KEY,
|
|
34
36
|
agentId TEXT NOT NULL,
|
|
@@ -36,13 +38,11 @@ function initSchema() {
|
|
|
36
38
|
created_at INTEGER NOT NULL,
|
|
37
39
|
updated_at INTEGER NOT NULL,
|
|
38
40
|
status TEXT DEFAULT 'active'
|
|
39
|
-
)
|
|
40
|
-
`);
|
|
41
|
+
);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_agent ON conversations(agentId);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at DESC);
|
|
44
45
|
|
|
45
|
-
db.run(`
|
|
46
46
|
CREATE TABLE IF NOT EXISTS messages (
|
|
47
47
|
id TEXT PRIMARY KEY,
|
|
48
48
|
conversationId TEXT NOT NULL,
|
|
@@ -50,12 +50,10 @@ function initSchema() {
|
|
|
50
50
|
content TEXT NOT NULL,
|
|
51
51
|
created_at INTEGER NOT NULL,
|
|
52
52
|
FOREIGN KEY (conversationId) REFERENCES conversations(id)
|
|
53
|
-
)
|
|
54
|
-
`);
|
|
53
|
+
);
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversationId);
|
|
57
56
|
|
|
58
|
-
db.run(`
|
|
59
57
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
60
58
|
id TEXT PRIMARY KEY,
|
|
61
59
|
conversationId TEXT NOT NULL,
|
|
@@ -65,13 +63,11 @@ function initSchema() {
|
|
|
65
63
|
response TEXT,
|
|
66
64
|
error TEXT,
|
|
67
65
|
FOREIGN KEY (conversationId) REFERENCES conversations(id)
|
|
68
|
-
)
|
|
69
|
-
`);
|
|
66
|
+
);
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_conversation ON sessions(conversationId);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(conversationId, status);
|
|
73
70
|
|
|
74
|
-
db.run(`
|
|
75
71
|
CREATE TABLE IF NOT EXISTS events (
|
|
76
72
|
id TEXT PRIMARY KEY,
|
|
77
73
|
type TEXT NOT NULL,
|
|
@@ -81,21 +77,19 @@ function initSchema() {
|
|
|
81
77
|
created_at INTEGER NOT NULL,
|
|
82
78
|
FOREIGN KEY (conversationId) REFERENCES conversations(id),
|
|
83
79
|
FOREIGN KEY (sessionId) REFERENCES sessions(id)
|
|
84
|
-
)
|
|
85
|
-
`);
|
|
80
|
+
);
|
|
86
81
|
|
|
87
|
-
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_events_conversation ON events(conversationId);
|
|
88
83
|
|
|
89
|
-
db.run(`
|
|
90
84
|
CREATE TABLE IF NOT EXISTS idempotencyKeys (
|
|
91
85
|
key TEXT PRIMARY KEY,
|
|
92
86
|
value TEXT NOT NULL,
|
|
93
87
|
created_at INTEGER NOT NULL,
|
|
94
88
|
ttl INTEGER NOT NULL
|
|
95
|
-
)
|
|
96
|
-
`);
|
|
89
|
+
);
|
|
97
90
|
|
|
98
|
-
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_created ON idempotencyKeys(created_at);
|
|
92
|
+
`);
|
|
99
93
|
}
|
|
100
94
|
|
|
101
95
|
function migrateFromJson() {
|
|
@@ -393,10 +387,10 @@ export const queries = {
|
|
|
393
387
|
const conv = this.getConversation(id);
|
|
394
388
|
if (!conv) return false;
|
|
395
389
|
|
|
396
|
-
db.
|
|
397
|
-
db.
|
|
398
|
-
db.
|
|
399
|
-
db.
|
|
390
|
+
db.prepare('DELETE FROM events WHERE conversationId = ?').run(id);
|
|
391
|
+
db.prepare('DELETE FROM sessions WHERE conversationId = ?').run(id);
|
|
392
|
+
db.prepare('DELETE FROM messages WHERE conversationId = ?').run(id);
|
|
393
|
+
db.prepare('DELETE FROM conversations WHERE id = ?').run(id);
|
|
400
394
|
|
|
401
395
|
return true;
|
|
402
396
|
},
|
|
@@ -404,14 +398,11 @@ export const queries = {
|
|
|
404
398
|
cleanup() {
|
|
405
399
|
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
|
|
406
400
|
|
|
407
|
-
db.
|
|
408
|
-
db.run(
|
|
409
|
-
'DELETE FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?',
|
|
410
|
-
[thirtyDaysAgo]
|
|
411
|
-
);
|
|
401
|
+
db.prepare('DELETE FROM events WHERE created_at < ?').run(thirtyDaysAgo);
|
|
402
|
+
db.prepare('DELETE FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?').run(thirtyDaysAgo);
|
|
412
403
|
|
|
413
404
|
const now = Date.now();
|
|
414
|
-
db.
|
|
405
|
+
db.prepare('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?').run(now);
|
|
415
406
|
},
|
|
416
407
|
|
|
417
408
|
setIdempotencyKey(key, value) {
|
|
@@ -441,6 +432,86 @@ export const queries = {
|
|
|
441
432
|
|
|
442
433
|
clearIdempotencyKey(key) {
|
|
443
434
|
db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
discoverClaudeCodeConversations() {
|
|
438
|
+
const claudeHomeDir = path.join(os.homedir(), '.claude-code');
|
|
439
|
+
const conversationsDir = path.join(claudeHomeDir, 'conversations');
|
|
440
|
+
|
|
441
|
+
if (!fs.existsSync(conversationsDir)) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const discovered = [];
|
|
446
|
+
try {
|
|
447
|
+
const items = fs.readdirSync(conversationsDir, { withFileTypes: true });
|
|
448
|
+
for (const item of items) {
|
|
449
|
+
if (!item.isDirectory()) continue;
|
|
450
|
+
|
|
451
|
+
const metadataPath = path.join(conversationsDir, item.name, 'metadata.json');
|
|
452
|
+
const messagesPath = path.join(conversationsDir, item.name, 'messages.json');
|
|
453
|
+
|
|
454
|
+
if (!fs.existsSync(metadataPath) || !fs.existsSync(messagesPath)) continue;
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
458
|
+
const messages = JSON.parse(fs.readFileSync(messagesPath, 'utf-8'));
|
|
459
|
+
|
|
460
|
+
discovered.push({
|
|
461
|
+
id: item.name,
|
|
462
|
+
metadata,
|
|
463
|
+
messages,
|
|
464
|
+
source: 'claude-code'
|
|
465
|
+
});
|
|
466
|
+
} catch (e) {
|
|
467
|
+
console.error(`Error reading Claude Code conversation ${item.name}:`, e.message);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
console.error('Error discovering Claude Code conversations:', e.message);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return discovered;
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
importClaudeCodeConversations() {
|
|
478
|
+
const discovered = this.discoverClaudeCodeConversations();
|
|
479
|
+
const imported = [];
|
|
480
|
+
|
|
481
|
+
for (const conv of discovered) {
|
|
482
|
+
try {
|
|
483
|
+
const existingConv = db.prepare('SELECT id FROM conversations WHERE id = ?').get(conv.id);
|
|
484
|
+
if (existingConv) {
|
|
485
|
+
imported.push({ id: conv.id, status: 'skipped', reason: 'Already imported' });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const title = conv.metadata?.title || 'Claude Code Conversation';
|
|
490
|
+
const createdAt = conv.metadata?.created_at || Date.now();
|
|
491
|
+
const updatedAt = conv.metadata?.updated_at || Date.now();
|
|
492
|
+
|
|
493
|
+
db.prepare(
|
|
494
|
+
`INSERT INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
|
|
495
|
+
).run(conv.id, 'claude-code', title, createdAt, updatedAt, 'active');
|
|
496
|
+
|
|
497
|
+
for (const msg of (conv.messages || [])) {
|
|
498
|
+
try {
|
|
499
|
+
db.prepare(
|
|
500
|
+
`INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
|
|
501
|
+
).run(msg.id || generateId('msg'), conv.id, msg.role || 'user', msg.content || '', msg.created_at || Date.now());
|
|
502
|
+
} catch (e) {
|
|
503
|
+
console.error(`Error importing message in conversation ${conv.id}:`, e.message);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
imported.push({ id: conv.id, status: 'imported', title });
|
|
508
|
+
} catch (e) {
|
|
509
|
+
console.error(`Error importing Claude Code conversation ${conv.id}:`, e.message);
|
|
510
|
+
imported.push({ id: conv.id, status: 'error', error: e.message });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return imported;
|
|
444
515
|
}
|
|
445
516
|
};
|
|
446
517
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"test:all": "npm run test:integration && npm run test"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"better-sqlite3": "^12.6.2",
|
|
29
30
|
"ws": "^8.14.2"
|
|
30
31
|
}
|
|
31
32
|
}
|
package/server.js
CHANGED
|
@@ -11,7 +11,7 @@ import ACPConnection from './acp-launcher.js';
|
|
|
11
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const PORT = process.env.PORT || 3000;
|
|
13
13
|
const BASE_URL = (process.env.BASE_URL || '/gm').replace(/\/+$/, '');
|
|
14
|
-
const watch = process.argv.includes('--watch');
|
|
14
|
+
const watch = process.argv.includes('--no-watch') ? false : (process.argv.includes('--watch') || process.env.HOT_RELOAD !== 'false');
|
|
15
15
|
|
|
16
16
|
const staticDir = path.join(__dirname, 'static');
|
|
17
17
|
if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
|
|
@@ -126,6 +126,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
126
126
|
if (req.method === 'DELETE') {
|
|
127
127
|
const deleted = queries.deleteConversation(convMatch[1]);
|
|
128
128
|
if (!deleted) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); return; }
|
|
129
|
+
broadcastSync({ type: 'conversation_deleted', conversationId: convMatch[1] });
|
|
129
130
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
130
131
|
res.end(JSON.stringify({ deleted: true }));
|
|
131
132
|
return;
|
|
@@ -195,6 +196,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
if (routePath === '/api/import/claude-code' && req.method === 'GET') {
|
|
200
|
+
const result = queries.importClaudeCodeConversations();
|
|
201
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
202
|
+
res.end(JSON.stringify({ imported: result }));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (routePath === '/api/discover/claude-code' && req.method === 'GET') {
|
|
207
|
+
const discovered = queries.discoverClaudeCodeConversations();
|
|
208
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
209
|
+
res.end(JSON.stringify({ discovered }));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
if (routePath === '/api/home' && req.method === 'GET') {
|
|
199
214
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
200
215
|
res.end(JSON.stringify({ home: process.env.HOME || '/config' }));
|
|
@@ -276,7 +291,7 @@ function serveFile(filePath, res) {
|
|
|
276
291
|
if (err) { res.writeHead(500); res.end('Server error'); return; }
|
|
277
292
|
let content = data.toString();
|
|
278
293
|
if (ext === '.html') {
|
|
279
|
-
const baseTag = `<script>window.__BASE_URL='${BASE_URL}'
|
|
294
|
+
const baseTag = `<script>window.__BASE_URL='${BASE_URL}';</script>`;
|
|
280
295
|
content = content.replace('<head>', '<head>\n ' + baseTag);
|
|
281
296
|
if (watch) {
|
|
282
297
|
content += `\n<script>(function(){const ws=new WebSocket('ws://'+location.host+'${BASE_URL}/hot-reload');ws.onmessage=e=>{if(JSON.parse(e.data).type==='reload')location.reload()};})();</script>`;
|
|
@@ -407,6 +422,11 @@ function onServerReady() {
|
|
|
407
422
|
console.log(`GMGUI running on http://localhost:${PORT}${BASE_URL}/`);
|
|
408
423
|
console.log(`Agents: ${discoveredAgents.map(a => a.name).join(', ') || 'none'}`);
|
|
409
424
|
console.log(`Hot reload: ${watch ? 'on' : 'off'}`);
|
|
425
|
+
// Auto-import Claude Code conversations
|
|
426
|
+
const imported = queries.importClaudeCodeConversations();
|
|
427
|
+
if (imported.length > 0) {
|
|
428
|
+
console.log(`Auto-imported ${imported.filter(i => i.status === 'imported').length} Claude Code conversations`);
|
|
429
|
+
}
|
|
410
430
|
}
|
|
411
431
|
|
|
412
432
|
server.listen(PORT, onServerReady);
|
package/static/app.js
CHANGED
|
@@ -98,12 +98,31 @@ class GMGUIApp {
|
|
|
98
98
|
this.setupEventListeners();
|
|
99
99
|
await this.fetchHome();
|
|
100
100
|
await this.fetchAgents();
|
|
101
|
+
await this.autoImportClaudeCode();
|
|
101
102
|
await this.fetchConversations();
|
|
102
103
|
this.connectSyncWebSocket();
|
|
103
104
|
this.setupCrossTabSync();
|
|
105
|
+
this.startPeriodicSync();
|
|
104
106
|
this.renderAll();
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
startPeriodicSync() {
|
|
110
|
+
// Rapid sync every 10 seconds: check for new Claude Code conversations and sync
|
|
111
|
+
setInterval(() => {
|
|
112
|
+
this.autoImportClaudeCode().then(() => {
|
|
113
|
+
this.fetchConversations().then(() => this.renderChatHistory());
|
|
114
|
+
});
|
|
115
|
+
}, 10000);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async autoImportClaudeCode() {
|
|
119
|
+
try {
|
|
120
|
+
await fetch(BASE_URL + '/api/import/claude-code');
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('autoImportClaudeCode:', e);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
connectSyncWebSocket() {
|
|
108
127
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
109
128
|
this.syncWs = new ReconnectingWebSocket(
|
|
@@ -259,6 +278,11 @@ class GMGUIApp {
|
|
|
259
278
|
}
|
|
260
279
|
|
|
261
280
|
setupEventListeners() {
|
|
281
|
+
window.addEventListener('focus', () => {
|
|
282
|
+
this.autoImportClaudeCode().then(() => {
|
|
283
|
+
this.fetchConversations().then(() => this.renderChatHistory());
|
|
284
|
+
});
|
|
285
|
+
});
|
|
262
286
|
const input = document.getElementById('messageInput');
|
|
263
287
|
if (input) {
|
|
264
288
|
input.addEventListener('keydown', (e) => {
|
|
@@ -396,20 +420,24 @@ class GMGUIApp {
|
|
|
396
420
|
async deleteConversation(id) {
|
|
397
421
|
try {
|
|
398
422
|
const res = await fetch(`${BASE_URL}/api/conversations/${id}`, { method: 'DELETE' });
|
|
423
|
+
if (!res.ok) {
|
|
424
|
+
console.error('deleteConversation failed:', res.status);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
this.conversations.delete(id);
|
|
428
|
+
if (this.currentConversation === id) {
|
|
429
|
+
this.currentConversation = null;
|
|
430
|
+
const first = Array.from(this.conversations.values())[0];
|
|
431
|
+
if (first) {
|
|
432
|
+
this.displayConversation(first.id);
|
|
433
|
+
} else {
|
|
434
|
+
this.showWelcome();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
this.renderChatHistory();
|
|
399
438
|
} catch (e) {
|
|
400
439
|
console.error('deleteConversation:', e);
|
|
401
440
|
}
|
|
402
|
-
this.conversations.delete(id);
|
|
403
|
-
if (this.currentConversation === id) {
|
|
404
|
-
this.currentConversation = null;
|
|
405
|
-
const first = Array.from(this.conversations.values())[0];
|
|
406
|
-
if (first) {
|
|
407
|
-
this.displayConversation(first.id);
|
|
408
|
-
} else {
|
|
409
|
-
this.showWelcome();
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
this.renderChatHistory();
|
|
413
441
|
}
|
|
414
442
|
|
|
415
443
|
showWelcome() {
|
|
@@ -886,6 +914,44 @@ function createChatInFolder() {
|
|
|
886
914
|
app.openFolderBrowser();
|
|
887
915
|
}
|
|
888
916
|
|
|
917
|
+
async function importClaudeCodeConversations() {
|
|
918
|
+
closeNewChatModal();
|
|
919
|
+
try {
|
|
920
|
+
const res = await fetch(BASE_URL + '/api/import/claude-code');
|
|
921
|
+
const data = await res.json();
|
|
922
|
+
|
|
923
|
+
if (!data.imported) {
|
|
924
|
+
alert('No Claude Code conversations found to import.');
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const imported = data.imported.filter(r => r.status === 'imported');
|
|
929
|
+
const skipped = data.imported.filter(r => r.status === 'skipped');
|
|
930
|
+
const errors = data.imported.filter(r => r.status === 'error');
|
|
931
|
+
|
|
932
|
+
let message = `Import complete!\n\n`;
|
|
933
|
+
if (imported.length > 0) {
|
|
934
|
+
message += `✓ Imported: ${imported.length} conversation(s)\n`;
|
|
935
|
+
}
|
|
936
|
+
if (skipped.length > 0) {
|
|
937
|
+
message += `⊘ Skipped: ${skipped.length} (already imported)\n`;
|
|
938
|
+
}
|
|
939
|
+
if (errors.length > 0) {
|
|
940
|
+
message += `✗ Errors: ${errors.length}\n`;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
alert(message.trim());
|
|
944
|
+
|
|
945
|
+
if (imported.length > 0) {
|
|
946
|
+
await app.fetchConversations();
|
|
947
|
+
app.renderAll();
|
|
948
|
+
}
|
|
949
|
+
} catch (e) {
|
|
950
|
+
console.error('Import error:', e);
|
|
951
|
+
alert('Failed to import Claude Code conversations: ' + e.message);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
889
955
|
function sendMessage() { app.sendMessage(); }
|
|
890
956
|
|
|
891
957
|
function toggleSidebar() {
|
package/static/index.html
CHANGED
|
@@ -129,6 +129,13 @@
|
|
|
129
129
|
<div class="chat-option-desc">Contextualize chat to a specific folder</div>
|
|
130
130
|
</div>
|
|
131
131
|
</button>
|
|
132
|
+
<button class="chat-option-btn" onclick="importClaudeCodeConversations()">
|
|
133
|
+
<span class="chat-option-icon">📥</span>
|
|
134
|
+
<div class="chat-option-content">
|
|
135
|
+
<div class="chat-option-title">Import Claude Code conversations</div>
|
|
136
|
+
<div class="chat-option-desc">Load existing conversations from Claude Code</div>
|
|
137
|
+
</div>
|
|
138
|
+
</button>
|
|
132
139
|
</div>
|
|
133
140
|
</div>
|
|
134
141
|
</div>
|