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 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.run(`
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
- db.run(`CREATE INDEX IF NOT EXISTS idx_conversations_agent ON conversations(agentId)`);
43
- db.run(`CREATE INDEX IF NOT EXISTS idx_conversations_updated ON conversations(updated_at DESC)`);
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
- db.run(`CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversationId)`);
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
- db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_conversation ON sessions(conversationId)`);
72
- db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(conversationId, status)`);
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
- db.run(`CREATE INDEX IF NOT EXISTS idx_events_conversation ON events(conversationId)`);
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
- db.run(`CREATE INDEX IF NOT EXISTS idx_idempotency_created ON idempotencyKeys(created_at)`);
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.run('DELETE FROM messages WHERE conversationId = ?', [id]);
397
- db.run('DELETE FROM sessions WHERE conversationId = ?', [id]);
398
- db.run('DELETE FROM events WHERE conversationId = ?', [id]);
399
- db.run('DELETE FROM conversations WHERE id = ?', [id]);
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.run('DELETE FROM events WHERE created_at < ?', [thirtyDaysAgo]);
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.run('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?', [now]);
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.4",
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}'; window.__AUTH_TOKEN=localStorage.getItem('gmgui-token');</script>`;
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>