nightytidy 0.2.2 → 0.2.4

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 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
- npx nightytidy gui
44
+ npm run gui
45
45
  ```
46
46
 
47
47
  From there:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nightytidy",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Automated overnight codebase improvement through Claude Code",
5
5
  "license": "MIT",
6
6
  "author": "Dorian Spitz",
@@ -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
- setToken(token, expiresAt) {
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 = expiresAt;
52
+ this.expiresAt = jwtExpiry || (Date.now() + 3600_000);
25
53
  this._refreshRequested = false;
26
- debug('Firebase auth token updated');
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 so we don't spam requests.
47
- * Cleared when setToken() is called with a new token.
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
- debug('Firebase auth token refresh requested');
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
@@ -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: '1.0.0',
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, Date.now() + 3600_000);
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
- if (firebaseAuth.isAuthenticated()) {
327
- webhookDispatcher.dispatch('run_failed', {
328
- projectId: interrupted.projectId,
329
- run: { id: interrupted.id },
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
- // Firebase ID tokens expire after 1 hour
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
  });
@@ -446,6 +483,10 @@ export async function startAgent() {
446
483
 
447
484
  const project = projectManager.getProject(run.projectId);
448
485
  if (!project) {
486
+ dispatchWithQueue('run_failed', {
487
+ projectId: run.projectId,
488
+ run: { id: run.id },
489
+ }, []);
449
490
  runQueue.completeCurrent({ success: false });
450
491
  processQueue();
451
492
  return;
@@ -465,6 +506,11 @@ export async function startAgent() {
465
506
  if (!initResult.success) {
466
507
  info(` ✗ Init failed: ${initResult.stderr}`);
467
508
  wsServer.broadcast({ type: 'run-failed', runId: run.id, error: initResult.stderr });
509
+ dispatchWithQueue('run_failed', {
510
+ project: project.name,
511
+ projectId: project.id,
512
+ run: { id: run.id },
513
+ }, project.webhooks);
468
514
  runQueue.completeCurrent({ success: false });
469
515
  processQueue();
470
516
  return;
@@ -500,15 +546,7 @@ export async function startAgent() {
500
546
  });
501
547
 
502
548
  // Send run_started webhook so Firestore run doc is created immediately
503
- const startEndpoints = [...(project.webhooks || [])];
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', {
549
+ dispatchWithQueue('run_started', {
512
550
  project: project.name,
513
551
  projectId: project.id,
514
552
  run: {
@@ -518,7 +556,7 @@ export async function startAgent() {
518
556
  gitBranch: initResult.parsed?.runBranch || '',
519
557
  gitTag: initResult.parsed?.tagName || '',
520
558
  },
521
- }, startEndpoints);
559
+ }, project.webhooks);
522
560
 
523
561
  startHeartbeat(run.id, project.id);
524
562
 
@@ -598,20 +636,12 @@ export async function startAgent() {
598
636
  });
599
637
 
600
638
  requestTokenRefreshIfNeeded();
601
- const endpoints = [...(project.webhooks || [])];
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', {
639
+ dispatchWithQueue('step_completed', {
610
640
  project: project.name,
611
641
  projectId: project.id,
612
642
  step: stepData,
613
643
  run: { id: run.id, progress: `${stepIndex + 1}/${totalSteps}`, costSoFar: stepData.cost, elapsedMs: stepData.duration },
614
- }, endpoints);
644
+ }, project.webhooks);
615
645
  stepIndex++;
616
646
  } else {
617
647
  const errorType = stepParsed.errorType;
@@ -654,20 +684,12 @@ export async function startAgent() {
654
684
  });
655
685
 
656
686
  requestTokenRefreshIfNeeded();
657
- const endpoints = [...(project.webhooks || [])];
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', {
687
+ dispatchWithQueue('step_failed', {
666
688
  project: project.name,
667
689
  projectId: project.id,
668
690
  step: { number: stepNum, name: stepParsed.name || `Step ${stepNum}`, status: 'failed', duration: stepParsed.duration || 0, cost: stepParsed.costUSD || 0 },
669
691
  run: { id: run.id },
670
- }, endpoints);
692
+ }, project.webhooks);
671
693
  stepIndex++;
672
694
  }
673
695
  }
@@ -686,19 +708,11 @@ export async function startAgent() {
686
708
  wsServer.broadcast({ type: 'run-completed', runId: run.id, results: {} });
687
709
 
688
710
  requestTokenRefreshIfNeeded();
689
- const completionEndpoints = [...(project.webhooks || [])];
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', {
711
+ dispatchWithQueue('run_completed', {
698
712
  project: project.name,
699
713
  projectId: project.id,
700
714
  run: { id: run.id, totalSteps, completedSteps: run.steps.length, elapsedMs: Date.now() - run.startedAt },
701
- }, completionEndpoints);
715
+ }, project.webhooks);
702
716
 
703
717
  activeBridge = null;
704
718
  runQueue.completeCurrent({ success: true });
@@ -714,8 +728,14 @@ export async function startAgent() {
714
728
  activeBridge.kill();
715
729
  activeBridge = null;
716
730
  }
731
+ const project = projectManager.getProject(current.projectId);
717
732
  runQueue.completeCurrent({ success: false });
718
733
  wsServer.broadcast({ type: 'run-failed', runId: msg.runId, error: 'Stopped by user' });
734
+ dispatchWithQueue('run_failed', {
735
+ project: project?.name,
736
+ projectId: current.projectId,
737
+ run: { id: msg.runId },
738
+ }, project?.webhooks || []);
719
739
  reply({ type: 'run-failed', runId: msg.runId, error: 'Stopped by user' });
720
740
  processQueue();
721
741
  } else {
@@ -793,17 +813,11 @@ export async function startAgent() {
793
813
 
794
814
  // Notify Firestore the run is active again (use run_resumed, NOT run_started
795
815
  // which would reset completedSteps/totalCost counters to 0)
796
- if (firebaseAuth.isAuthenticated()) {
797
- webhookDispatcher.dispatch('run_resumed', {
798
- project: project.name,
799
- projectId: project.id,
800
- run: { id: interrupted.id, startedAt: interrupted.startedAt },
801
- }, [{
802
- url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
803
- label: 'nightytidy.com',
804
- headers: firebaseAuth.getAuthHeader(),
805
- }]);
806
- }
816
+ dispatchWithQueue('run_resumed', {
817
+ project: project.name,
818
+ projectId: project.id,
819
+ run: { id: interrupted.id, startedAt: interrupted.startedAt },
820
+ }, project.webhooks);
807
821
 
808
822
  // Run remaining steps (reuse the same step loop pattern)
809
823
  for (const stepNum of remainingSteps) {
@@ -856,14 +870,10 @@ export async function startAgent() {
856
870
  wsServer.broadcast({ type: 'step-completed', runId: interrupted.id, step: stepData, cost: stepData.cost });
857
871
 
858
872
  requestTokenRefreshIfNeeded();
859
- const endpoints = [...(project.webhooks || [])];
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', {
873
+ dispatchWithQueue('step_completed', {
864
874
  project: project.name, projectId: project.id, step: stepData,
865
875
  run: { id: interrupted.id, costSoFar: stepData.cost, elapsedMs: stepData.duration },
866
- }, endpoints);
876
+ }, project.webhooks);
867
877
  } else if (stepParsed.errorType === 'rate_limit') {
868
878
  const waitMs = stepParsed.retryAfterMs || 120000;
869
879
  info(` ⏸ Rate limited — waiting ${Math.round(waitMs / 1000)}s`);
@@ -902,12 +912,10 @@ export async function startAgent() {
902
912
  wsServer.broadcast({ type: 'run-completed', runId: interrupted.id, results: {} });
903
913
 
904
914
  requestTokenRefreshIfNeeded();
905
- if (firebaseAuth.isAuthenticated()) {
906
- webhookDispatcher.dispatch('run_completed', {
907
- project: project.name, projectId: project.id,
908
- run: { id: interrupted.id, totalSteps: interrupted.steps.length, completedSteps: runProgress.completedCount, elapsedMs: Date.now() - interrupted.startedAt },
909
- }, [{ url: 'https://webhookingest-24h6taciuq-uc.a.run.app', label: 'nightytidy.com', headers: firebaseAuth.getAuthHeader() }]);
910
- }
915
+ dispatchWithQueue('run_completed', {
916
+ project: project.name, projectId: project.id,
917
+ run: { id: interrupted.id, totalSteps: interrupted.steps.length, completedSteps: runProgress.completedCount, elapsedMs: Date.now() - interrupted.startedAt },
918
+ }, project.webhooks);
911
919
 
912
920
  activeBridge = null;
913
921
  runQueue.completeCurrent({ success: true });
@@ -937,12 +945,10 @@ export async function startAgent() {
937
945
  wsServer.broadcast({ type: 'run-completed', runId: interrupted.id, status: 'completed', results: {} });
938
946
 
939
947
  requestTokenRefreshIfNeeded();
940
- if (firebaseAuth.isAuthenticated()) {
941
- webhookDispatcher.dispatch('run_completed', {
942
- project: project.name, projectId: project.id,
943
- run: { id: interrupted.id, totalSteps: interrupted.steps.length, completedSteps: interrupted.lastProgress?.completedCount || 0, elapsedMs: Date.now() - interrupted.startedAt },
944
- }, [{ url: 'https://webhookingest-24h6taciuq-uc.a.run.app', label: 'nightytidy.com', headers: firebaseAuth.getAuthHeader() }]);
945
- }
948
+ dispatchWithQueue('run_completed', {
949
+ project: project.name, projectId: project.id,
950
+ run: { id: interrupted.id, totalSteps: interrupted.steps.length, completedSteps: interrupted.lastProgress?.completedCount || 0, elapsedMs: Date.now() - interrupted.startedAt },
951
+ }, project.webhooks);
946
952
 
947
953
  activeBridge = null;
948
954
  runQueue.completeCurrent({ success: true });
@@ -960,15 +966,14 @@ export async function startAgent() {
960
966
  currentProjectId = projectId;
961
967
  heartbeatInterval = setInterval(() => {
962
968
  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
969
  webhookDispatcher.dispatch('heartbeat', {
969
970
  projectId: currentProjectId,
970
971
  run: { id: currentRunId },
971
- }, endpoints);
972
+ }, [{
973
+ url: FIREBASE_WEBHOOK_URL,
974
+ label: 'nightytidy.com',
975
+ headers: firebaseAuth.getAuthHeader(),
976
+ }]);
972
977
  }, 60_000);
973
978
  }
974
979
 
@@ -990,22 +995,15 @@ export async function startAgent() {
990
995
  runQueue.markInterrupted(runProgress);
991
996
 
992
997
  // Best-effort: notify Firestore (may not complete before exit)
993
- if (firebaseAuth.isAuthenticated()) {
994
- const endpoints = [{
995
- url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
996
- label: 'nightytidy.com',
997
- headers: firebaseAuth.getAuthHeader(),
998
- }];
999
- webhookDispatcher.dispatch('run_interrupted', {
1000
- projectId: current.projectId,
1001
- run: {
1002
- id: current.id,
1003
- completedSteps: runProgress.completedCount,
1004
- failedSteps: runProgress.failedCount,
1005
- totalCost: runProgress.totalCost,
1006
- },
1007
- }, endpoints);
1008
- }
998
+ dispatchWithQueue('run_interrupted', {
999
+ projectId: current.projectId,
1000
+ run: {
1001
+ id: current.id,
1002
+ completedSteps: runProgress.completedCount,
1003
+ failedSteps: runProgress.failedCount,
1004
+ totalCost: runProgress.totalCost,
1005
+ },
1006
+ }, []);
1009
1007
 
1010
1008
  // Notify connected clients
1011
1009
  wsServer.broadcast({
@@ -1058,8 +1056,8 @@ export async function startAgent() {
1058
1056
 
1059
1057
  // Best-effort: notify Firestore that this run is interrupted
1060
1058
  // (in case the shutdown webhook didn't make it)
1061
- if (firebaseAuth.isAuthenticated() && proj) {
1062
- webhookDispatcher.dispatch('run_interrupted', {
1059
+ if (proj) {
1060
+ dispatchWithQueue('run_interrupted', {
1063
1061
  projectId: proj.id,
1064
1062
  run: {
1065
1063
  id: interrupted.id,
@@ -1067,11 +1065,7 @@ export async function startAgent() {
1067
1065
  failedSteps: progress.failedCount || 0,
1068
1066
  totalCost: progress.totalCost || 0,
1069
1067
  },
1070
- }, [{
1071
- url: 'https://webhookingest-24h6taciuq-uc.a.run.app',
1072
- label: 'nightytidy.com',
1073
- headers: firebaseAuth.getAuthHeader(),
1074
- }]);
1068
+ }, []);
1075
1069
  }
1076
1070
  }
1077
1071
 
@@ -1084,7 +1078,7 @@ export async function startAgent() {
1084
1078
  }
1085
1079
 
1086
1080
  // Print startup info
1087
- console.log(`\nNightyTidy Agent v1.0.0`);
1081
+ console.log(`\nNightyTidy Agent v${AGENT_VERSION}`);
1088
1082
  console.log(`WebSocket: ws://127.0.0.1:${actualPort}`);
1089
1083
  console.log(`Token: ${config.token.slice(0, 6)}...(see ~/.nightytidy/config.json)`);
1090
1084
  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: '1.0.0',
134
+ version: this.version,
134
135
  startedAt: this.startedAt,
135
136
  }));
136
137
  debug('Client authenticated');