@gowelle/stint-agent 1.2.16 → 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-7TIF7QZL.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-I6DTKIFX.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-7TIF7QZL.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 {
@@ -100,7 +127,7 @@ var CommitQueueProcessor = class {
100
127
  pushed = true;
101
128
  logger.success("queue", `Pushed commit ${sha} to remote`);
102
129
  notify({
103
- title: "Commit Executed & Pushed",
130
+ title: `Commit Pushed - ${project.name}`,
104
131
  message: `Commit "${commit.message}" successfully pushed.`
105
132
  });
106
133
  } catch (error) {
@@ -109,14 +136,14 @@ var CommitQueueProcessor = class {
109
136
  if (isConflict) {
110
137
  logger.warn("queue", `Push failed due to remote conflict: ${pushError}`);
111
138
  notify({
112
- title: "\u26A0\uFE0F Push Failed - Manual Action Required",
139
+ title: `Push Conflict - ${project.name}`,
113
140
  message: `Commit "${commit.message}" created but push failed.
114
- Run "git pull --rebase && git push" to resolve.`
141
+ Run "git pull --rebase" to resolve.`
115
142
  });
116
143
  } else {
117
144
  logger.error("queue", `Push failed: ${pushError}`);
118
145
  notify({
119
- title: "\u274C Push Failed",
146
+ title: `Push Failed - ${project.name}`,
120
147
  message: `Commit created but push failed: ${pushError}`
121
148
  });
122
149
  }
@@ -126,16 +153,16 @@ Run "git pull --rebase && git push" to resolve.`
126
153
  await this.reportSuccess(commit.id, sha, pushed, pushError);
127
154
  if (!pushed && !pushError) {
128
155
  notify({
129
- title: "Commit Executed",
156
+ title: `Commit Created - ${project.name}`,
130
157
  message: `Commit "${commit.message}" created locally.`
131
158
  });
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
- title: "Commit Failed",
165
+ title: `Commit Failed - ${project.name}`,
139
166
  message: `Failed to execute commit "${commit.message}": ${msg}`
140
167
  });
141
168
  await this.reportFailure(commit.id, msg);
@@ -174,9 +201,9 @@ Run "git pull --rebase && git push" 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 && git push" 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-IB5F32WJ.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-7TIF7QZL.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.16";
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-Z44NPHXG.js";
6
+ } from "../chunk-SXPP272L.js";
6
7
  import {
7
8
  apiService
8
- } from "../chunk-I6DTKIFX.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-7TIF7QZL.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 {
@@ -286,9 +163,16 @@ var FileWatcher = class {
286
163
  const repoInfo = await gitService.getRepoInfo(projectPath);
287
164
  await apiService.syncProject(projectId, repoInfo);
288
165
  logger.success("watcher", `Synced project ${projectId}`);
166
+ let projectName = projectId;
167
+ try {
168
+ const projects = await apiService.getLinkedProjects();
169
+ const p = projects.find((proj) => proj.id === projectId);
170
+ if (p) projectName = p.name;
171
+ } catch (_e) {
172
+ }
289
173
  notify({
290
174
  title: "Sync Complete",
291
- message: `Project ${projectId} synced successfully.`
175
+ message: `Project "${projectName}" synced successfully.`
292
176
  });
293
177
  } catch (error) {
294
178
  logger.error("watcher", `Failed to sync project ${projectId}`, error);
@@ -384,19 +268,18 @@ Project: ${project.name}`
384
268
  });
385
269
  commitQueue.addToQueue(commit, project);
386
270
  });
387
- pollingService.onCommitApproved((commit, project) => {
388
- logger.info("daemon", `Commit approved (via polling): ${commit.id} for project ${project.name}`);
389
- notify({
390
- title: "Commit Approved",
391
- message: `${commit.message}
392
- Project: ${project.name}`
393
- });
394
- commitQueue.addToQueue(commit, project);
395
- });
396
- websocketService.onCommitPending((commit) => {
271
+ websocketService.onCommitPending(async (data) => {
272
+ const commit = data;
397
273
  logger.info("daemon", `Commit pending: ${commit.id}`);
274
+ let projectName = commit.projectId;
275
+ try {
276
+ const projects = await apiService.getLinkedProjects();
277
+ const p = projects.find((proj) => proj.id === commit.projectId);
278
+ if (p) projectName = p.name;
279
+ } catch (_e) {
280
+ }
398
281
  notify({
399
- title: "New Pending Commit",
282
+ title: `New Proposal - ${projectName}`,
400
283
  message: commit.message
401
284
  });
402
285
  });
@@ -407,13 +290,6 @@ Project: ${project.name}`
407
290
  message: project.name
408
291
  });
409
292
  });
410
- pollingService.onProjectUpdated((project) => {
411
- logger.info("daemon", `Project updated (via polling): ${project.id} - ${project.name}`);
412
- notify({
413
- title: "Project Updated",
414
- message: project.name
415
- });
416
- });
417
293
  websocketService.onDisconnect(() => {
418
294
  logger.warn("daemon", "WebSocket disconnected, will attempt to reconnect");
419
295
  });
@@ -436,9 +312,18 @@ Priority: ${suggestion.priority}`,
436
312
  websocketService.onSyncRequested(async (projectId) => {
437
313
  logger.info("daemon", `Server requested sync for project: ${projectId}`);
438
314
  try {
315
+ let projectName = projectId;
316
+ try {
317
+ const projects = await apiService.getLinkedProjects();
318
+ const project = projects.find((p) => p.id === projectId);
319
+ if (project) {
320
+ projectName = project.name;
321
+ }
322
+ } catch (_e) {
323
+ }
439
324
  notify({
440
325
  title: "Sync Requested",
441
- message: `Syncing project ${projectId}...`
326
+ message: `Syncing project "${projectName}"...`
442
327
  });
443
328
  await fileWatcher.syncProjectById(projectId);
444
329
  } catch (error) {
@@ -453,7 +338,6 @@ Priority: ${suggestion.priority}`,
453
338
  logger.info("daemon", `Linked project: ${project.projectId}`);
454
339
  fileWatcher.watchProject(project.projectId);
455
340
  });
456
- pollingService.start();
457
341
  logger.info("daemon", "Daemon started successfully");
458
342
  const projectEntries = Object.entries(linkedProjects);
459
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-Z44NPHXG.js";
5
+ } from "./chunk-SXPP272L.js";
6
6
  import {
7
7
  apiService
8
- } from "./chunk-I6DTKIFX.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-7TIF7QZL.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-LCGOELR4.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.16";
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.16",
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-I6DTKIFX.js";
4
- import "./chunk-7TIF7QZL.js";
5
- import "./chunk-XHXSWLUC.js";
6
- export {
7
- apiService
8
- };
@@ -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-IB5F32WJ.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,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,7 +0,0 @@
1
- import {
2
- notify
3
- } from "./chunk-DCY3EXDX.js";
4
- import "./chunk-XHXSWLUC.js";
5
- export {
6
- notify
7
- };