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/README.md +213 -0
- package/acp-launcher.js +348 -0
- package/bin/gmgui.cjs +70 -0
- package/database.js +447 -0
- package/install.sh +147 -0
- package/package.json +23 -0
- package/server.js +412 -0
- package/static/app.js +925 -0
- package/static/index.html +201 -0
- package/static/rippleui.css +208 -0
- package/static/styles.css +1432 -0
- package/static/theme.js +72 -0
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
|
+
}
|