@gowelle/stint-agent 1.2.17 → 1.2.18

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/assets/logo.png CHANGED
Binary file
@@ -2,11 +2,10 @@ import {
2
2
  gitService,
3
3
  projectService,
4
4
  validatePidFile
5
- } from "./chunk-QQP6IASS.js";
5
+ } from "./chunk-A2CVSQ3K.js";
6
6
  import {
7
7
  authService
8
- } from "./chunk-ES33YYVA.js";
9
- import "./chunk-XHXSWLUC.js";
8
+ } from "./chunk-WETVBZ6Z.js";
10
9
 
11
10
  // src/components/StatusDashboard.tsx
12
11
  import { useState, useEffect } from "react";
@@ -0,0 +1,7 @@
1
+ import {
2
+ apiService
3
+ } from "./chunk-XPZNWXB4.js";
4
+ import "./chunk-WETVBZ6Z.js";
5
+ export {
6
+ apiService
7
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  config,
3
3
  logger
4
- } from "./chunk-XHXSWLUC.js";
4
+ } from "./chunk-WETVBZ6Z.js";
5
5
 
6
6
  // src/services/git.ts
7
7
  import simpleGit from "simple-git";
@@ -1,20 +1,47 @@
1
1
  import {
2
2
  apiService
3
- } from "./chunk-D6BP2Z5S.js";
3
+ } from "./chunk-XPZNWXB4.js";
4
4
  import {
5
5
  gitService,
6
6
  projectService
7
- } from "./chunk-QQP6IASS.js";
8
- import {
9
- authService
10
- } from "./chunk-ES33YYVA.js";
11
- import {
12
- notify
13
- } from "./chunk-DCY3EXDX.js";
7
+ } from "./chunk-A2CVSQ3K.js";
14
8
  import {
9
+ authService,
15
10
  config,
16
11
  logger
17
- } from "./chunk-XHXSWLUC.js";
12
+ } from "./chunk-WETVBZ6Z.js";
13
+
14
+ // src/utils/notify.ts
15
+ import notifier from "node-notifier";
16
+ import path from "path";
17
+ import { fileURLToPath } from "url";
18
+ var __filename = fileURLToPath(import.meta.url);
19
+ var __dirname = path.dirname(__filename);
20
+ var DEFAULT_ICON = path.resolve(__dirname, "../assets/logo.png");
21
+ function notify(options) {
22
+ if (!config.areNotificationsEnabled()) {
23
+ logger.debug("notify", "Notifications disabled, skipping notification");
24
+ return;
25
+ }
26
+ try {
27
+ notifier.notify({
28
+ title: options.title,
29
+ message: options.message,
30
+ open: options.open,
31
+ icon: options.icon || DEFAULT_ICON,
32
+ sound: true,
33
+ wait: false,
34
+ appID: "Stint Agent"
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ }, (error) => {
37
+ if (error) {
38
+ logger.error("notify", "Failed to send notification", error);
39
+ }
40
+ });
41
+ } catch (error) {
42
+ logger.error("notify", "Failed to send notification", error);
43
+ }
44
+ }
18
45
 
19
46
  // src/daemon/queue.ts
