agentgui 1.0.3

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 ADDED
@@ -0,0 +1,447 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const dbDir = path.join(os.homedir(), '.gmgui');
6
+ const dbFilePath = path.join(dbDir, 'data.db');
7
+ const oldJsonPath = path.join(dbDir, 'data.json');
8
+
9
+ if (!fs.existsSync(dbDir)) {
10
+ fs.mkdirSync(dbDir, { recursive: true });
11
+ }
12
+
13
+ let db;
14
+ try {
15
+ const Database = (await import('bun:sqlite')).default;
16
+ db = new Database(dbFilePath);
17
+ db.run('PRAGMA journal_mode = WAL');
18
+ db.run('PRAGMA foreign_keys = ON');
19
+ } catch (e) {
20
+ try {
21
+ const sqlite3 = require('better-sqlite3');
22
+ db = new sqlite3(dbFilePath);
23
+ db.pragma('journal_mode = WAL');
24
+ db.pragma('foreign_keys = ON');
25
+ } catch (e2) {
26
+ throw new Error('SQLite database is required. Please run with bun (recommended) or install better-sqlite3: npm install better-sqlite3');
27
+ }
28
+ }
29
+
30
+ function initSchema() {
31
+ db.run(`
32
+ CREATE TABLE IF NOT EXISTS conversations (
33
+ id TEXT PRIMARY KEY,
34
+ agentId TEXT NOT NULL,
35
+ title TEXT,
36
+ created_at INTEGER NOT NULL,
37
+ updated_at INTEGER NOT NULL,
38
+ status TEXT DEFAULT 'active'
39
+ )
40
+ `);
41
+
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)`);
44
+
45
+ db.run(`
46
+ CREATE TABLE IF NOT EXISTS messages (
47
+ id TEXT PRIMARY KEY,
48
+ conversationId TEXT NOT NULL,
49
+ role TEXT NOT NULL,
50
+ content TEXT NOT NULL,
51
+ created_at INTEGER NOT NULL,
52
+ FOREIGN KEY (conversationId) REFERENCES conversations(id)
53
+ )
54
+ `);
55
+
56
+ db.run(`CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversationId)`);
57
+
58
+ db.run(`
59
+ CREATE TABLE IF NOT EXISTS sessions (
60
+ id TEXT PRIMARY KEY,
61
+ conversationId TEXT NOT NULL,
62
+ status TEXT NOT NULL,
63
+ started_at INTEGER NOT NULL,
64
+ completed_at INTEGER,
65
+ response TEXT,
66
+ error TEXT,
67
+ FOREIGN KEY (conversationId) REFERENCES conversations(id)
68
+ )
69
+ `);
70
+
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)`);
73
+
74
+ db.run(`
75
+ CREATE TABLE IF NOT EXISTS events (
76
+ id TEXT PRIMARY KEY,
77
+ type TEXT NOT NULL,
78
+ conversationId TEXT,
79
+ sessionId TEXT,
80
+ data TEXT NOT NULL,
81
+ created_at INTEGER NOT NULL,
82
+ FOREIGN KEY (conversationId) REFERENCES conversations(id),
83
+ FOREIGN KEY (sessionId) REFERENCES sessions(id)
84
+ )
85
+ `);
86
+
87
+ db.run(`CREATE INDEX IF NOT EXISTS idx_events_conversation ON events(conversationId)`);
88
+
89
+ db.run(`
90
+ CREATE TABLE IF NOT EXISTS idempotencyKeys (
91
+ key TEXT PRIMARY KEY,
92
+ value TEXT NOT NULL,
93
+ created_at INTEGER NOT NULL,
94
+ ttl INTEGER NOT NULL
95
+ )
96
+ `);
97
+
98
+ db.run(`CREATE INDEX IF NOT EXISTS idx_idempotency_created ON idempotencyKeys(created_at)`);
99
+ }
100
+
101
+ function migrateFromJson() {
102
+ if (!fs.existsSync(oldJsonPath)) return;
103
+
104
+ try {
105
+ const content = fs.readFileSync(oldJsonPath, 'utf-8');
106
+ const data = JSON.parse(content);
107
+
108
+ if (data.conversations) {
109
+ for (const id in data.conversations) {
110
+ const conv = data.conversations[id];
111
+ db.run(
112
+ `INSERT OR REPLACE INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`,
113
+ [conv.id, conv.agentId, conv.title || null, conv.created_at, conv.updated_at, conv.status || 'active']
114
+ );
115
+ }
116
+ }
117
+
118
+ if (data.messages) {
119
+ for (const id in data.messages) {
120
+ const msg = data.messages[id];
121
+ db.run(
122
+ `INSERT OR REPLACE INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
123
+ [msg.id, msg.conversationId, msg.role, msg.content, msg.created_at]
124
+ );
125
+ }
126
+ }
127
+
128
+ if (data.sessions) {
129
+ for (const id in data.sessions) {
130
+ const sess = data.sessions[id];
131
+ db.run(
132
+ `INSERT OR REPLACE INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`,
133
+ [sess.id, sess.conversationId, sess.status, sess.started_at, sess.completed_at || null, sess.response || null, sess.error || null]
134
+ );
135
+ }
136
+ }
137
+
138
+ if (data.events) {
139
+ for (const id in data.events) {
140
+ const evt = data.events[id];
141
+ db.run(
142
+ `INSERT OR REPLACE INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
143
+ [evt.id, evt.type, evt.conversationId || null, evt.sessionId || null, JSON.stringify(evt.data), evt.created_at]
144
+ );
145
+ }
146
+ }
147
+
148
+ if (data.idempotencyKeys) {
149
+ for (const key in data.idempotencyKeys) {
150
+ const entry = data.idempotencyKeys[key];
151
+ db.run(
152
+ `INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)`,
153
+ [key, JSON.stringify(entry.value), entry.created_at, entry.ttl]
154
+ );
155
+ }
156
+ }
157
+
158
+ fs.renameSync(oldJsonPath, `${oldJsonPath}.migrated`);
159
+ console.log('Migrated data from JSON to SQLite');
160
+ } catch (e) {
161
+ console.error('Error during migration:', e.message);
162
+ }
163
+ }
164
+
165
+ initSchema();
166
+ migrateFromJson();
167
+
168
+ function generateId(prefix) {
169
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
170
+ }
171
+
172
+ export const queries = {
173
+ createConversation(agentId, title = null) {
174
+ const id = generateId('conv');
175
+ const now = Date.now();
176
+ const stmt = db.prepare(
177
+ `INSERT INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
178
+ );
179
+ stmt.run(id, agentId, title, now, now, 'active');
180
+
181
+ return {
182
+ id,
183
+ agentId,
184
+ title,
185
+ created_at: now,
186
+ updated_at: now,
187
+ status: 'active'
188
+ };
189
+ },
190
+
191
+ getConversation(id) {
192
+ const stmt = db.prepare('SELECT * FROM conversations WHERE id = ?');
193
+ return stmt.get(id);
194
+ },
195
+
196
+ getAllConversations() {
197
+ const stmt = db.prepare('SELECT * FROM conversations ORDER BY updated_at DESC');
198
+ return stmt.all();
199
+ },
200
+
201
+ updateConversation(id, data) {
202
+ const conv = this.getConversation(id);
203
+ if (!conv) return null;
204
+
205
+ const now = Date.now();
206
+ const title = data.title !== undefined ? data.title : conv.title;
207
+ const status = data.status !== undefined ? data.status : conv.status;
208
+
209
+ const stmt = db.prepare(
210
+ `UPDATE conversations SET title = ?, status = ?, updated_at = ? WHERE id = ?`
211
+ );
212
+ stmt.run(title, status, now, id);
213
+
214
+ return {
215
+ ...conv,
216
+ title,
217
+ status,
218
+ updated_at: now
219
+ };
220
+ },
221
+
222
+ createMessage(conversationId, role, content, idempotencyKey = null) {
223
+ if (idempotencyKey) {
224
+ const cached = this.getIdempotencyKey(idempotencyKey);
225
+ if (cached) return JSON.parse(cached);
226
+ }
227
+
228
+ const id = generateId('msg');
229
+ const now = Date.now();
230
+
231
+ const stmt = db.prepare(
232
+ `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
233
+ );
234
+ stmt.run(id, conversationId, role, content, now);
235
+
236
+ const updateConvStmt = db.prepare('UPDATE conversations SET updated_at = ? WHERE id = ?');
237
+ updateConvStmt.run(now, conversationId);
238
+
239
+ const message = {
240
+ id,
241
+ conversationId,
242
+ role,
243
+ content,
244
+ created_at: now
245
+ };
246
+
247
+ if (idempotencyKey) {
248
+ this.setIdempotencyKey(idempotencyKey, message);
249
+ }
250
+
251
+ return message;
252
+ },
253
+
254
+ getMessage(id) {
255
+ const stmt = db.prepare('SELECT * FROM messages WHERE id = ?');
256
+ return stmt.get(id);
257
+ },
258
+
259
+ getConversationMessages(conversationId) {
260
+ const stmt = db.prepare(
261
+ 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC'
262
+ );
263
+ return stmt.all(conversationId);
264
+ },
265
+
266
+ createSession(conversationId) {
267
+ const id = generateId('sess');
268
+ const now = Date.now();
269
+
270
+ const stmt = db.prepare(
271
+ `INSERT INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
272
+ );
273
+ stmt.run(id, conversationId, 'pending', now, null, null, null);
274
+
275
+ return {
276
+ id,
277
+ conversationId,
278
+ status: 'pending',
279
+ started_at: now,
280
+ completed_at: null,
281
+ response: null,
282
+ error: null
283
+ };
284
+ },
285
+
286
+ getSession(id) {
287
+ const stmt = db.prepare('SELECT * FROM sessions WHERE id = ?');
288
+ return stmt.get(id);
289
+ },
290
+
291
+ getConversationSessions(conversationId) {
292
+ const stmt = db.prepare(
293
+ 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC'
294
+ );
295
+ return stmt.all(conversationId);
296
+ },
297
+
298
+ updateSession(id, data) {
299
+ const session = this.getSession(id);
300
+ if (!session) return null;
301
+
302
+ const status = data.status !== undefined ? data.status : session.status;
303
+ const response = data.response !== undefined ? data.response : session.response;
304
+ const error = data.error !== undefined ? data.error : session.error;
305
+ const completed_at = data.completed_at !== undefined ? data.completed_at : session.completed_at;
306
+
307
+ const stmt = db.prepare(
308
+ `UPDATE sessions SET status = ?, response = ?, error = ?, completed_at = ? WHERE id = ?`
309
+ );
310
+
311
+ try {
312
+ stmt.run(status, response, error, completed_at, id);
313
+ return {
314
+ ...session,
315
+ status,
316
+ response,
317
+ error,
318
+ completed_at
319
+ };
320
+ } catch (e) {
321
+ throw e;
322
+ }
323
+ },
324
+
325
+ getLatestSession(conversationId) {
326
+ const stmt = db.prepare(
327
+ 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT 1'
328
+ );
329
+ return stmt.get(conversationId) || null;
330
+ },
331
+
332
+ getSessionsByStatus(conversationId, status) {
333
+ const stmt = db.prepare(
334
+ 'SELECT * FROM sessions WHERE conversationId = ? AND status = ? ORDER BY started_at DESC'
335
+ );
336
+ return stmt.all(conversationId, status);
337
+ },
338
+
339
+ createEvent(type, data, conversationId = null, sessionId = null) {
340
+ const id = generateId('evt');
341
+ const now = Date.now();
342
+
343
+ const stmt = db.prepare(
344
+ `INSERT INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
345
+ );
346
+ stmt.run(id, type, conversationId, sessionId, JSON.stringify(data), now);
347
+
348
+ return {
349
+ id,
350
+ type,
351
+ conversationId,
352
+ sessionId,
353
+ data,
354
+ created_at: now
355
+ };
356
+ },
357
+
358
+ getEvent(id) {
359
+ const stmt = db.prepare('SELECT * FROM events WHERE id = ?');
360
+ const row = stmt.get(id);
361
+ if (row) {
362
+ return {
363
+ ...row,
364
+ data: JSON.parse(row.data)
365
+ };
366
+ }
367
+ return undefined;
368
+ },
369
+
370
+ getConversationEvents(conversationId) {
371
+ const stmt = db.prepare(
372
+ 'SELECT * FROM events WHERE conversationId = ? ORDER BY created_at ASC'
373
+ );
374
+ const rows = stmt.all(conversationId);
375
+ return rows.map(row => ({
376
+ ...row,
377
+ data: JSON.parse(row.data)
378
+ }));
379
+ },
380
+
381
+ getSessionEvents(sessionId) {
382
+ const stmt = db.prepare(
383
+ 'SELECT * FROM events WHERE sessionId = ? ORDER BY created_at ASC'
384
+ );
385
+ const rows = stmt.all(sessionId);
386
+ return rows.map(row => ({
387
+ ...row,
388
+ data: JSON.parse(row.data)
389
+ }));
390
+ },
391
+
392
+ deleteConversation(id) {
393
+ const conv = this.getConversation(id);
394
+ if (!conv) return false;
395
+
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]);
400
+
401
+ return true;
402
+ },
403
+
404
+ cleanup() {
405
+ const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
406
+
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
+ );
412
+
413
+ const now = Date.now();
414
+ db.run('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?', [now]);
415
+ },
416
+
417
+ setIdempotencyKey(key, value) {
418
+ const now = Date.now();
419
+ const ttl = 24 * 60 * 60 * 1000;
420
+
421
+ const stmt = db.prepare(
422
+ 'INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)'
423
+ );
424
+ stmt.run(key, JSON.stringify(value), now, ttl);
425
+ },
426
+
427
+ getIdempotencyKey(key) {
428
+ const stmt = db.prepare('SELECT * FROM idempotencyKeys WHERE key = ?');
429
+ const entry = stmt.get(key);
430
+
431
+ if (!entry) return null;
432
+
433
+ const isExpired = Date.now() - entry.created_at > entry.ttl;
434
+ if (isExpired) {
435
+ db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
436
+ return null;
437
+ }
438
+
439
+ return entry.value;
440
+ },
441
+
442
+ clearIdempotencyKey(key) {
443
+ db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
444
+ }
445
+ };
446
+
447
+ export default { queries };
package/install.sh ADDED
@@ -0,0 +1,147 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # GMGUI Installation Script
5
+ # Robust, error-handled installation with proper cleanup
6
+
7
+ # Enable error handling and cleanup
8
+ TMPDIR=""
9
+ EXIT_CODE=0
10
+
11
+ cleanup() {
12
+ local code=$?
13
+ if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then
14
+ rm -rf "$TMPDIR" 2>/dev/null || true
15
+ fi
16
+ if [ $code -ne 0 ] && [ $code -ne 130 ]; then
17
+ echo "Error: Installation failed. Check messages above." >&2
18
+ fi
19
+ exit $code
20
+ }
21
+
22
+ # Handle interrupts gracefully
23
+ handle_interrupt() {
24
+ echo ""
25
+ echo "Installation interrupted. Cleaning up..." >&2
26
+ exit 130
27
+ }
28
+
29
+ trap cleanup EXIT
30
+ trap handle_interrupt SIGINT SIGTERM
31
+
32
+ # Detect runtime (prefer bun, fall back to node)
33
+ RUNTIME=""
34
+ if command -v bun &> /dev/null; then
35
+ RUNTIME=bun
36
+ elif command -v node &> /dev/null; then
37
+ RUNTIME=node
38
+ else
39
+ echo "Error: Neither bun nor node is installed." >&2
40
+ echo "Install one with:" >&2
41
+ echo " • Bun (recommended, 3-4x faster):" >&2
42
+ echo " curl -fsSL https://bun.sh/install | bash" >&2
43
+ echo " • Node.js:" >&2
44
+ echo " curl https://nodejs.org/en/download/" >&2
45
+ exit 1
46
+ fi
47
+
48
+ # Verify curl or wget is available
49
+ if ! command -v curl &> /dev/null && ! command -v wget &> /dev/null; then
50
+ echo "Error: Neither curl nor wget found. Install one to download gmgui." >&2
51
+ exit 1
52
+ fi
53
+
54
+ # Create temp directory with secure permissions
55
+ TMPDIR=$(mktemp -d) || {
56
+ echo "Error: Could not create temporary directory." >&2
57
+ exit 1
58
+ }
59
+
60
+ # Verify we have write access to temp directory
61
+ if ! [ -w "$TMPDIR" ]; then
62
+ echo "Error: No write permission to temporary directory." >&2
63
+ exit 1
64
+ fi
65
+
66
+ # Check disk space (need at least 200MB)
67
+ AVAILABLE_KB=$(df "$TMPDIR" 2>/dev/null | tail -1 | awk '{print $4}' || echo 0)
68
+ if [ "$AVAILABLE_KB" -lt 200000 ]; then
69
+ echo "Warning: Low disk space. At least 200MB recommended." >&2
70
+ fi
71
+
72
+ echo "Downloading gmgui from GitHub..."
73
+ cd "$TMPDIR"
74
+
75
+ # Use git if available, otherwise use curl/wget to download tarball
76
+ if command -v git &> /dev/null; then
77
+ # Git clone with error handling
78
+ if ! git clone --depth=1 https://github.com/AnEntrypoint/gmgui.git . 2>&1 | grep -v "^Cloning"; then
79
+ echo "Warning: Git clone failed, trying tarball download..." >&2
80
+ # Fallback to tarball
81
+ if command -v curl &> /dev/null; then
82
+ curl -fsSL https://github.com/AnEntrypoint/gmgui/archive/refs/heads/main.tar.gz | tar xz --strip-components=1 || {
83
+ echo "Error: Could not download gmgui from GitHub." >&2
84
+ exit 1
85
+ }
86
+ else
87
+ wget -qO- https://github.com/AnEntrypoint/gmgui/archive/refs/heads/main.tar.gz | tar xz --strip-components=1 || {
88
+ echo "Error: Could not download gmgui from GitHub." >&2
89
+ exit 1
90
+ }
91
+ fi
92
+ fi
93
+ else
94
+ echo "Downloading tarball (git not available)..."
95
+ # Download tarball with curl or wget
96
+ if command -v curl &> /dev/null; then
97
+ curl -fsSL https://github.com/AnEntrypoint/gmgui/archive/refs/heads/main.tar.gz | tar xz --strip-components=1 || {
98
+ echo "Error: Could not download gmgui from GitHub." >&2
99
+ exit 1
100
+ }
101
+ else
102
+ wget -qO- https://github.com/AnEntrypoint/gmgui/archive/refs/heads/main.tar.gz | tar xz --strip-components=1 || {
103
+ echo "Error: Could not download gmgui from GitHub." >&2
104
+ exit 1
105
+ }
106
+ fi
107
+ fi
108
+
109
+ # Verify essential files were downloaded
110
+ if [ ! -f server.js ] || [ ! -f package.json ]; then
111
+ echo "Error: Downloaded files appear corrupted. Missing server.js or package.json." >&2
112
+ exit 1
113
+ fi
114
+
115
+ echo "Installing dependencies with $RUNTIME..."
116
+ if [ "$RUNTIME" = "bun" ]; then
117
+ if ! bun install --frozen-lockfile 2>&1 | grep -E "added|removed|found|vulnerabilities" | tail -1; then
118
+ # Warn but don't fail if grep doesn't match
119
+ bun install --frozen-lockfile 2>&1 | tail -3
120
+ fi
121
+ else
122
+ if ! npm install 2>&1 | grep -E "added|removed|found|vulnerabilities" | tail -1; then
123
+ # Warn but don't fail if grep doesn't match
124
+ npm install 2>&1 | tail -3
125
+ fi
126
+
127
+ # For Node.js, install better-sqlite3 if not already present
128
+ echo "Installing better-sqlite3 for Node.js..."
129
+ if ! npm install better-sqlite3 2>&1 | grep -E "added|removed|found|vulnerabilities" | tail -1; then
130
+ # Warn but don't fail - may already be installed
131
+ echo "Note: better-sqlite3 installation completed." >&2
132
+ fi
133
+ fi
134
+
135
+ echo ""
136
+ echo "✓ Installation complete!"
137
+ echo ""
138
+ echo "Starting gmgui server on http://localhost:3000"
139
+ echo "Press Ctrl+C to stop"
140
+ echo ""
141
+
142
+ # Start server with proper error handling
143
+ if [ "$RUNTIME" = "bun" ]; then
144
+ exec bun run server.js
145
+ else
146
+ exec node server.js
147
+ fi
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "agentgui",
3
+ "version": "1.0.3",
4
+ "description": "Multi-agent ACP client with real-time communication",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "bin": {
8
+ "gmgui": "./bin/gmgui.cjs",
9
+ "start": "./bin/gmgui.cjs"
10
+ },
11
+ "scripts": {
12
+ "start": "node server.js",
13
+ "start:bun": "bun run server-bun.js",
14
+ "dev": "node server.js --watch",
15
+ "dev:bun": "bun run server-bun.js --watch",
16
+ "test": "node run-browser-tests.js",
17
+ "test:integration": "./test-integration.sh",
18
+ "test:all": "npm run test:integration && npm run test"
19
+ },
20
+ "dependencies": {
21
+ "ws": "^8.14.2"
22
+ }
23
+ }