nightytidy 0.2.2 → 0.2.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 +2 -2
- package/package.json +1 -1
- package/src/agent/firebase-auth.js +84 -7
- package/src/agent/index.js +92 -113
- package/src/agent/websocket-server.js +3 -2
package/README.md
CHANGED
|
@@ -38,10 +38,10 @@ The agent runs locally at `127.0.0.1:48372`. The web app connects to it via WebS
|
|
|
38
38
|
|
|
39
39
|
### Desktop GUI (local)
|
|
40
40
|
|
|
41
|
-
A Chrome app-mode window for fully local use — no account needed:
|
|
41
|
+
A Chrome app-mode window for fully local use — no account needed. Clone the repo and run:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
|
|
44
|
+
npm run gui
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
From there:
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { debug, info, warn } from '../logger.js';
|
|
2
2
|
|
|
3
3
|
const REFRESH_BUFFER_MS = 15 * 60_000; // Request refresh 15 min before expiry
|
|
4
|
+
const MAX_BACKOFF_MS = 4 * 60_000; // Cap retry backoff at 4 minutes
|
|
5
|
+
const MAX_QUEUED_WEBHOOKS = 200; // Prevent unbounded queue growth
|
|
4
6
|
|
|
5
7
|
export class FirebaseAuth {
|
|
6
8
|
constructor(configDir) {
|
|
@@ -8,6 +10,26 @@ export class FirebaseAuth {
|
|
|
8
10
|
this.token = null;
|
|
9
11
|
this.expiresAt = null;
|
|
10
12
|
this._refreshRequested = false;
|
|
13
|
+
this._refreshAttempts = 0;
|
|
14
|
+
this._refreshTimer = null;
|
|
15
|
+
this._pendingWebhooks = [];
|
|
16
|
+
this._replayCallback = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse the `exp` claim from a Firebase ID token (standard JWT).
|
|
21
|
+
* Returns expiry as milliseconds since epoch, or null on failure.
|
|
22
|
+
* No crypto needed — we only read the unverified payload for timing.
|
|
23
|
+
*/
|
|
24
|
+
static parseJwtExpiry(token) {
|
|
25
|
+
try {
|
|
26
|
+
const parts = token.split('.');
|
|
27
|
+
if (parts.length !== 3) return null;
|
|
28
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
29
|
+
return typeof payload.exp === 'number' ? payload.exp * 1000 : null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
11
33
|
}
|
|
12
34
|
|
|
13
35
|
isAuthenticated() {
|
|
@@ -19,11 +41,24 @@ export class FirebaseAuth {
|
|
|
19
41
|
return this.token;
|
|
20
42
|
}
|
|
21
43
|
|
|
22
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Store a Firebase ID token. Expiry is parsed from the JWT's `exp` claim
|
|
46
|
+
* rather than assuming 1 hour from now — the token may have been minted
|
|
47
|
+
* well before the agent received it.
|
|
48
|
+
*/
|
|
49
|
+
setToken(token) {
|
|
50
|
+
const jwtExpiry = FirebaseAuth.parseJwtExpiry(token);
|
|
23
51
|
this.token = token;
|
|
24
|
-
this.expiresAt =
|
|
52
|
+
this.expiresAt = jwtExpiry || (Date.now() + 3600_000);
|
|
25
53
|
this._refreshRequested = false;
|
|
26
|
-
|
|
54
|
+
this._refreshAttempts = 0;
|
|
55
|
+
if (this._refreshTimer) {
|
|
56
|
+
clearTimeout(this._refreshTimer);
|
|
57
|
+
this._refreshTimer = null;
|
|
58
|
+
}
|
|
59
|
+
const remainMin = Math.round((this.expiresAt - Date.now()) / 60_000);
|
|
60
|
+
debug(`Firebase auth token updated (expires in ${remainMin}m)`);
|
|
61
|
+
this._replayQueue();
|
|
27
62
|
}
|
|
28
63
|
|
|
29
64
|
getAuthHeader() {
|
|
@@ -34,7 +69,8 @@ export class FirebaseAuth {
|
|
|
34
69
|
|
|
35
70
|
/**
|
|
36
71
|
* Returns true if the token is within REFRESH_BUFFER_MS of expiry
|
|
37
|
-
* and a refresh has not already been requested
|
|
72
|
+
* and a refresh has not already been requested (or the retry timer
|
|
73
|
+
* has reset the flag).
|
|
38
74
|
*/
|
|
39
75
|
needsRefresh() {
|
|
40
76
|
if (!this.token || !this.expiresAt) return false;
|
|
@@ -43,12 +79,53 @@ export class FirebaseAuth {
|
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
/**
|
|
46
|
-
* Mark that a refresh has been requested
|
|
47
|
-
*
|
|
82
|
+
* Mark that a refresh has been requested. Starts a backoff timer
|
|
83
|
+
* that resets the flag so needsRefresh() can fire again if the
|
|
84
|
+
* web app doesn't respond.
|
|
48
85
|
*/
|
|
49
86
|
markRefreshRequested() {
|
|
50
87
|
this._refreshRequested = true;
|
|
51
|
-
|
|
88
|
+
this._refreshAttempts++;
|
|
89
|
+
const backoff = Math.min(30_000 * Math.pow(2, this._refreshAttempts - 1), MAX_BACKOFF_MS);
|
|
90
|
+
debug(`Firebase auth refresh requested (attempt ${this._refreshAttempts}, retry in ${backoff / 1000}s)`);
|
|
91
|
+
if (this._refreshTimer) clearTimeout(this._refreshTimer);
|
|
92
|
+
this._refreshTimer = setTimeout(() => {
|
|
93
|
+
this._refreshRequested = false;
|
|
94
|
+
this._refreshTimer = null;
|
|
95
|
+
debug('Firebase auth refresh request expired — will retry on next check');
|
|
96
|
+
}, backoff);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Register a callback that fires when a fresh token arrives,
|
|
101
|
+
* receiving the array of queued webhook payloads to replay.
|
|
102
|
+
*/
|
|
103
|
+
onTokenRefresh(callback) {
|
|
104
|
+
this._replayCallback = callback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Queue a webhook payload for replay when a fresh token arrives.
|
|
109
|
+
* Called by index.js when a webhook can't be sent due to expired auth.
|
|
110
|
+
*/
|
|
111
|
+
queueWebhook(event, data) {
|
|
112
|
+
this._pendingWebhooks.push({ event, data, queuedAt: Date.now() });
|
|
113
|
+
if (this._pendingWebhooks.length > MAX_QUEUED_WEBHOOKS) {
|
|
114
|
+
this._pendingWebhooks.shift(); // drop oldest
|
|
115
|
+
}
|
|
116
|
+
debug(`Queued webhook ${event} for replay (${this._pendingWebhooks.length} pending)`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Drain the queue and replay through the registered callback.
|
|
121
|
+
* Called automatically by setToken() when a fresh token arrives.
|
|
122
|
+
*/
|
|
123
|
+
_replayQueue() {
|
|
124
|
+
if (this._pendingWebhooks.length === 0 || !this._replayCallback) return;
|
|
125
|
+
const queue = [...this._pendingWebhooks];
|
|
126
|
+
this._pendingWebhooks = [];
|
|
127
|
+
info(`Replaying ${queue.length} queued webhook(s) with fresh token`);
|
|
128
|
+
this._replayCallback(queue);
|
|
52
129
|
}
|
|
53
130
|
|
|
54
131
|
// Full OAuth flow will be implemented in integration phase
|
package/src/agent/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/agent/index.js
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { info, warn, debug } from '../logger.js';
|
|
5
6
|
import { getConfigDir, readConfig, writeConfig, ensureConfigDir } from './config.js';
|
|
6
7
|
import { ProjectManager } from './project-manager.js';
|
|
@@ -12,6 +13,13 @@ import { CliBridge } from './cli-bridge.js';
|
|
|
12
13
|
import { AgentGit } from './git-integration.js';
|
|
13
14
|
import { FirebaseAuth } from './firebase-auth.js';
|
|
14
15
|
|
|
16
|
+
const FIREBASE_WEBHOOK_URL = 'https://webhookingest-24h6taciuq-uc.a.run.app';
|
|
17
|
+
|
|
18
|
+
// Read version from package.json so it stays in sync with npm
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
21
|
+
const AGENT_VERSION = pkg.version;
|
|
22
|
+
|
|
15
23
|
export async function startAgent() {
|
|
16
24
|
const configDir = getConfigDir();
|
|
17
25
|
ensureConfigDir(configDir);
|
|
@@ -27,9 +35,44 @@ export async function startAgent() {
|
|
|
27
35
|
const firebaseAuth = new FirebaseAuth(configDir);
|
|
28
36
|
const webhookDispatcher = new WebhookDispatcher({
|
|
29
37
|
machine: config.machine,
|
|
30
|
-
version:
|
|
38
|
+
version: AGENT_VERSION,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Wire up webhook queue replay: when a fresh token arrives,
|
|
42
|
+
// re-dispatch any webhooks that failed due to expired auth.
|
|
43
|
+
firebaseAuth.onTokenRefresh((queue) => {
|
|
44
|
+
for (const { event, data } of queue) {
|
|
45
|
+
webhookDispatcher.dispatch(event, data, [{
|
|
46
|
+
url: FIREBASE_WEBHOOK_URL,
|
|
47
|
+
label: 'nightytidy.com',
|
|
48
|
+
headers: firebaseAuth.getAuthHeader(),
|
|
49
|
+
}]);
|
|
50
|
+
}
|
|
31
51
|
});
|
|
32
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Dispatch a webhook to user endpoints + Firestore.
|
|
55
|
+
* If not authenticated, queues the Firestore payload for replay when a fresh token arrives.
|
|
56
|
+
* User webhooks (Slack/Discord) are always sent immediately.
|
|
57
|
+
*/
|
|
58
|
+
function dispatchWithQueue(event, data, projectWebhooks) {
|
|
59
|
+
const userEndpoints = [...(projectWebhooks || [])];
|
|
60
|
+
if (firebaseAuth.isAuthenticated()) {
|
|
61
|
+
userEndpoints.push({
|
|
62
|
+
url: FIREBASE_WEBHOOK_URL,
|
|
63
|
+
label: 'nightytidy.com',
|
|
64
|
+
headers: firebaseAuth.getAuthHeader(),
|
|
65
|
+
});
|
|
66
|
+
webhookDispatcher.dispatch(event, data, userEndpoints);
|
|
67
|
+
} else {
|
|
68
|
+
if (userEndpoints.length > 0) {
|
|
69
|
+
webhookDispatcher.dispatch(event, data, userEndpoints);
|
|
70
|
+
}
|
|
71
|
+
firebaseAuth.queueWebhook(event, data);
|
|
72
|
+
warn(`Firebase webhook queued (not authenticated) — will replay when token arrives`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
33
76
|
// Track the active CLI bridge so stop-run can kill it
|
|
34
77
|
let activeBridge = null;
|
|
35
78
|
let pauseRequested = false;
|
|
@@ -267,7 +310,7 @@ export async function startAgent() {
|
|
|
267
310
|
|
|
268
311
|
case 'auth-refresh': {
|
|
269
312
|
if (msg.token && typeof msg.token === 'string') {
|
|
270
|
-
firebaseAuth.setToken(msg.token
|
|
313
|
+
firebaseAuth.setToken(msg.token);
|
|
271
314
|
info('Firebase auth token refreshed by web app');
|
|
272
315
|
reply({ type: 'auth-refresh-ack' });
|
|
273
316
|
} else {
|
|
@@ -323,16 +366,10 @@ export async function startAgent() {
|
|
|
323
366
|
}
|
|
324
367
|
runQueue.clearInterrupted();
|
|
325
368
|
// Notify Firestore
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}, [{
|
|
331
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
332
|
-
label: 'nightytidy.com',
|
|
333
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
334
|
-
}]);
|
|
335
|
-
}
|
|
369
|
+
dispatchWithQueue('run_failed', {
|
|
370
|
+
projectId: interrupted.projectId,
|
|
371
|
+
run: { id: interrupted.id },
|
|
372
|
+
}, []);
|
|
336
373
|
reply({ type: 'interrupted-discarded', runId: interrupted.id });
|
|
337
374
|
break;
|
|
338
375
|
}
|
|
@@ -380,10 +417,10 @@ export async function startAgent() {
|
|
|
380
417
|
const wsServer = new AgentWebSocketServer({
|
|
381
418
|
port: config.port,
|
|
382
419
|
token: config.token,
|
|
420
|
+
version: AGENT_VERSION,
|
|
383
421
|
onCommand: handleCommand,
|
|
384
422
|
onAuthCallback: ({ token }) => {
|
|
385
|
-
|
|
386
|
-
firebaseAuth.setToken(token, Date.now() + 3600_000);
|
|
423
|
+
firebaseAuth.setToken(token);
|
|
387
424
|
info('Firebase auth token received from web app');
|
|
388
425
|
},
|
|
389
426
|
});
|
|
@@ -500,15 +537,7 @@ export async function startAgent() {
|
|
|
500
537
|
});
|
|
501
538
|
|
|
502
539
|
// Send run_started webhook so Firestore run doc is created immediately
|
|
503
|
-
|
|
504
|
-
if (firebaseAuth.isAuthenticated()) {
|
|
505
|
-
startEndpoints.push({
|
|
506
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
507
|
-
label: 'nightytidy.com',
|
|
508
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
webhookDispatcher.dispatch('run_started', {
|
|
540
|
+
dispatchWithQueue('run_started', {
|
|
512
541
|
project: project.name,
|
|
513
542
|
projectId: project.id,
|
|
514
543
|
run: {
|
|
@@ -518,7 +547,7 @@ export async function startAgent() {
|
|
|
518
547
|
gitBranch: initResult.parsed?.runBranch || '',
|
|
519
548
|
gitTag: initResult.parsed?.tagName || '',
|
|
520
549
|
},
|
|
521
|
-
},
|
|
550
|
+
}, project.webhooks);
|
|
522
551
|
|
|
523
552
|
startHeartbeat(run.id, project.id);
|
|
524
553
|
|
|
@@ -598,20 +627,12 @@ export async function startAgent() {
|
|
|
598
627
|
});
|
|
599
628
|
|
|
600
629
|
requestTokenRefreshIfNeeded();
|
|
601
|
-
|
|
602
|
-
if (firebaseAuth.isAuthenticated()) {
|
|
603
|
-
endpoints.push({
|
|
604
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
605
|
-
label: 'nightytidy.com',
|
|
606
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
webhookDispatcher.dispatch('step_completed', {
|
|
630
|
+
dispatchWithQueue('step_completed', {
|
|
610
631
|
project: project.name,
|
|
611
632
|
projectId: project.id,
|
|
612
633
|
step: stepData,
|
|
613
634
|
run: { id: run.id, progress: `${stepIndex + 1}/${totalSteps}`, costSoFar: stepData.cost, elapsedMs: stepData.duration },
|
|
614
|
-
},
|
|
635
|
+
}, project.webhooks);
|
|
615
636
|
stepIndex++;
|
|
616
637
|
} else {
|
|
617
638
|
const errorType = stepParsed.errorType;
|
|
@@ -654,20 +675,12 @@ export async function startAgent() {
|
|
|
654
675
|
});
|
|
655
676
|
|
|
656
677
|
requestTokenRefreshIfNeeded();
|
|
657
|
-
|
|
658
|
-
if (firebaseAuth.isAuthenticated()) {
|
|
659
|
-
endpoints.push({
|
|
660
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
661
|
-
label: 'nightytidy.com',
|
|
662
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
webhookDispatcher.dispatch('step_failed', {
|
|
678
|
+
dispatchWithQueue('step_failed', {
|
|
666
679
|
project: project.name,
|
|
667
680
|
projectId: project.id,
|
|
668
681
|
step: { number: stepNum, name: stepParsed.name || `Step ${stepNum}`, status: 'failed', duration: stepParsed.duration || 0, cost: stepParsed.costUSD || 0 },
|
|
669
682
|
run: { id: run.id },
|
|
670
|
-
},
|
|
683
|
+
}, project.webhooks);
|
|
671
684
|
stepIndex++;
|
|
672
685
|
}
|
|
673
686
|
}
|
|
@@ -686,19 +699,11 @@ export async function startAgent() {
|
|
|
686
699
|
wsServer.broadcast({ type: 'run-completed', runId: run.id, results: {} });
|
|
687
700
|
|
|
688
701
|
requestTokenRefreshIfNeeded();
|
|
689
|
-
|
|
690
|
-
if (firebaseAuth.isAuthenticated()) {
|
|
691
|
-
completionEndpoints.push({
|
|
692
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
693
|
-
label: 'nightytidy.com',
|
|
694
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
webhookDispatcher.dispatch('run_completed', {
|
|
702
|
+
dispatchWithQueue('run_completed', {
|
|
698
703
|
project: project.name,
|
|
699
704
|
projectId: project.id,
|
|
700
705
|
run: { id: run.id, totalSteps, completedSteps: run.steps.length, elapsedMs: Date.now() - run.startedAt },
|
|
701
|
-
},
|
|
706
|
+
}, project.webhooks);
|
|
702
707
|
|
|
703
708
|
activeBridge = null;
|
|
704
709
|
runQueue.completeCurrent({ success: true });
|
|
@@ -793,17 +798,11 @@ export async function startAgent() {
|
|
|
793
798
|
|
|
794
799
|
// Notify Firestore the run is active again (use run_resumed, NOT run_started
|
|
795
800
|
// which would reset completedSteps/totalCost counters to 0)
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}, [{
|
|
802
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
803
|
-
label: 'nightytidy.com',
|
|
804
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
805
|
-
}]);
|
|
806
|
-
}
|
|
801
|
+
dispatchWithQueue('run_resumed', {
|
|
802
|
+
project: project.name,
|
|
803
|
+
projectId: project.id,
|
|
804
|
+
run: { id: interrupted.id, startedAt: interrupted.startedAt },
|
|
805
|
+
}, project.webhooks);
|
|
807
806
|
|
|
808
807
|
// Run remaining steps (reuse the same step loop pattern)
|
|
809
808
|
for (const stepNum of remainingSteps) {
|
|
@@ -856,14 +855,10 @@ export async function startAgent() {
|
|
|
856
855
|
wsServer.broadcast({ type: 'step-completed', runId: interrupted.id, step: stepData, cost: stepData.cost });
|
|
857
856
|
|
|
858
857
|
requestTokenRefreshIfNeeded();
|
|
859
|
-
|
|
860
|
-
if (firebaseAuth.isAuthenticated()) {
|
|
861
|
-
endpoints.push({ url: 'https://webhookingest-24h6taciuq-uc.a.run.app', label: 'nightytidy.com', headers: firebaseAuth.getAuthHeader() });
|
|
862
|
-
}
|
|
863
|
-
webhookDispatcher.dispatch('step_completed', {
|
|
858
|
+
dispatchWithQueue('step_completed', {
|
|
864
859
|
project: project.name, projectId: project.id, step: stepData,
|
|
865
860
|
run: { id: interrupted.id, costSoFar: stepData.cost, elapsedMs: stepData.duration },
|
|
866
|
-
},
|
|
861
|
+
}, project.webhooks);
|
|
867
862
|
} else if (stepParsed.errorType === 'rate_limit') {
|
|
868
863
|
const waitMs = stepParsed.retryAfterMs || 120000;
|
|
869
864
|
info(` ⏸ Rate limited — waiting ${Math.round(waitMs / 1000)}s`);
|
|
@@ -902,12 +897,10 @@ export async function startAgent() {
|
|
|
902
897
|
wsServer.broadcast({ type: 'run-completed', runId: interrupted.id, results: {} });
|
|
903
898
|
|
|
904
899
|
requestTokenRefreshIfNeeded();
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}, [{ url: 'https://webhookingest-24h6taciuq-uc.a.run.app', label: 'nightytidy.com', headers: firebaseAuth.getAuthHeader() }]);
|
|
910
|
-
}
|
|
900
|
+
dispatchWithQueue('run_completed', {
|
|
901
|
+
project: project.name, projectId: project.id,
|
|
902
|
+
run: { id: interrupted.id, totalSteps: interrupted.steps.length, completedSteps: runProgress.completedCount, elapsedMs: Date.now() - interrupted.startedAt },
|
|
903
|
+
}, project.webhooks);
|
|
911
904
|
|
|
912
905
|
activeBridge = null;
|
|
913
906
|
runQueue.completeCurrent({ success: true });
|
|
@@ -937,12 +930,10 @@ export async function startAgent() {
|
|
|
937
930
|
wsServer.broadcast({ type: 'run-completed', runId: interrupted.id, status: 'completed', results: {} });
|
|
938
931
|
|
|
939
932
|
requestTokenRefreshIfNeeded();
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}, [{ url: 'https://webhookingest-24h6taciuq-uc.a.run.app', label: 'nightytidy.com', headers: firebaseAuth.getAuthHeader() }]);
|
|
945
|
-
}
|
|
933
|
+
dispatchWithQueue('run_completed', {
|
|
934
|
+
project: project.name, projectId: project.id,
|
|
935
|
+
run: { id: interrupted.id, totalSteps: interrupted.steps.length, completedSteps: interrupted.lastProgress?.completedCount || 0, elapsedMs: Date.now() - interrupted.startedAt },
|
|
936
|
+
}, project.webhooks);
|
|
946
937
|
|
|
947
938
|
activeBridge = null;
|
|
948
939
|
runQueue.completeCurrent({ success: true });
|
|
@@ -960,15 +951,14 @@ export async function startAgent() {
|
|
|
960
951
|
currentProjectId = projectId;
|
|
961
952
|
heartbeatInterval = setInterval(() => {
|
|
962
953
|
if (!firebaseAuth.isAuthenticated()) return;
|
|
963
|
-
const endpoints = [{
|
|
964
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
965
|
-
label: 'nightytidy.com',
|
|
966
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
967
|
-
}];
|
|
968
954
|
webhookDispatcher.dispatch('heartbeat', {
|
|
969
955
|
projectId: currentProjectId,
|
|
970
956
|
run: { id: currentRunId },
|
|
971
|
-
},
|
|
957
|
+
}, [{
|
|
958
|
+
url: FIREBASE_WEBHOOK_URL,
|
|
959
|
+
label: 'nightytidy.com',
|
|
960
|
+
headers: firebaseAuth.getAuthHeader(),
|
|
961
|
+
}]);
|
|
972
962
|
}, 60_000);
|
|
973
963
|
}
|
|
974
964
|
|
|
@@ -990,22 +980,15 @@ export async function startAgent() {
|
|
|
990
980
|
runQueue.markInterrupted(runProgress);
|
|
991
981
|
|
|
992
982
|
// Best-effort: notify Firestore (may not complete before exit)
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
id: current.id,
|
|
1003
|
-
completedSteps: runProgress.completedCount,
|
|
1004
|
-
failedSteps: runProgress.failedCount,
|
|
1005
|
-
totalCost: runProgress.totalCost,
|
|
1006
|
-
},
|
|
1007
|
-
}, endpoints);
|
|
1008
|
-
}
|
|
983
|
+
dispatchWithQueue('run_interrupted', {
|
|
984
|
+
projectId: current.projectId,
|
|
985
|
+
run: {
|
|
986
|
+
id: current.id,
|
|
987
|
+
completedSteps: runProgress.completedCount,
|
|
988
|
+
failedSteps: runProgress.failedCount,
|
|
989
|
+
totalCost: runProgress.totalCost,
|
|
990
|
+
},
|
|
991
|
+
}, []);
|
|
1009
992
|
|
|
1010
993
|
// Notify connected clients
|
|
1011
994
|
wsServer.broadcast({
|
|
@@ -1058,8 +1041,8 @@ export async function startAgent() {
|
|
|
1058
1041
|
|
|
1059
1042
|
// Best-effort: notify Firestore that this run is interrupted
|
|
1060
1043
|
// (in case the shutdown webhook didn't make it)
|
|
1061
|
-
if (
|
|
1062
|
-
|
|
1044
|
+
if (proj) {
|
|
1045
|
+
dispatchWithQueue('run_interrupted', {
|
|
1063
1046
|
projectId: proj.id,
|
|
1064
1047
|
run: {
|
|
1065
1048
|
id: interrupted.id,
|
|
@@ -1067,11 +1050,7 @@ export async function startAgent() {
|
|
|
1067
1050
|
failedSteps: progress.failedCount || 0,
|
|
1068
1051
|
totalCost: progress.totalCost || 0,
|
|
1069
1052
|
},
|
|
1070
|
-
}, [
|
|
1071
|
-
url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
|
|
1072
|
-
label: 'nightytidy.com',
|
|
1073
|
-
headers: firebaseAuth.getAuthHeader(),
|
|
1074
|
-
}]);
|
|
1053
|
+
}, []);
|
|
1075
1054
|
}
|
|
1076
1055
|
}
|
|
1077
1056
|
|
|
@@ -1084,7 +1063,7 @@ export async function startAgent() {
|
|
|
1084
1063
|
}
|
|
1085
1064
|
|
|
1086
1065
|
// Print startup info
|
|
1087
|
-
console.log(`\nNightyTidy Agent
|
|
1066
|
+
console.log(`\nNightyTidy Agent v${AGENT_VERSION}`);
|
|
1088
1067
|
console.log(`WebSocket: ws://127.0.0.1:${actualPort}`);
|
|
1089
1068
|
console.log(`Token: ${config.token.slice(0, 6)}...(see ~/.nightytidy/config.json)`);
|
|
1090
1069
|
if (interrupted) {
|
|
@@ -6,8 +6,9 @@ import { info, debug, warn } from '../logger.js';
|
|
|
6
6
|
const RATE_LIMIT_PER_SEC = 10;
|
|
7
7
|
|
|
8
8
|
export class AgentWebSocketServer {
|
|
9
|
-
constructor({ port, token, onCommand, onAuthCallback }) {
|
|
9
|
+
constructor({ port, token, onCommand, onAuthCallback, version }) {
|
|
10
10
|
this.port = port;
|
|
11
|
+
this.version = version || '0.0.0';
|
|
11
12
|
this.token = token;
|
|
12
13
|
this.onCommand = onCommand || (() => {});
|
|
13
14
|
this.onAuthCallback = onAuthCallback || (() => {});
|
|
@@ -130,7 +131,7 @@ export class AgentWebSocketServer {
|
|
|
130
131
|
ws.send(JSON.stringify({
|
|
131
132
|
type: 'connected',
|
|
132
133
|
machine: process.env.COMPUTERNAME || os.hostname(),
|
|
133
|
-
version:
|
|
134
|
+
version: this.version,
|
|
134
135
|
startedAt: this.startedAt,
|
|
135
136
|
}));
|
|
136
137
|
debug('Client authenticated');
|