20
47
  var CommitQueueProcessor = class {
@@ -132,7 +159,7 @@ Run "git pull --rebase" to resolve.`
132
159
  }
133
160
  return sha;
134
161
  } catch (error) {
135
- msg = error.message;
162
+ const msg = error.message;
136
163
  logger.error("queue", `Commit execution failed: ${msg}`);
137
164
  notify({
138
165
  title: `Commit Failed - ${project.name}`,
@@ -174,9 +201,9 @@ Run "git pull --rebase" to resolve.`
174
201
  */
175
202
  findProjectPath(projectId) {
176
203
  const allProjects = projectService.getAllLinkedProjects();
177
- for (const [path, linkedProject] of Object.entries(allProjects)) {
204
+ for (const [path3, linkedProject] of Object.entries(allProjects)) {
178
205
  if (linkedProject.projectId === projectId) {
179
- return path;
206
+ return path3;
180
207
  }
181
208
  }
182
209
  return null;
@@ -197,11 +224,33 @@ Run "git pull --rebase" to resolve.`
197
224
  var commitQueue = new CommitQueueProcessor();
198
225
 
199
226
  // src/services/websocket.ts
200
- import WebSocket from "ws";
227
+ import Echo from "laravel-echo";
228
+ import Pusher from "pusher-js";
229
+ import fs from "fs";
230
+ import os from "os";
231
+ import path2 from "path";
232
+ var STATUS_FILE_PATH = path2.join(os.homedir(), ".config", "stint", "daemon.status.json");
233
+ function writeStatus(update) {
234
+ try {
235
+ const dir = path2.dirname(STATUS_FILE_PATH);
236
+ if (!fs.existsSync(dir)) {
237
+ fs.mkdirSync(dir, { recursive: true });
238
+ }
239
+ let status = { websocket: { connected: false } };
240
+ if (fs.existsSync(STATUS_FILE_PATH)) {
241
+ try {
242
+ status = JSON.parse(fs.readFileSync(STATUS_FILE_PATH, "utf8"));
243
+ } catch {
244
+ }
245
+ }
246
+ status.websocket = { ...status.websocket, ...update };
247
+ fs.writeFileSync(STATUS_FILE_PATH, JSON.stringify(status, null, 2));
248
+ } catch {
249
+ }
250
+ }
201
251
  var WebSocketServiceImpl = class {
202
- ws = null;
252
+ echo = null;
203
253
  userId = null;
204
- socketId = null;
205
254
  reconnectAttempts = 0;
206
255
  maxReconnectAttempts = 10;
207
256
  reconnectTimer = null;
@@ -215,7 +264,7 @@ var WebSocketServiceImpl = class {
215
264
  agentDisconnectedHandlers = [];
216
265
  syncRequestedHandlers = [];
217
266
  /**
218
- * Connect to the WebSocket server
267
+ * Connect to the WebSocket server using Laravel Echo
219
268
  * @throws Error if connection fails or no auth token available
220
269
  */
221
270
  async connect() {
@@ -224,31 +273,117 @@ var WebSocketServiceImpl = class {
224
273
  if (!token) {
225
274
  throw new Error("No authentication token available");
226
275
  }
227
- const wsUrl = config.getWsUrl();
228
- const url = `${wsUrl}?token=${encodeURIComponent(token)}`;
229
- logger.info("websocket", `Connecting to ${wsUrl}...`);
230
- this.ws = new WebSocket(url);
276
+ const reverbAppKey = config.getReverbAppKey();
277
+ if (!reverbAppKey) {
278
+ throw new Error("Reverb app key not configured");
279
+ }
280
+ const apiUrl = config.getApiUrl();
281
+ const environment = config.getEnvironment();
282
+ let wsHost;
283
+ let wsPort;
284
+ let forceTLS;
285
+ if (environment === "development") {
286
+ wsHost = "localhost";
287
+ wsPort = 8080;
288
+ forceTLS = false;
289
+ } else {
290
+ wsHost = "stint.codes";
291
+ wsPort = 443;
292
+ forceTLS = true;
293
+ }
294
+ logger.info("websocket", `Connecting to ${wsHost}:${wsPort} with key ${reverbAppKey}...`);
295
+ const pusherClient = new Pusher(reverbAppKey, {
296
+ wsHost,
297
+ wsPort,
298
+ forceTLS,
299
+ enabledTransports: ["ws", "wss"],
300
+ disableStats: true,
301
+ cluster: "",
302
+ // Required but unused for Reverb
303
+ authorizer: (channel) => ({
304
+ authorize: async (socketId, callback) => {
305
+ try {
306
+ const response = await fetch(`${apiUrl}/api/broadcasting/auth`, {
307
+ method: "POST",
308
+ headers: {
309
+ "Authorization": `Bearer ${token}`,
310
+ "Accept": "application/json",
311
+ "Content-Type": "application/json"
312
+ },
313
+ body: JSON.stringify({
314
+ socket_id: socketId,
315
+ channel_name: channel.name
316
+ })
317
+ });
318
+ if (!response.ok) {
319
+ const errorText = await response.text();
320
+ logger.error("websocket", `Auth failed (${response.status}): ${errorText}`);
321
+ callback(new Error(`Auth failed: ${response.status}`));
322
+ return;
323
+ }
324
+ const data = await response.json();
325
+ callback(null, data);
326
+ } catch (error) {
327
+ logger.error("websocket", "Channel auth error", error);
328
+ callback(error);
329
+ }
330
+ }
331
+ })
332
+ });
333
+ this.echo = new Echo({
334
+ broadcaster: "reverb",
335
+ key: reverbAppKey,
336
+ wsHost,
337
+ wsPort,
338
+ forceTLS,
339
+ disableStats: true,
340
+ enabledTransports: ["ws", "wss"],
341
+ authEndpoint: `${apiUrl}/api/broadcasting/auth`,
342
+ auth: {
343
+ headers: {
344
+ Authorization: `Bearer ${token}`,
345
+ Accept: "application/json"
346
+ }
347
+ },
348
+ client: pusherClient
349
+ });
350
+ logger.info("websocket", "Echo instance created, setting up connection handlers...");
231
351
  return new Promise((resolve, reject) => {
232
- if (!this.ws) {
233
- reject(new Error("WebSocket not initialized"));
352
+ if (!this.echo) {
353
+ reject(new Error("Echo not initialized"));
234
354
  return;
235
355
  }
236
- this.ws.on("open", () => {
237
- logger.success("websocket", "WebSocket connected");
356
+ const connectionTimeout = setTimeout(() => {
357
+ const state = this.echo?.connector.pusher.connection.state || "unknown";
358
+ logger.error("websocket", `Connection timeout after 15s (state: ${state})`);
359
+ reject(new Error(`Connection timeout - stuck in state: ${state}`));
360
+ }, 15e3);
361
+ this.echo.connector.pusher.connection.bind("state_change", (states) => {
362
+ logger.info("websocket", `Connection state: ${states.previous} -> ${states.current}`);
363
+ });
364
+ this.echo.connector.pusher.connection.bind("connected", () => {
365
+ clearTimeout(connectionTimeout);
366
+ logger.success("websocket", "\u2705 Connected to Broadcaster via Sanctum");
367
+ writeStatus({ connected: true });
238
368
  this.reconnectAttempts = 0;
239
369
  this.isManualDisconnect = false;
240
370
  resolve();
241
371
  });
242
- this.ws.on("message", (data) => {
243
- this.handleMessage(data);
372
+ this.echo.connector.pusher.connection.bind("error", (error) => {
373
+ clearTimeout(connectionTimeout);
374
+ const errorMessage = error instanceof Error ? error.message : JSON.stringify(error) || "Unknown connection error";
375
+ logger.error("websocket", `WebSocket error: ${errorMessage}`);
376
+ reject(new Error(errorMessage));
244
377
  });
245
- this.ws.on("close", () => {
378
+ this.echo.connector.pusher.connection.bind("disconnected", () => {
246
379
  logger.warn("websocket", "WebSocket disconnected");
380
+ writeStatus({ connected: false });
247
381
  this.handleDisconnect();
248
382
  });
249
- this.ws.on("error", (error) => {
250
- logger.error("websocket", "WebSocket error", error);
251
- reject(error);
383
+ this.echo.connector.pusher.connection.bind("failed", () => {
384
+ clearTimeout(connectionTimeout);
385
+ logger.error("websocket", "WebSocket connection failed");
386
+ reject(new Error("WebSocket connection failed"));
252
387
  });
253
388
  });
254
389
  } catch (error) {
@@ -266,17 +401,12 @@ var WebSocketServiceImpl = class {
266
401
  clearTimeout(this.reconnectTimer);
267
402
  this.reconnectTimer = null;
268
403
  }
269
- if (this.ws) {
404
+ if (this.echo) {
270
405
  if (this.userId) {
271
- this.sendMessage({
272
- event: "pusher:unsubscribe",
273
- data: {
274
- channel: `private-user.${this.userId}`
275
- }
276
- });
406
+ this.echo.leave(`user.${this.userId}`);
277
407
  }
278
- this.ws.close();
279
- this.ws = null;
408
+ this.echo.disconnect();
409
+ this.echo = null;
280
410
  logger.info("websocket", "WebSocket disconnected");
281
411
  }
282
412
  }
@@ -285,38 +415,55 @@ var WebSocketServiceImpl = class {
285
415
  * @returns True if connected and ready
286
416
  */
287
417
  isConnected() {
288
- return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
418
+ return this.echo !== null && this.echo.connector.pusher.connection.state === "connected";
289
419
  }
290
420
  /**
291
- * Subscribe to user-specific channel for real-time updates
421
+ * Subscribe to user-specific private channel for real-time updates
292
422
  * @param userId - User ID to subscribe to
293
423
  */
294
424
  async subscribeToUserChannel(userId) {
295
425
  this.userId = userId;
296
- if (this.userId) {
297
- const channel = `user.${this.userId}`;
298
- logger.info("websocket", `Subscribing to channel: ${channel}`);
299
- this.sendMessage({
300
- event: "pusher:subscribe",
301
- data: {
302
- channel
303
- }
304
- });
426
+ if (!this.echo) {
427
+ logger.warn("websocket", "Cannot subscribe: not connected");
428
+ return;
305
429
  }
306
- }
307
- /**
308
- * Get authentication signature for private channel from Laravel backend
309
- */
310
- async getChannelAuth(channel, socketId) {
311
- const { apiService: apiService2 } = await import("./api-ORVLKYAQ.js");
312
- const response = await apiService2.request("/api/broadcasting/auth", {
313
- method: "POST",
314
- body: JSON.stringify({
315
- socket_id: socketId,
316
- channel_name: channel
317
- })
430
+ if (!this.isConnected()) {
431
+ logger.warn("websocket", "Cannot subscribe: not connected");
432
+ return;
433
+ }
434
+ const channel = `user.${userId}`;
435
+ logger.info("websocket", `Subscribing to private channel: ${channel}`);
436
+ const privateChannel = this.echo.private(channel);
437
+ writeStatus({ channel });
438
+ privateChannel.listen(".commit.approved", (data) => {
439
+ logger.info("websocket", `Commit approved: ${data.pendingCommit.id}`);
440
+ writeStatus({ lastEvent: "commit.approved", lastEventTime: (/* @__PURE__ */ new Date()).toISOString() });
441
+ this.commitApprovedHandlers.forEach(
442
+ (handler) => handler(data.pendingCommit, data.pendingCommit.project)
443
+ );
444
+ }).listen(".commit.pending", (data) => {
445
+ logger.info("websocket", `Commit pending: ${data.pendingCommit.id}`);
446
+ writeStatus({ lastEvent: "commit.pending", lastEventTime: (/* @__PURE__ */ new Date()).toISOString() });
447
+ this.commitPendingHandlers.forEach((handler) => handler(data.pendingCommit));
448
+ }).listen(".suggestion.created", (data) => {
449
+ logger.info("websocket", `Suggestion created: ${data.suggestion.id}`);
450
+ writeStatus({ lastEvent: "suggestion.created", lastEventTime: (/* @__PURE__ */ new Date()).toISOString() });
451
+ this.suggestionCreatedHandlers.forEach((handler) => handler(data.suggestion));
452
+ }).listen(".project.updated", (data) => {
453
+ logger.info("websocket", `Project updated: ${data.project.id}`);
454
+ writeStatus({ lastEvent: "project.updated", lastEventTime: (/* @__PURE__ */ new Date()).toISOString() });
455
+ this.projectUpdatedHandlers.forEach((handler) => handler(data.project));
456
+ }).listen(".sync.requested", (data) => {
457
+ logger.info("websocket", `Sync requested for project: ${data.project.id}`);
458
+ writeStatus({ lastEvent: "sync.requested", lastEventTime: (/* @__PURE__ */ new Date()).toISOString() });
459
+ this.syncRequestedHandlers.forEach((handler) => handler(data.project.id));
460
+ }).listen(".agent.disconnected", (data) => {
461
+ const reason = data.reason ?? "Server requested disconnect";
462
+ logger.warn("websocket", `Agent disconnected by server: ${reason}`);
463
+ writeStatus({ lastEvent: "agent.disconnected", lastEventTime: (/* @__PURE__ */ new Date()).toISOString() });
464
+ this.agentDisconnectedHandlers.forEach((handler) => handler(reason));
318
465
  });
319
- return response.auth;
466
+ logger.success("websocket", `Subscribed to private channel: ${channel}`);
320
467
  }
321
468
  /**
322
469
  * Register handler for commit approved events
@@ -343,98 +490,7 @@ var WebSocketServiceImpl = class {
343
490
  onSyncRequested(handler) {
344
491
  this.syncRequestedHandlers.push(handler);
345
492
  }
346
- sendMessage(message) {
347
- if (!this.isConnected()) {
348
- logger.warn("websocket", "Cannot send message: not connected");
349
- return;
350
- }
351
- this.ws.send(JSON.stringify(message));
352
- }
353
- async handleMessage(data) {
354
- try {
355
- const message = JSON.parse(data.toString());
356
- logger.info("websocket", `Received message: ${message.event}`);
357
- if (message.event === "pusher:connection_established") {
358
- try {
359
- const connectionData = typeof message.data === "string" ? JSON.parse(message.data) : message.data;
360
- this.socketId = connectionData.socket_id;
361
- logger.success("websocket", `Connection established (socket_id: ${this.socketId})`);
362
- if (this.userId) {
363
- this.subscribeToUserChannel(this.userId);
364
- }
365
- } catch (error) {
366
- logger.success("websocket", "Connection established");
367
- }
368
- return;
369
- }
370
- if (message.event === "pusher_internal:subscription_succeeded") {
371
- logger.success("websocket", `Subscribed to channel: ${message.channel}`);
372
- return;
373
- }
374
- if (message.event === "pusher:error") {
375
- try {
376
- const errorData = typeof message.data === "string" ? JSON.parse(message.data) : message.data;
377
- const errorCode = errorData.code;
378
- const errorMessage = errorData.message;
379
- logger.error("websocket", `WebSocket error (${errorCode}): ${errorMessage}`);
380
- if (errorCode === 4001) {
381
- logger.error("websocket", "Application does not exist - check Reverb app key configuration");
382
- } else if (errorCode === 4009) {
383
- logger.error("websocket", "Connection is unauthorized - authentication token may be invalid or expired");
384
- const { notify: notify2 } = await import("./notify-NXUEEO7M.js");
385
- notify2({
386
- title: "Stint Agent - Connection Issue",
387
- message: "WebSocket authentication failed. Notifications may be delayed (falling back to polling)."
388
- });
389
- }
390
- } catch (parseError) {
391
- logger.error("websocket", `WebSocket error: ${JSON.stringify(message.data)}`);
392
- }
393
- return;
394
- }
395
- if (message.event === "commit.approved") {
396
- const { pendingCommit } = message.data;
397
- logger.info("websocket", `Commit approved: ${pendingCommit.id}`);
398
- this.commitApprovedHandlers.forEach((handler) => handler(pendingCommit, pendingCommit.project));
399
- return;
400
- }
401
- if (message.event === "commit.pending") {
402
- const { pendingCommit } = message.data;
403
- logger.info("websocket", `Commit pending: ${pendingCommit.id}`);
404
- this.commitPendingHandlers.forEach((handler) => handler(pendingCommit));
405
- return;
406
- }
407
- if (message.event === "suggestion.created") {
408
- const { suggestion } = message.data;
409
- logger.info("websocket", `Suggestion created: ${suggestion.id}`);
410
- this.suggestionCreatedHandlers.forEach((handler) => handler(suggestion));
411
- return;
412
- }
413
- if (message.event === "project.updated") {
414
- const { project } = message.data;
415
- logger.info("websocket", `Project updated: ${project.id}`);
416
- this.projectUpdatedHandlers.forEach((handler) => handler(project));
417
- return;
418
- }
419
- if (message.event === "sync.requested") {
420
- const { project } = message.data;
421
- logger.info("websocket", `Sync requested for project: ${project.id}`);
422
- this.syncRequestedHandlers.forEach((handler) => handler(project.id));
423
- return;
424
- }
425
- if (message.event === "agent.disconnected") {
426
- const { reason } = message.data;
427
- logger.warn("websocket", `Agent disconnected by server: ${reason ?? "Server requested disconnect"}`);
428
- this.agentDisconnectedHandlers.forEach((handler) => handler(reason ?? "Server requested disconnect"));
429
- return;
430
- }
431
- logger.info("websocket", `Unhandled event: ${message.event}, payload: ${JSON.stringify(message)}`);
432
- } catch (error) {
433
- logger.error("websocket", "Failed to parse message", error);
434
- }
435
- }
436
493
  handleDisconnect() {
437
- this.ws = null;
438
494
  this.disconnectHandlers.forEach((handler) => handler());
439
495
  if (this.isManualDisconnect) {
440
496
  return;
@@ -472,6 +528,7 @@ var WebSocketServiceImpl = class {
472
528
  var websocketService = new WebSocketServiceImpl();
473
529
 
474
530
  export {
531
+ notify,
475
532
  commitQueue,
476
533
  websocketService
477
534
  };
@@ -214,7 +214,95 @@ var Logger = class {
214
214
  };
215
215
  var logger = new Logger();
216
216
 
217
+ // src/utils/crypto.ts
218
+ import crypto from "crypto";
219
+ import os3 from "os";
220
+ function getMachineKey() {
221
+ const machineInfo = `${os3.hostname()}-${os3.platform()}-${os3.arch()}`;
222
+ return crypto.createHash("sha256").update(machineInfo).digest();
223
+ }
224
+ var ALGORITHM = "aes-256-gcm";
225
+ var IV_LENGTH = 16;
226
+ var AUTH_TAG_LENGTH = 16;
227
+ function encrypt(text) {
228
+ const key = getMachineKey();
229
+ const iv = crypto.randomBytes(IV_LENGTH);
230
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
231
+ let encrypted = cipher.update(text, "utf8", "hex");
232
+ encrypted += cipher.final("hex");
233
+ const authTag = cipher.getAuthTag();
234
+ return iv.toString("hex") + authTag.toString("hex") + encrypted;
235
+ }
236
+ function decrypt(encryptedText) {
237
+ const key = getMachineKey();
238
+ const iv = Buffer.from(encryptedText.slice(0, IV_LENGTH * 2), "hex");
239
+ const authTag = Buffer.from(
240
+ encryptedText.slice(IV_LENGTH * 2, (IV_LENGTH + AUTH_TAG_LENGTH) * 2),
241
+ "hex"
242
+ );
243
+ const encrypted = encryptedText.slice((IV_LENGTH + AUTH_TAG_LENGTH) * 2);
244
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
245
+ decipher.setAuthTag(authTag);
246
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
247
+ decrypted += decipher.final("utf8");
248
+ return decrypted;
249
+ }
250
+
251
+ // src/services/auth.ts
252
+ var AuthServiceImpl = class {
253
+ async saveToken(token) {
254
+ try {
255
+ const encryptedToken = encrypt(token);
256
+ config.setToken(encryptedToken);
257
+ logger.info("auth", "Token saved successfully");
258
+ } catch (error) {
259
+ logger.error("auth", "Failed to save token", error);
260
+ throw error;
261
+ }
262
+ }
263
+ async getToken() {
264
+ try {
265
+ const encryptedToken = config.getToken();
266
+ if (!encryptedToken) {
267
+ return null;
268
+ }
269
+ return decrypt(encryptedToken);
270
+ } catch (error) {
271
+ logger.error("auth", "Failed to decrypt token", error);
272
+ return null;
273
+ }
274
+ }
275
+ async clearToken() {
276
+ config.clearToken();
277
+ logger.info("auth", "Token cleared");
278
+ }
279
+ async validateToken() {
280
+ const token = await this.getToken();
281
+ if (!token) {
282
+ return null;
283
+ }
284
+ try {
285
+ const { apiService } = await import("./api-6EGHLTCT.js");
286
+ const user = await apiService.getCurrentUser();
287
+ logger.info("auth", `Token validated for user: ${user.email}`);
288
+ return user;
289
+ } catch (error) {
290
+ logger.warn("auth", "Token validation failed");
291
+ logger.error("auth", "Failed to validate token", error);
292
+ return null;
293
+ }
294
+ }
295
+ getMachineId() {
296
+ return config.getMachineId();
297
+ }
298
+ getMachineName() {
299
+ return config.getMachineName();
300
+ }
301
+ };
302
+ var authService = new AuthServiceImpl();
303
+
217
304
  export {
218
305
  config,
219
- logger
306
+ logger,
307
+ authService
220
308
  };
@@ -1,10 +1,8 @@
1
1
  import {
2
- authService
3
- } from "./chunk-ES33YYVA.js";
4
- import {
2
+ authService,
5
3
  config,
6
4
  logger
7
- } from "./chunk-XHXSWLUC.js";
5
+ } from "./chunk-WETVBZ6Z.js";
8
6
 
9
7
  // src/utils/circuit-breaker.ts
10
8
  var CircuitBreaker = class {
@@ -100,7 +98,7 @@ var CircuitBreaker = class {
100
98
  };
101
99
 
102
100
  // src/services/api.ts
103
- var AGENT_VERSION = "1.2.17";
101
+ var AGENT_VERSION = "1.2.18";
104
102
  var ApiServiceImpl = class {
105
103
  sessionId = null;
106
104
  circuitBreaker = new CircuitBreaker({
@@ -1,149 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  commitQueue,
4
+ notify,
4
5
  websocketService
5
- } from "../chunk-VHCNMUHN.js";
6
+ } from "../chunk-SXPP272L.js";
6
7
  import {
7
8
  apiService
8
- } from "../chunk-D6BP2Z5S.js";
9
+ } from "../chunk-XPZNWXB4.js";
9
10
  import {
10
11
  gitService,
11
12
  projectService,
12
13
  removePidFile,
13
14
  writePidFile
14
- } from "../chunk-QQP6IASS.js";
15
- import {
16
- authService
17
- } from "../chunk-ES33YYVA.js";
18
- import {
19
- notify
20
- } from "../chunk-DCY3EXDX.js";
15
+ } from "../chunk-A2CVSQ3K.js";
21
16
  import {
17
+ authService,
22
18
  logger
23
- } from "../chunk-XHXSWLUC.js";
19
+ } from "../chunk-WETVBZ6Z.js";
24
20
 
25
21
  // src/daemon/runner.ts
26
22
  import "dotenv/config";
27
23
 
28
- // src/services/polling.ts
29
- var PollingServiceImpl = class {
30
- interval = null;
31
- knownCommitIds = /* @__PURE__ */ new Set();
32
- knownProjects = /* @__PURE__ */ new Map();
33
- commitApprovedHandlers = [];
34
- projectUpdatedHandlers = [];
35
- isFirstRun = true;
36
- isPolling = false;
37
- /**
38
- * Start polling for pending commits
39
- * @param intervalMs - Polling interval in milliseconds (default: 10000)
40
- */
41
- start(intervalMs = 1e4) {
42
- if (this.interval) {
43
- return;
44
- }
45
- logger.info("polling", `Starting polling service (interval: ${intervalMs}ms)`);
46
- this.poll();
47
- this.interval = setInterval(() => {
48
- this.poll();
49
- }, intervalMs);
50
- }
51
- /**
52
- * Stop polling
53
- */
54
- stop() {
55
- if (this.interval) {
56
- clearInterval(this.interval);
57
- this.interval = null;
58
- logger.info("polling", "Stopping polling service");
59
- }
60
- }
61
- /**
62
- * Register handler for commit approved event
63
- * @param handler - Function to call when a commit is approved where it was previously unknown
64
- */
65
- onCommitApproved(handler) {
66
- this.commitApprovedHandlers.push(handler);
67
- }
68
- /**
69
- * Register handler for project updated event
70
- * @param handler - Function to call when a project is updated
71
- */
72
- onProjectUpdated(handler) {
73
- this.projectUpdatedHandlers.push(handler);
74
- }
75
- /**
76
- * Poll for updates
77
- */
78
- async poll() {
79
- if (this.isPolling) {
80
- return;
81
- }
82
- this.isPolling = true;
83
- try {
84
- const linkedProjects = projectService.getAllLinkedProjects();
85
- const projectIds = Object.values(linkedProjects).map((p) => p.projectId);
86
- if (projectIds.length === 0) {
87
- this.isFirstRun = false;
88
- this.isPolling = false;
89
- return;
90
- }
91
- const apiProjects = await apiService.getLinkedProjects();
92
- const projects = apiProjects.filter((p) => projectIds.includes(p.id));
93
- for (const project of projects) {
94
- const cachedProject = this.knownProjects.get(project.id);
95
- if (cachedProject) {
96
- if (cachedProject.updatedAt !== project.updatedAt) {
97
- if (!this.isFirstRun) {
98
- logger.info("polling", `Project update detected: ${project.id}`);
99
- this.notifyProjectUpdated(project);
100
- }
101
- }
102
- }
103
- this.knownProjects.set(project.id, project);
104
- try {
105
- const commits = await apiService.getPendingCommits(project.id);
106
- for (const commit of commits) {
107
- if (!this.knownCommitIds.has(commit.id)) {
108
- this.knownCommitIds.add(commit.id);
109
- if (!this.isFirstRun) {
110
- logger.info("polling", `New pending commit detected: ${commit.id}`);
111
- this.notifyCommitApproved(commit, project);
112
- }
113
- }
114
- }
115
- } catch (error) {
116
- logger.debug("polling", `Failed to poll project ${project.id}: ${error.message}`);
117
- }
118
- }
119
- this.isFirstRun = false;
120
- } catch (error) {
121
- logger.error("polling", "Poll cycle failed", error);
122
- } finally {
123
- this.isPolling = false;
124
- }
125
- }
126
- notifyCommitApproved(commit, project) {
127
- this.commitApprovedHandlers.forEach((handler) => {
128
- try {
129
- handler(commit, project);
130
- } catch (error) {
131
- logger.error("polling", "Error in commit approved handler", error);
132
- }
133
- });
134
- }
135
- notifyProjectUpdated(project) {
136
- this.projectUpdatedHandlers.forEach((handler) => {
137
- try {
138
- handler(project);
139
- } catch (error) {
140
- logger.error("polling", "Error in project updated handler", error);
141
- }
142
- });
143
- }
144
- };
145
- var pollingService = new PollingServiceImpl();
146
-
147
24
  // src/daemon/watcher.ts
148
25
  import fs from "fs";
149
26
  var FileWatcher = class {
@@ -291,7 +168,7 @@ var FileWatcher = class {
291
168
  const projects = await apiService.getLinkedProjects();
292
169
  const p = projects.find((proj) => proj.id === projectId);
293
170
  if (p) projectName = p.name;
294
- } catch (e) {
171
+ } catch (_e) {
295
172
  }
296
173
  notify({
297
174
  title: "Sync Complete",
@@ -387,15 +264,6 @@ async function startDaemon() {
387
264
  notify({
388
265
  title: "Commit Approved",
389
266
  message: `${commit.message}
390
- Project: ${project.name}`
391
- });
392
- commitQueue.addToQueue(commit, project);
393
- });
394
- pollingService.onCommitApproved((commit, project) => {
395
- logger.info("daemon", `Commit approved (via polling): ${commit.id} for project ${project.name}`);
396
- notify({
397
- title: "Commit Approved",
398
- message: `${commit.message}
399
267
  Project: ${project.name}`
400
268
  });
401
269
  commitQueue.addToQueue(commit, project);
@@ -408,7 +276,7 @@ Project: ${project.name}`
408
276
  const projects = await apiService.getLinkedProjects();
409
277
  const p = projects.find((proj) => proj.id === commit.projectId);
410
278
  if (p) projectName = p.name;
411
- } catch (e) {
279
+ } catch (_e) {
412
280
  }
413
281
  notify({
414
282
  title: `New Proposal - ${projectName}`,
@@ -422,13 +290,6 @@ Project: ${project.name}`
422
290
  message: project.name
423
291
  });
424
292
  });
425
- pollingService.onProjectUpdated((project) => {
426
- logger.info("daemon", `Project updated (via polling): ${project.id} - ${project.name}`);
427
- notify({
428
- title: "Project Updated",
429
- message: project.name
430
- });
431
- });
432
293
  websocketService.onDisconnect(() => {
433
294
  logger.warn("daemon", "WebSocket disconnected, will attempt to reconnect");
434
295
  });
@@ -458,7 +319,7 @@ Priority: ${suggestion.priority}`,
458
319
  if (project) {
459
320
  projectName = project.name;
460
321
  }
461
- } catch (e) {
322
+ } catch (_e) {
462
323
  }
463
324
  notify({
464
325
  title: "Sync Requested",
@@ -477,7 +338,6 @@ Priority: ${suggestion.priority}`,
477
338
  logger.info("daemon", `Linked project: ${project.projectId}`);
478
339
  fileWatcher.watchProject(project.projectId);
479
340
  });
480
- pollingService.start();
481
341
  logger.info("daemon", "Daemon started successfully");
482
342
  const projectEntries = Object.entries(linkedProjects);
483
343
  if (projectEntries.length > 0) {
package/dist/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  commitQueue,
4
4
  websocketService
5
- } from "./chunk-VHCNMUHN.js";
5
+ } from "./chunk-SXPP272L.js";
6
6
  import {
7
7
  apiService
8
- } from "./chunk-D6BP2Z5S.js";
8
+ } from "./chunk-XPZNWXB4.js";
9
9
  import {
10
10
  getPidFilePath,
11
11
  gitService,
@@ -14,15 +14,12 @@ import {
14
14
  projectService,
15
15
  spawnDetached,
16
16
  validatePidFile
17
- } from "./chunk-QQP6IASS.js";
18
- import {
19
- authService
20
- } from "./chunk-ES33YYVA.js";
21
- import "./chunk-DCY3EXDX.js";
17
+ } from "./chunk-A2CVSQ3K.js";
22
18
  import {
19
+ authService,
23
20
  config,
24
21
  logger
25
- } from "./chunk-XHXSWLUC.js";
22
+ } from "./chunk-WETVBZ6Z.js";
26
23
 
27
24
  // src/index.ts
28
25
  import "dotenv/config";
@@ -623,7 +620,7 @@ function registerStatusCommand(program2) {
623
620
  try {
624
621
  const { render } = await import("ink");
625
622
  const { createElement } = await import("react");
626
- const { StatusDashboard } = await import("./StatusDashboard-ATB5BFZR.js");
623
+ const { StatusDashboard } = await import("./StatusDashboard-KCHNLVD7.js");
627
624
  render(createElement(StatusDashboard, { cwd }));
628
625
  return;
629
626
  } catch (error) {
@@ -784,9 +781,8 @@ function getProcessStats(pid) {
784
781
  } else if (platform === "win32") {
785
782
  return getWindowsStats(pid);
786
783
  }
787
- throw new Error(`Unsupported platform: ${platform}`);
788
- } catch (error) {
789
- logger.error("monitor", `Failed to get process stats for PID ${pid}`, error);
784
+ return null;
785
+ } catch {
790
786
  return null;
791
787
  }
792
788
  }
@@ -1025,6 +1021,30 @@ function registerDaemonCommands(program2) {
1025
1021
  console.log(`${chalk8.bold("Threads:")} ${stats.threads}`);
1026
1022
  console.log(`${chalk8.bold("Uptime:")} ${formatUptime(stats.uptime)}`);
1027
1023
  }
1024
+ const statusPath = path3.join(os3.homedir(), ".config", "stint", "daemon.status.json");
1025
+ if (fs.existsSync(statusPath)) {
1026
+ try {
1027
+ const statusData = JSON.parse(fs.readFileSync(statusPath, "utf8"));
1028
+ console.log(chalk8.blue("\n\u{1F4E1} WebSocket Status:"));
1029
+ console.log(chalk8.gray("\u2500".repeat(50)));
1030
+ if (statusData.websocket?.connected) {
1031
+ console.log(`${chalk8.bold("Connected:")} ${chalk8.green("\u2713 Yes")}`);
1032
+ if (statusData.websocket.channel) {
1033
+ console.log(`${chalk8.bold("Channel:")} ${statusData.websocket.channel}`);
1034
+ }
1035
+ } else {
1036
+ console.log(`${chalk8.bold("Connected:")} ${chalk8.yellow("\u2717 No")}`);
1037
+ }
1038
+ if (statusData.websocket?.lastEvent) {
1039
+ console.log(`${chalk8.bold("Last Event:")} ${statusData.websocket.lastEvent}`);
1040
+ if (statusData.websocket.lastEventTime) {
1041
+ const ago = Math.floor((Date.now() - new Date(statusData.websocket.lastEventTime).getTime()) / 1e3);
1042
+ console.log(`${chalk8.bold("Event Time:")} ${formatUptime(ago)} ago`);
1043
+ }
1044
+ }
1045
+ } catch {
1046
+ }
1047
+ }
1028
1048
  } else {
1029
1049
  console.log(`${chalk8.bold("Status:")} ${chalk8.yellow("Not running")}`);
1030
1050
  console.log(chalk8.gray('Run "stint daemon start" to start the daemon.'));
@@ -2003,7 +2023,7 @@ function registerDoctorCommand(program2) {
2003
2023
  }
2004
2024
 
2005
2025
  // src/index.ts
2006
- var AGENT_VERSION = "1.2.17";
2026
+ var AGENT_VERSION = "1.2.18";
2007
2027
  var program = new Command();
2008
2028
  program.name("stint").description("Stint Agent - Local daemon for Stint Project Assistant").version(AGENT_VERSION, "-v, --version", "output the current version").addHelpText("after", `
2009
2029
  ${chalk13.bold("Examples:")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gowelle/stint-agent",
3
- "version": "1.2.17",
3
+ "version": "1.2.18",
4
4
  "description": "Local agent for Stint - Project Assistant",
5
5
  "author": "Gowelle John <gowelle.john@icloud.com>",
6
6
  "license": "MIT",
@@ -44,10 +44,12 @@
44
44
  "conf": "^12.0.0",
45
45
  "dotenv": "^17.2.3",
46
46
  "ink": "^5.2.1",
47
+ "laravel-echo": "^2.2.6",
47
48
  "node-fetch": "^3.3.2",
48
49
  "node-notifier": "^10.0.1",
49
50
  "open": "^10.0.0",
50
51
  "ora": "^8.0.1",
52
+ "pusher-js": "^8.4.0",
51
53
  "react": "^18.3.1",
52
54
  "simple-git": "^3.22.0",
53
55
  "ws": "^8.16.0"
@@ -1,8 +0,0 @@
1
- import {
2
- apiService
3
- } from "./chunk-D6BP2Z5S.js";
4
- import "./chunk-ES33YYVA.js";
5
- import "./chunk-XHXSWLUC.js";
6
- export {
7
- apiService
8
- };
@@ -1,40 +0,0 @@
1
- import {
2
- config,
3
- logger
4
- } from "./chunk-XHXSWLUC.js";
5
-
6
- // src/utils/notify.ts
7
- import notifier from "node-notifier";
8
- import path from "path";
9
- import { fileURLToPath } from "url";
10
- var __filename = fileURLToPath(import.meta.url);
11
- var __dirname = path.dirname(__filename);
12
- var DEFAULT_ICON = path.resolve(__dirname, "../assets/logo.png");
13
- function notify(options) {
14
- if (!config.areNotificationsEnabled()) {
15
- logger.debug("notify", "Notifications disabled, skipping notification");
16
- return;
17
- }
18
- try {
19
- notifier.notify({
20
- title: options.title,
21
- message: options.message,
22
- open: options.open,
23
- icon: options.icon || DEFAULT_ICON,
24
- sound: true,
25
- wait: false,
26
- appID: "Stint Agent"
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- }, (error) => {
29
- if (error) {
30
- logger.error("notify", "Failed to send notification", error);
31
- }
32
- });
33
- } catch (error) {
34
- logger.error("notify", "Failed to send notification", error);
35
- }
36
- }
37
-
38
- export {
39
- notify
40
- };
@@ -1,95 +0,0 @@
1
- import {
2
- config,
3
- logger
4
- } from "./chunk-XHXSWLUC.js";
5
-
6
- // src/utils/crypto.ts
7
- import crypto from "crypto";
8
- import os from "os";
9
- function getMachineKey() {
10
- const machineInfo = `${os.hostname()}-${os.platform()}-${os.arch()}`;
11
- return crypto.createHash("sha256").update(machineInfo).digest();
12
- }
13
- var ALGORITHM = "aes-256-gcm";
14
- var IV_LENGTH = 16;
15
- var AUTH_TAG_LENGTH = 16;
16
- function encrypt(text) {
17
- const key = getMachineKey();
18
- const iv = crypto.randomBytes(IV_LENGTH);
19
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
20
- let encrypted = cipher.update(text, "utf8", "hex");
21
- encrypted += cipher.final("hex");
22
- const authTag = cipher.getAuthTag();
23
- return iv.toString("hex") + authTag.toString("hex") + encrypted;
24
- }
25
- function decrypt(encryptedText) {
26
- const key = getMachineKey();
27
- const iv = Buffer.from(encryptedText.slice(0, IV_LENGTH * 2), "hex");
28
- const authTag = Buffer.from(
29
- encryptedText.slice(IV_LENGTH * 2, (IV_LENGTH + AUTH_TAG_LENGTH) * 2),
30
- "hex"
31
- );
32
- const encrypted = encryptedText.slice((IV_LENGTH + AUTH_TAG_LENGTH) * 2);
33
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
34
- decipher.setAuthTag(authTag);
35
- let decrypted = decipher.update(encrypted, "hex", "utf8");
36
- decrypted += decipher.final("utf8");
37
- return decrypted;
38
- }
39
-
40
- // src/services/auth.ts
41
- var AuthServiceImpl = class {
42
- async saveToken(token) {
43
- try {
44
- const encryptedToken = encrypt(token);
45
- config.setToken(encryptedToken);
46
- logger.info("auth", "Token saved successfully");
47
- } catch (error) {
48
- logger.error("auth", "Failed to save token", error);
49
- throw error;
50
- }
51
- }
52
- async getToken() {
53
- try {
54
- const encryptedToken = config.getToken();
55
- if (!encryptedToken) {
56
- return null;
57
- }
58
- return decrypt(encryptedToken);
59
- } catch (error) {
60
- logger.error("auth", "Failed to decrypt token", error);
61
- return null;
62
- }
63
- }
64
- async clearToken() {
65
- config.clearToken();
66
- logger.info("auth", "Token cleared");
67
- }
68
- async validateToken() {
69
- const token = await this.getToken();
70
- if (!token) {
71
- return null;
72
- }
73
- try {
74
- const { apiService } = await import("./api-ORVLKYAQ.js");
75
- const user = await apiService.getCurrentUser();
76
- logger.info("auth", `Token validated for user: ${user.email}`);
77
- return user;
78
- } catch (error) {
79
- logger.warn("auth", "Token validation failed");
80
- logger.error("auth", "Failed to validate token", error);
81
- return null;
82
- }
83
- }
84
- getMachineId() {
85
- return config.getMachineId();
86
- }
87
- getMachineName() {
88
- return config.getMachineName();
89
- }
90
- };
91
- var authService = new AuthServiceImpl();
92
-
93
- export {
94
- authService
95
- };
@@ -1,7 +0,0 @@
1
- import {
2
- notify
3
- } from "./chunk-DCY3EXDX.js";
4
- import "./chunk-XHXSWLUC.js";
5
- export {
6
- notify
7
- };