agentgate 0.3.1 → 0.4.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/package.json +1 -1
- package/src/index.js +76 -23
- package/src/lib/db.js +43 -1
- package/src/lib/queueExecutor.js +1 -5
- package/src/lib/socketManager.js +73 -0
- package/src/routes/agents.js +73 -0
- package/src/routes/bluesky.js +9 -0
- package/src/routes/queue.js +78 -3
- package/src/routes/ui/auth.js +149 -0
- package/src/routes/ui/keys.js +302 -0
- package/src/routes/ui/messages.js +451 -0
- package/src/routes/ui/queue.js +420 -0
- package/src/routes/ui/settings.js +59 -0
- package/src/routes/ui/shared.js +139 -0
- package/src/routes/ui-new.js +196 -0
- package/src/routes/ui.js +260 -162
- package/src/lib/notifier.js +0 -286
package/src/lib/notifier.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { getSetting, updateQueueNotification } from './db.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Send a notification to Clawdbot webhook when queue items complete/fail
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const DEFAULT_RETRY_ATTEMPTS = 3;
|
|
8
|
-
const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Find the most relevant result to display (usually last one with a URL, or first failure)
|
|
12
|
-
*/
|
|
13
|
-
function findRelevantResult(results, forError = false) {
|
|
14
|
-
if (!results?.length) return null;
|
|
15
|
-
|
|
16
|
-
if (forError) {
|
|
17
|
-
// For errors, find the first non-ok result
|
|
18
|
-
return results.find(r => !r.ok) || results[results.length - 1];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// For success, find the last result with a useful URL (PR, issue, etc.)
|
|
22
|
-
// Search backwards to get the most relevant one (e.g., PR URL, not branch ref)
|
|
23
|
-
for (let i = results.length - 1; i >= 0; i--) {
|
|
24
|
-
const r = results[i];
|
|
25
|
-
if (r.body?.html_url || r.body?.url) {
|
|
26
|
-
return r;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Fallback to last result
|
|
31
|
-
return results[results.length - 1];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Format the notification text for a queue entry
|
|
36
|
-
*/
|
|
37
|
-
function formatNotification(entry) {
|
|
38
|
-
const emoji = entry.status === 'completed' ? '✅' :
|
|
39
|
-
entry.status === 'failed' ? '❌' : '🚫';
|
|
40
|
-
|
|
41
|
-
let text = `${emoji} [agentgate] Queue #${entry.id} ${entry.status}`;
|
|
42
|
-
text += `\n→ ${entry.service}/${entry.account_name}`;
|
|
43
|
-
|
|
44
|
-
// Include key result info (e.g., PR URL, issue URL)
|
|
45
|
-
if (entry.status === 'completed' && entry.results?.length) {
|
|
46
|
-
const relevantResult = findRelevantResult(entry.results, false);
|
|
47
|
-
if (relevantResult?.body) {
|
|
48
|
-
// GitHub PR/Issue
|
|
49
|
-
if (relevantResult.body.html_url) {
|
|
50
|
-
text += `\n→ ${relevantResult.body.html_url}`;
|
|
51
|
-
}
|
|
52
|
-
// Other useful fields
|
|
53
|
-
else if (relevantResult.body.url) {
|
|
54
|
-
text += `\n→ ${relevantResult.body.url}`;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Include error info for failures
|
|
60
|
-
if (entry.status === 'failed' && entry.results?.length) {
|
|
61
|
-
const failingResult = findRelevantResult(entry.results, true);
|
|
62
|
-
if (failingResult.error) {
|
|
63
|
-
text += `\n→ Error: ${failingResult.error}`;
|
|
64
|
-
} else if (failingResult.body?.message) {
|
|
65
|
-
text += `\n→ Error: ${failingResult.body.message}`;
|
|
66
|
-
} else if (!failingResult.ok && failingResult.status) {
|
|
67
|
-
text += `\n→ Error: HTTP ${failingResult.status}`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Include rejection reason
|
|
72
|
-
if (entry.status === 'rejected' && entry.rejection_reason) {
|
|
73
|
-
text += `\n→ Reason: ${entry.rejection_reason}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Original comment/intent
|
|
77
|
-
if (entry.comment) {
|
|
78
|
-
// Truncate long comments
|
|
79
|
-
const comment = entry.comment.length > 100
|
|
80
|
-
? entry.comment.substring(0, 100) + '...'
|
|
81
|
-
: entry.comment;
|
|
82
|
-
text += `\nOriginal: "${comment}"`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return text;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Sleep helper
|
|
90
|
-
*/
|
|
91
|
-
function sleep(ms) {
|
|
92
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Send notification to Clawdbot
|
|
97
|
-
* @param {object} entry - The queue entry to notify about
|
|
98
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
99
|
-
*/
|
|
100
|
-
export async function notifyClawdbot(entry) {
|
|
101
|
-
const config = getSetting('notifications');
|
|
102
|
-
|
|
103
|
-
if (!config?.clawdbot?.enabled) {
|
|
104
|
-
return { success: true, skipped: true };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const { url, token, events, retryAttempts, retryDelayMs } = config.clawdbot;
|
|
108
|
-
|
|
109
|
-
// Check if this event type should be notified
|
|
110
|
-
const allowedEvents = events || ['completed', 'failed'];
|
|
111
|
-
if (!allowedEvents.includes(entry.status)) {
|
|
112
|
-
return { success: true, skipped: true };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!url || !token) {
|
|
116
|
-
const error = 'Clawdbot notification config incomplete (missing url or token)';
|
|
117
|
-
updateQueueNotification(entry.id, false, error);
|
|
118
|
-
return { success: false, error };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const payload = {
|
|
122
|
-
text: formatNotification(entry),
|
|
123
|
-
mode: 'now'
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const maxAttempts = retryAttempts || DEFAULT_RETRY_ATTEMPTS;
|
|
127
|
-
const delay = retryDelayMs || DEFAULT_RETRY_DELAY_MS;
|
|
128
|
-
let lastError = null;
|
|
129
|
-
|
|
130
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
131
|
-
try {
|
|
132
|
-
const response = await fetch(url, {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Authorization': `Bearer ${token}`,
|
|
136
|
-
'Content-Type': 'application/json'
|
|
137
|
-
},
|
|
138
|
-
body: JSON.stringify(payload)
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (response.ok) {
|
|
142
|
-
// Success
|
|
143
|
-
updateQueueNotification(entry.id, true, null);
|
|
144
|
-
return { success: true };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Non-OK response
|
|
148
|
-
const text = await response.text().catch(() => '');
|
|
149
|
-
lastError = `HTTP ${response.status}: ${text.substring(0, 100)}`;
|
|
150
|
-
|
|
151
|
-
} catch (err) {
|
|
152
|
-
lastError = err.message;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Retry after delay (unless last attempt)
|
|
156
|
-
if (attempt < maxAttempts) {
|
|
157
|
-
await sleep(delay);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// All retries failed
|
|
162
|
-
updateQueueNotification(entry.id, false, lastError);
|
|
163
|
-
return { success: false, error: lastError };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Retry notification for a specific queue entry
|
|
168
|
-
*/
|
|
169
|
-
export async function retryNotification(entryId, getQueueEntry) {
|
|
170
|
-
const entry = getQueueEntry(entryId);
|
|
171
|
-
if (!entry) {
|
|
172
|
-
return { success: false, error: 'Queue entry not found' };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Only retry for completed/failed/rejected entries
|
|
176
|
-
if (!['completed', 'failed', 'rejected'].includes(entry.status)) {
|
|
177
|
-
return { success: false, error: 'Can only notify for completed/failed/rejected entries' };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return notifyClawdbot(entry);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Format a short summary line for batch notifications
|
|
185
|
-
*/
|
|
186
|
-
function formatBatchLine(entry) {
|
|
187
|
-
const emoji = entry.status === 'completed' ? '✅' :
|
|
188
|
-
entry.status === 'failed' ? '❌' : '🚫';
|
|
189
|
-
|
|
190
|
-
let line = `${emoji} #${entry.id.substring(0, 8)} - ${entry.service}/${entry.account_name}`;
|
|
191
|
-
|
|
192
|
-
// Add brief result info
|
|
193
|
-
if (entry.status === 'completed') {
|
|
194
|
-
const relevantResult = findRelevantResult(entry.results, false);
|
|
195
|
-
if (relevantResult?.body?.html_url) {
|
|
196
|
-
line += ` - ${relevantResult.body.html_url}`;
|
|
197
|
-
}
|
|
198
|
-
} else if (entry.status === 'failed') {
|
|
199
|
-
const failingResult = findRelevantResult(entry.results, true);
|
|
200
|
-
const err = failingResult?.error || failingResult?.body?.message || (failingResult && !failingResult.ok ? `HTTP ${failingResult.status || '?'}` : 'Unknown error');
|
|
201
|
-
line += ` - ${err.substring(0, 50)}`;
|
|
202
|
-
} else if (entry.status === 'rejected') {
|
|
203
|
-
line += ` - ${(entry.rejection_reason || 'rejected').substring(0, 50)}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return line;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Send a single batched notification for multiple queue entries
|
|
211
|
-
* @param {object[]} entries - Array of queue entries to notify about
|
|
212
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
213
|
-
*/
|
|
214
|
-
export async function notifyClawdbotBatch(entries) {
|
|
215
|
-
const config = getSetting('notifications');
|
|
216
|
-
|
|
217
|
-
if (!config?.clawdbot?.enabled) {
|
|
218
|
-
return { success: true, skipped: true };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const { url, token, retryAttempts, retryDelayMs } = config.clawdbot;
|
|
222
|
-
|
|
223
|
-
if (!url || !token) {
|
|
224
|
-
const error = 'Clawdbot notification config incomplete (missing url or token)';
|
|
225
|
-
for (const entry of entries) {
|
|
226
|
-
updateQueueNotification(entry.id, false, error);
|
|
227
|
-
}
|
|
228
|
-
return { success: false, error };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Build batched message
|
|
232
|
-
const completed = entries.filter(e => e.status === 'completed').length;
|
|
233
|
-
const failed = entries.filter(e => e.status === 'failed').length;
|
|
234
|
-
const rejected = entries.filter(e => e.status === 'rejected').length;
|
|
235
|
-
|
|
236
|
-
const counts = [];
|
|
237
|
-
if (completed) counts.push(`${completed} completed`);
|
|
238
|
-
if (failed) counts.push(`${failed} failed`);
|
|
239
|
-
if (rejected) counts.push(`${rejected} rejected`);
|
|
240
|
-
|
|
241
|
-
let text = `📬 [agentgate] Catch-up: ${entries.length} item${entries.length > 1 ? 's' : ''} (${counts.join(', ')})\n\n`;
|
|
242
|
-
text += entries.map(formatBatchLine).join('\n');
|
|
243
|
-
|
|
244
|
-
const payload = { text, mode: 'now' };
|
|
245
|
-
|
|
246
|
-
const maxAttempts = retryAttempts || DEFAULT_RETRY_ATTEMPTS;
|
|
247
|
-
const delay = retryDelayMs || DEFAULT_RETRY_DELAY_MS;
|
|
248
|
-
let lastError = null;
|
|
249
|
-
|
|
250
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
251
|
-
try {
|
|
252
|
-
const response = await fetch(url, {
|
|
253
|
-
method: 'POST',
|
|
254
|
-
headers: {
|
|
255
|
-
'Authorization': `Bearer ${token}`,
|
|
256
|
-
'Content-Type': 'application/json'
|
|
257
|
-
},
|
|
258
|
-
body: JSON.stringify(payload)
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
if (response.ok) {
|
|
262
|
-
// Success - mark all as notified
|
|
263
|
-
for (const entry of entries) {
|
|
264
|
-
updateQueueNotification(entry.id, true, null);
|
|
265
|
-
}
|
|
266
|
-
return { success: true };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const respText = await response.text().catch(() => '');
|
|
270
|
-
lastError = `HTTP ${response.status}: ${respText.substring(0, 100)}`;
|
|
271
|
-
|
|
272
|
-
} catch (err) {
|
|
273
|
-
lastError = err.message;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (attempt < maxAttempts) {
|
|
277
|
-
await sleep(delay);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// All retries failed
|
|
282
|
-
for (const entry of entries) {
|
|
283
|
-
updateQueueNotification(entry.id, false, lastError);
|
|
284
|
-
}
|
|
285
|
-
return { success: false, error: lastError };
|
|
286
|
-
}
|