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 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.3",
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
  });
@@ -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
- 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', {
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
- }, startEndpoints);
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
- 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', {
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
- }, endpoints);
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
- 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', {
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
- }, endpoints);
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
- 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', {
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
- }, completionEndpoints);
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
- 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
- }
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
- 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', {
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
- }, endpoints);
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
- 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
- }
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
- 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
- }
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
- }, endpoints);
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
- 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
- }
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 (firebaseAuth.isAuthenticated() && proj) {
1062
- webhookDispatcher.dispatch('run_interrupted', {
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 v1.0.0`);
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: '1.0.0',
134
+ version: this.version,
134
135
  startedAt: this.startedAt,
135
136
  }));
136
137
  debug('Client authenticated');