polydev-ai 1.4.5 → 1.5.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/README.md +2 -0
- package/lib/cliManager.js +138 -1
- package/lib/statusReporter.js +516 -0
- package/mcp/manifest.json +92 -0
- package/mcp/server.js +184 -0
- package/package.json +3 -3
- package/lib/cliManager.ts +0 -755
- package/lib/smartCliCache.ts +0 -189
- package/lib/universalMemoryExtractor.js +0 -607
- package/lib/zeroKnowledgeEncryption.js +0 -289
- package/mcp/cliManager.js +0 -715
- package/mcp/package.json +0 -47
package/README.md
CHANGED
package/lib/cliManager.js
CHANGED
|
@@ -5,13 +5,34 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const which = require('which');
|
|
7
7
|
|
|
8
|
+
// Status reporter for sending CLI status to polydev.ai server
|
|
9
|
+
let StatusReporter;
|
|
10
|
+
try {
|
|
11
|
+
StatusReporter = require('./statusReporter').StatusReporter;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
// StatusReporter not available, continue without it
|
|
14
|
+
StatusReporter = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
const execAsync = promisify(exec);
|
|
9
18
|
|
|
10
19
|
class CLIManager {
|
|
11
|
-
constructor() {
|
|
20
|
+
constructor(options = {}) {
|
|
12
21
|
this.providers = new Map();
|
|
13
22
|
this.statusCache = new Map();
|
|
14
23
|
this.CACHE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
24
|
+
|
|
25
|
+
// Initialize status reporter if available
|
|
26
|
+
this.statusReporter = null;
|
|
27
|
+
if (StatusReporter && options.enableStatusReporting !== false) {
|
|
28
|
+
this.statusReporter = new StatusReporter({
|
|
29
|
+
userToken: options.userToken || process.env.POLYDEV_USER_TOKEN,
|
|
30
|
+
reportingEnabled: options.reportingEnabled !== false,
|
|
31
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
32
|
+
debug: options.debug || process.env.POLYDEV_CLI_DEBUG === 'true'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
this.initializeProviders();
|
|
16
37
|
}
|
|
17
38
|
|
|
@@ -96,6 +117,13 @@ class CLIManager {
|
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
// Report status to polydev.ai server (non-blocking)
|
|
121
|
+
if (this.statusReporter && this.statusReporter.isConfigured()) {
|
|
122
|
+
this.statusReporter.reportAllStatuses(results).catch(err => {
|
|
123
|
+
console.warn(`[Polydev CLI] Status reporting failed:`, err.message);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
99
127
|
return results;
|
|
100
128
|
}
|
|
101
129
|
|
|
@@ -710,6 +738,115 @@ This is a known issue with @google/gemini-cli@0.3.4 and older Node.js versions.`
|
|
|
710
738
|
getProvider(providerId) {
|
|
711
739
|
return this.providers.get(providerId);
|
|
712
740
|
}
|
|
741
|
+
|
|
742
|
+
// ============================================
|
|
743
|
+
// Status Reporting Methods
|
|
744
|
+
// ============================================
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Enable status reporting to polydev.ai server
|
|
748
|
+
* @param {string} userToken - User's MCP token (pd_xxx or polydev_xxx)
|
|
749
|
+
* @param {object} options - Additional configuration options
|
|
750
|
+
*/
|
|
751
|
+
enableStatusReporting(userToken, options = {}) {
|
|
752
|
+
if (!StatusReporter) {
|
|
753
|
+
console.warn('[Polydev CLI] StatusReporter not available');
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (!this.statusReporter) {
|
|
758
|
+
this.statusReporter = new StatusReporter({
|
|
759
|
+
userToken,
|
|
760
|
+
reportingEnabled: true,
|
|
761
|
+
...options
|
|
762
|
+
});
|
|
763
|
+
} else {
|
|
764
|
+
this.statusReporter.configure({
|
|
765
|
+
userToken,
|
|
766
|
+
reportingEnabled: true,
|
|
767
|
+
...options
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
console.log('[Polydev CLI] Status reporting enabled');
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Disable status reporting
|
|
777
|
+
*/
|
|
778
|
+
disableStatusReporting() {
|
|
779
|
+
if (this.statusReporter) {
|
|
780
|
+
this.statusReporter.configure({ reportingEnabled: false });
|
|
781
|
+
this.statusReporter.stopHeartbeat();
|
|
782
|
+
console.log('[Polydev CLI] Status reporting disabled');
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Start automatic heartbeat for periodic status updates
|
|
788
|
+
* @param {number} intervalMs - Interval in milliseconds (default: 15 minutes)
|
|
789
|
+
*/
|
|
790
|
+
startStatusHeartbeat(intervalMs) {
|
|
791
|
+
if (this.statusReporter) {
|
|
792
|
+
if (intervalMs) {
|
|
793
|
+
this.statusReporter.configure({ heartbeatIntervalMs: intervalMs });
|
|
794
|
+
}
|
|
795
|
+
this.statusReporter.startHeartbeat(this);
|
|
796
|
+
console.log('[Polydev CLI] Status heartbeat started');
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Stop automatic heartbeat
|
|
804
|
+
*/
|
|
805
|
+
stopStatusHeartbeat() {
|
|
806
|
+
if (this.statusReporter) {
|
|
807
|
+
this.statusReporter.stopHeartbeat();
|
|
808
|
+
console.log('[Polydev CLI] Status heartbeat stopped');
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Get status reporting configuration and history
|
|
814
|
+
*/
|
|
815
|
+
getStatusReportingInfo() {
|
|
816
|
+
if (!this.statusReporter) {
|
|
817
|
+
return {
|
|
818
|
+
available: false,
|
|
819
|
+
reason: 'StatusReporter not initialized'
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return {
|
|
824
|
+
available: true,
|
|
825
|
+
config: this.statusReporter.getConfig(),
|
|
826
|
+
history: this.statusReporter.getHistory()
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Manually trigger a status report
|
|
832
|
+
* @returns {Promise<object>} - Report results
|
|
833
|
+
*/
|
|
834
|
+
async reportStatusNow() {
|
|
835
|
+
if (!this.statusReporter || !this.statusReporter.isConfigured()) {
|
|
836
|
+
return {
|
|
837
|
+
success: false,
|
|
838
|
+
reason: 'Status reporting not configured'
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const statuses = await this.forceCliDetection();
|
|
843
|
+
// forceCliDetection already reports, but we return the result
|
|
844
|
+
return {
|
|
845
|
+
success: true,
|
|
846
|
+
statuses,
|
|
847
|
+
reported: true
|
|
848
|
+
};
|
|
849
|
+
}
|
|
713
850
|
}
|
|
714
851
|
|
|
715
852
|
module.exports = { CLIManager, default: CLIManager };
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusReporter - Reports CLI status to polydev.ai server
|
|
3
|
+
*
|
|
4
|
+
* This module enables the NPM package to report CLI tool status back to the
|
|
5
|
+
* polydev.ai server, allowing users to see their CLI status on the dashboard.
|
|
6
|
+
*
|
|
7
|
+
* Key Features:
|
|
8
|
+
* 1. Automatic reporting on CLI detection
|
|
9
|
+
* 2. Heartbeat system for periodic updates
|
|
10
|
+
* 3. Event-triggered updates (auth change, install/uninstall)
|
|
11
|
+
* 4. Retry logic with exponential backoff
|
|
12
|
+
* 5. Offline queue for failed reports
|
|
13
|
+
* 6. Deduplication to avoid redundant calls
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
class StatusReporter {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
// Server configuration
|
|
23
|
+
this.serverUrl = options.serverUrl || process.env.POLYDEV_STATUS_SERVER || 'https://www.polydev.ai/api/mcp';
|
|
24
|
+
this.userToken = options.userToken || process.env.POLYDEV_USER_TOKEN;
|
|
25
|
+
|
|
26
|
+
// Feature flags
|
|
27
|
+
this.reportingEnabled = options.reportingEnabled !== false && process.env.POLYDEV_REPORTING_ENABLED !== 'false';
|
|
28
|
+
|
|
29
|
+
// Timing configuration
|
|
30
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ||
|
|
31
|
+
parseInt(process.env.POLYDEV_HEARTBEAT_INTERVAL || '900000', 10); // Default: 15 minutes
|
|
32
|
+
|
|
33
|
+
// Retry configuration
|
|
34
|
+
this.maxRetries = options.maxRetries || 3;
|
|
35
|
+
this.retryDelayMs = options.retryDelayMs || 1000;
|
|
36
|
+
this.requestTimeoutMs = options.requestTimeoutMs || 10000; // 10 seconds
|
|
37
|
+
|
|
38
|
+
// State management
|
|
39
|
+
this.pendingReports = [];
|
|
40
|
+
this.isOnline = true;
|
|
41
|
+
this.lastReportedStatus = new Map();
|
|
42
|
+
this.heartbeatTimer = null;
|
|
43
|
+
this.reportHistory = [];
|
|
44
|
+
this.maxHistorySize = 100;
|
|
45
|
+
|
|
46
|
+
// Debug mode
|
|
47
|
+
this.debug = options.debug || process.env.POLYDEV_CLI_DEBUG === 'true';
|
|
48
|
+
|
|
49
|
+
// Persistence for offline queue
|
|
50
|
+
this.queueFilePath = path.join(os.tmpdir(), '.polydev-pending-reports.json');
|
|
51
|
+
this.loadPendingReports();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Log debug messages
|
|
56
|
+
*/
|
|
57
|
+
log(message, ...args) {
|
|
58
|
+
if (this.debug) {
|
|
59
|
+
console.log(`[StatusReporter] ${message}`, ...args);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Log errors (always shown)
|
|
65
|
+
*/
|
|
66
|
+
error(message, ...args) {
|
|
67
|
+
console.error(`[StatusReporter] ${message}`, ...args);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if reporting is properly configured
|
|
72
|
+
*/
|
|
73
|
+
isConfigured() {
|
|
74
|
+
return this.reportingEnabled && !!this.userToken;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Report status for a single provider
|
|
79
|
+
* @param {string} provider - Provider ID (claude_code, codex_cli, gemini_cli)
|
|
80
|
+
* @param {object} status - Status object with available, authenticated, version, error fields
|
|
81
|
+
* @returns {Promise<object>} - Report result
|
|
82
|
+
*/
|
|
83
|
+
async reportStatus(provider, status) {
|
|
84
|
+
if (!this.isConfigured()) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
reason: this.reportingEnabled ? 'No user token configured' : 'Reporting disabled',
|
|
88
|
+
skipped: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Validate provider
|
|
93
|
+
if (!['claude_code', 'codex_cli', 'gemini_cli'].includes(provider)) {
|
|
94
|
+
return { success: false, reason: `Invalid provider: ${provider}` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Deduplicate - don't report if status unchanged within last 5 minutes
|
|
98
|
+
const statusKey = this.generateStatusKey(provider, status);
|
|
99
|
+
const lastReported = this.lastReportedStatus.get(provider);
|
|
100
|
+
|
|
101
|
+
if (lastReported && lastReported.key === statusKey) {
|
|
102
|
+
const timeSinceLastReport = Date.now() - lastReported.timestamp;
|
|
103
|
+
if (timeSinceLastReport < 5 * 60 * 1000) { // 5 minutes
|
|
104
|
+
this.log(`Skipping duplicate report for ${provider} (unchanged for ${Math.round(timeSinceLastReport/1000)}s)`);
|
|
105
|
+
return { success: true, reason: 'Status unchanged, skipped', skipped: true };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Build payload
|
|
110
|
+
const payload = {
|
|
111
|
+
provider,
|
|
112
|
+
status: this.mapStatusToServerFormat(status),
|
|
113
|
+
authenticated: status.authenticated || false,
|
|
114
|
+
version: status.version || null,
|
|
115
|
+
message: this.buildStatusMessage(status)
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.log(`Reporting status for ${provider}:`, payload);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await this.sendReport(payload);
|
|
122
|
+
|
|
123
|
+
if (result.success) {
|
|
124
|
+
// Update last reported status
|
|
125
|
+
this.lastReportedStatus.set(provider, {
|
|
126
|
+
key: statusKey,
|
|
127
|
+
timestamp: Date.now()
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Record in history
|
|
131
|
+
this.addToHistory({
|
|
132
|
+
provider,
|
|
133
|
+
status: payload.status,
|
|
134
|
+
success: true,
|
|
135
|
+
timestamp: new Date().toISOString()
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.error(`Failed to report status for ${provider}:`, error.message);
|
|
143
|
+
|
|
144
|
+
// Queue for retry
|
|
145
|
+
this.queueReport({ payload, timestamp: Date.now(), retries: 0 });
|
|
146
|
+
|
|
147
|
+
// Record failure in history
|
|
148
|
+
this.addToHistory({
|
|
149
|
+
provider,
|
|
150
|
+
status: payload.status,
|
|
151
|
+
success: false,
|
|
152
|
+
error: error.message,
|
|
153
|
+
timestamp: new Date().toISOString()
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return { success: false, error: error.message, queued: true };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Report all CLI statuses at once
|
|
162
|
+
* @param {object} statuses - Map of provider ID to status object
|
|
163
|
+
* @returns {Promise<object>} - Results for each provider
|
|
164
|
+
*/
|
|
165
|
+
async reportAllStatuses(statuses) {
|
|
166
|
+
if (!this.isConfigured()) {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
reason: 'Not configured',
|
|
170
|
+
results: {}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const results = {};
|
|
175
|
+
const promises = [];
|
|
176
|
+
|
|
177
|
+
for (const [provider, status] of Object.entries(statuses)) {
|
|
178
|
+
promises.push(
|
|
179
|
+
this.reportStatus(provider, status)
|
|
180
|
+
.then(result => { results[provider] = result; })
|
|
181
|
+
.catch(error => { results[provider] = { success: false, error: error.message }; })
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await Promise.all(promises);
|
|
186
|
+
|
|
187
|
+
const successCount = Object.values(results).filter(r => r.success).length;
|
|
188
|
+
const totalCount = Object.keys(results).length;
|
|
189
|
+
|
|
190
|
+
this.log(`Reported ${successCount}/${totalCount} statuses successfully`);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
success: successCount > 0,
|
|
194
|
+
successCount,
|
|
195
|
+
totalCount,
|
|
196
|
+
results
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Send report to server with retry logic
|
|
202
|
+
*/
|
|
203
|
+
async sendReport(payload, retryCount = 0) {
|
|
204
|
+
const body = JSON.stringify({
|
|
205
|
+
jsonrpc: '2.0',
|
|
206
|
+
method: 'tools/call',
|
|
207
|
+
params: {
|
|
208
|
+
name: 'report_cli_status',
|
|
209
|
+
arguments: payload
|
|
210
|
+
},
|
|
211
|
+
id: `status-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.log(`Sending report to ${this.serverUrl} (attempt ${retryCount + 1}/${this.maxRetries + 1})`);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// Use native fetch with timeout
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const timeoutId = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
220
|
+
|
|
221
|
+
const response = await fetch(this.serverUrl, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: {
|
|
224
|
+
'Content-Type': 'application/json',
|
|
225
|
+
'Authorization': `Bearer ${this.userToken}`,
|
|
226
|
+
'User-Agent': 'polydev-ai-npm/1.4.0'
|
|
227
|
+
},
|
|
228
|
+
body,
|
|
229
|
+
signal: controller.signal
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
clearTimeout(timeoutId);
|
|
233
|
+
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
236
|
+
throw new Error(`HTTP ${response.status}: ${errorText.substring(0, 200)}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = await response.json();
|
|
240
|
+
|
|
241
|
+
if (result.error) {
|
|
242
|
+
throw new Error(result.error.message || 'Server returned error');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.isOnline = true;
|
|
246
|
+
return { success: true, response: result };
|
|
247
|
+
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Handle abort (timeout)
|
|
250
|
+
if (error.name === 'AbortError') {
|
|
251
|
+
throw new Error(`Request timed out after ${this.requestTimeoutMs}ms`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check if we should retry
|
|
255
|
+
if (retryCount < this.maxRetries) {
|
|
256
|
+
const delay = this.retryDelayMs * Math.pow(2, retryCount); // Exponential backoff
|
|
257
|
+
this.log(`Retrying in ${delay}ms...`);
|
|
258
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
259
|
+
return this.sendReport(payload, retryCount + 1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Mark as offline if we've exhausted retries
|
|
263
|
+
this.isOnline = false;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Generate a unique key for status deduplication
|
|
270
|
+
*/
|
|
271
|
+
generateStatusKey(provider, status) {
|
|
272
|
+
return `${provider}:${status.available}:${status.authenticated}:${status.version || 'none'}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Map local status object to server-expected format
|
|
277
|
+
*/
|
|
278
|
+
mapStatusToServerFormat(status) {
|
|
279
|
+
if (!status.available) return 'not_installed';
|
|
280
|
+
if (status.error?.includes('Compatibility Issue')) return 'error';
|
|
281
|
+
if (!status.authenticated) return 'unavailable';
|
|
282
|
+
return 'available';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Build a human-readable status message
|
|
287
|
+
*/
|
|
288
|
+
buildStatusMessage(status) {
|
|
289
|
+
const parts = [];
|
|
290
|
+
|
|
291
|
+
if (status.error) {
|
|
292
|
+
parts.push(status.error);
|
|
293
|
+
} else if (status.available && status.authenticated) {
|
|
294
|
+
parts.push('CLI is installed and authenticated');
|
|
295
|
+
} else if (status.available && !status.authenticated) {
|
|
296
|
+
parts.push('CLI is installed but not authenticated');
|
|
297
|
+
} else {
|
|
298
|
+
parts.push('CLI is not installed');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
parts.push(`(reported by polydev-ai NPM package)`);
|
|
302
|
+
|
|
303
|
+
return parts.join(' ');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Start the heartbeat system for periodic status updates
|
|
308
|
+
* @param {CLIManager} cliManager - Reference to CLIManager instance
|
|
309
|
+
*/
|
|
310
|
+
startHeartbeat(cliManager) {
|
|
311
|
+
if (this.heartbeatTimer) {
|
|
312
|
+
this.log('Heartbeat already running');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!this.isConfigured()) {
|
|
317
|
+
this.log('Heartbeat not started - not configured');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.log(`Starting heartbeat (interval: ${this.heartbeatIntervalMs}ms)`);
|
|
322
|
+
|
|
323
|
+
// Initial report
|
|
324
|
+
this.runHeartbeat(cliManager);
|
|
325
|
+
|
|
326
|
+
// Set up periodic heartbeat
|
|
327
|
+
this.heartbeatTimer = setInterval(() => {
|
|
328
|
+
this.runHeartbeat(cliManager);
|
|
329
|
+
}, this.heartbeatIntervalMs);
|
|
330
|
+
|
|
331
|
+
// Make sure interval doesn't prevent process exit
|
|
332
|
+
if (this.heartbeatTimer.unref) {
|
|
333
|
+
this.heartbeatTimer.unref();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Run a single heartbeat iteration
|
|
339
|
+
*/
|
|
340
|
+
async runHeartbeat(cliManager) {
|
|
341
|
+
try {
|
|
342
|
+
this.log('Running heartbeat...');
|
|
343
|
+
|
|
344
|
+
// Force fresh detection
|
|
345
|
+
const statuses = await cliManager.forceCliDetection();
|
|
346
|
+
|
|
347
|
+
// Report all statuses
|
|
348
|
+
await this.reportAllStatuses(statuses);
|
|
349
|
+
|
|
350
|
+
// Process any queued reports
|
|
351
|
+
await this.processQueue();
|
|
352
|
+
|
|
353
|
+
this.log('Heartbeat complete');
|
|
354
|
+
} catch (error) {
|
|
355
|
+
this.error('Heartbeat failed:', error.message);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Stop the heartbeat system
|
|
361
|
+
*/
|
|
362
|
+
stopHeartbeat() {
|
|
363
|
+
if (this.heartbeatTimer) {
|
|
364
|
+
this.log('Stopping heartbeat');
|
|
365
|
+
clearInterval(this.heartbeatTimer);
|
|
366
|
+
this.heartbeatTimer = null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Queue a report for later retry
|
|
372
|
+
*/
|
|
373
|
+
queueReport(report) {
|
|
374
|
+
this.pendingReports.push(report);
|
|
375
|
+
this.savePendingReports();
|
|
376
|
+
this.log(`Queued report for ${report.payload.provider} (queue size: ${this.pendingReports.length})`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Process queued reports (for offline handling)
|
|
381
|
+
*/
|
|
382
|
+
async processQueue() {
|
|
383
|
+
if (this.pendingReports.length === 0) return;
|
|
384
|
+
|
|
385
|
+
this.log(`Processing ${this.pendingReports.length} queued reports...`);
|
|
386
|
+
|
|
387
|
+
const queue = [...this.pendingReports];
|
|
388
|
+
this.pendingReports = [];
|
|
389
|
+
|
|
390
|
+
for (const item of queue) {
|
|
391
|
+
// Skip if too many retries
|
|
392
|
+
if (item.retries >= this.maxRetries) {
|
|
393
|
+
this.log(`Dropping report for ${item.payload.provider} after ${item.retries} retries`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Skip if report is too old (> 24 hours)
|
|
398
|
+
if (Date.now() - item.timestamp > 24 * 60 * 60 * 1000) {
|
|
399
|
+
this.log(`Dropping stale report for ${item.payload.provider}`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
await this.sendReport(item.payload);
|
|
405
|
+
this.log(`Successfully sent queued report for ${item.payload.provider}`);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
item.retries++;
|
|
408
|
+
this.pendingReports.push(item);
|
|
409
|
+
this.log(`Re-queued report for ${item.payload.provider} (retry ${item.retries})`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.savePendingReports();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Save pending reports to disk for persistence across sessions
|
|
418
|
+
*/
|
|
419
|
+
savePendingReports() {
|
|
420
|
+
try {
|
|
421
|
+
fs.writeFileSync(this.queueFilePath, JSON.stringify(this.pendingReports, null, 2));
|
|
422
|
+
} catch (error) {
|
|
423
|
+
// Silently fail - not critical
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Load pending reports from disk
|
|
429
|
+
*/
|
|
430
|
+
loadPendingReports() {
|
|
431
|
+
try {
|
|
432
|
+
if (fs.existsSync(this.queueFilePath)) {
|
|
433
|
+
const data = fs.readFileSync(this.queueFilePath, 'utf8');
|
|
434
|
+
this.pendingReports = JSON.parse(data);
|
|
435
|
+
this.log(`Loaded ${this.pendingReports.length} pending reports from disk`);
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
this.pendingReports = [];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Add entry to history
|
|
444
|
+
*/
|
|
445
|
+
addToHistory(entry) {
|
|
446
|
+
this.reportHistory.push(entry);
|
|
447
|
+
if (this.reportHistory.length > this.maxHistorySize) {
|
|
448
|
+
this.reportHistory.shift();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get report history
|
|
454
|
+
*/
|
|
455
|
+
getHistory() {
|
|
456
|
+
return [...this.reportHistory];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get current configuration
|
|
461
|
+
*/
|
|
462
|
+
getConfig() {
|
|
463
|
+
return {
|
|
464
|
+
serverUrl: this.serverUrl,
|
|
465
|
+
reportingEnabled: this.reportingEnabled,
|
|
466
|
+
heartbeatIntervalMs: this.heartbeatIntervalMs,
|
|
467
|
+
maxRetries: this.maxRetries,
|
|
468
|
+
isConfigured: this.isConfigured(),
|
|
469
|
+
isOnline: this.isOnline,
|
|
470
|
+
pendingReportsCount: this.pendingReports.length,
|
|
471
|
+
heartbeatRunning: !!this.heartbeatTimer
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Update configuration
|
|
477
|
+
*/
|
|
478
|
+
configure(options) {
|
|
479
|
+
if (options.userToken !== undefined) {
|
|
480
|
+
this.userToken = options.userToken;
|
|
481
|
+
}
|
|
482
|
+
if (options.reportingEnabled !== undefined) {
|
|
483
|
+
this.reportingEnabled = options.reportingEnabled;
|
|
484
|
+
}
|
|
485
|
+
if (options.heartbeatIntervalMs !== undefined) {
|
|
486
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs;
|
|
487
|
+
}
|
|
488
|
+
if (options.debug !== undefined) {
|
|
489
|
+
this.debug = options.debug;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
this.log('Configuration updated:', this.getConfig());
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Report status change event (for immediate updates)
|
|
497
|
+
*/
|
|
498
|
+
async reportStatusChange(provider, previousStatus, newStatus) {
|
|
499
|
+
// Skip if status hasn't actually changed
|
|
500
|
+
if (this.generateStatusKey(provider, previousStatus) === this.generateStatusKey(provider, newStatus)) {
|
|
501
|
+
return { success: true, reason: 'No actual change', skipped: true };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this.log(`Status change detected for ${provider}:`, {
|
|
505
|
+
from: this.mapStatusToServerFormat(previousStatus),
|
|
506
|
+
to: this.mapStatusToServerFormat(newStatus)
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Force report (bypass deduplication)
|
|
510
|
+
this.lastReportedStatus.delete(provider);
|
|
511
|
+
|
|
512
|
+
return this.reportStatus(provider, newStatus);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
module.exports = { StatusReporter, default: StatusReporter };
|