@gowelle/stint-agent 1.0.8 → 1.1.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
CHANGED
|
@@ -113,32 +113,30 @@ stint status
|
|
|
113
113
|
|
|
114
114
|
## Troubleshooting
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
For comprehensive troubleshooting help, see the **[Troubleshooting Guide](docs/TROUBLESHOOTING.md)**.
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
### "Repository has uncommitted changes"
|
|
121
|
-
|
|
122
|
-
The agent requires a clean repository to execute commits:
|
|
118
|
+
### Quick Tips
|
|
123
119
|
|
|
120
|
+
**"Not authenticated" error**
|
|
124
121
|
```bash
|
|
125
|
-
|
|
126
|
-
# or
|
|
127
|
-
git add . && git commit -m "message"
|
|
122
|
+
stint login
|
|
128
123
|
```
|
|
129
124
|
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
**Daemon won't start**
|
|
132
126
|
```bash
|
|
133
127
|
stint daemon status # Check if already running
|
|
134
128
|
stint daemon logs # Check logs for errors
|
|
135
|
-
stint daemon
|
|
136
|
-
stint daemon start # Then start again
|
|
129
|
+
stint daemon restart # Restart daemon
|
|
137
130
|
```
|
|
138
131
|
|
|
139
|
-
|
|
132
|
+
**For detailed solutions**, including:
|
|
133
|
+
- Connection issues (WebSocket, API, Circuit Breaker)
|
|
134
|
+
- Daemon problems (crashes, autostart)
|
|
135
|
+
- Authentication errors
|
|
136
|
+
- Git operation failures
|
|
137
|
+
- Platform-specific issues (Windows, macOS, Linux)
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
See the **[Troubleshooting Guide](docs/TROUBLESHOOTING.md)**.
|
|
142
140
|
|
|
143
141
|
## Logging
|
|
144
142
|
|
|
@@ -254,7 +254,7 @@ var AuthServiceImpl = class {
|
|
|
254
254
|
return null;
|
|
255
255
|
}
|
|
256
256
|
try {
|
|
257
|
-
const { apiService: apiService2 } = await import("./api-
|
|
257
|
+
const { apiService: apiService2 } = await import("./api-YI2HWZGL.js");
|
|
258
258
|
const user = await apiService2.getCurrentUser();
|
|
259
259
|
logger.info("auth", `Token validated for user: ${user.email}`);
|
|
260
260
|
return user;
|
|
@@ -273,10 +273,116 @@ var AuthServiceImpl = class {
|
|
|
273
273
|
};
|
|
274
274
|
var authService = new AuthServiceImpl();
|
|
275
275
|
|
|
276
|
+
// src/utils/circuit-breaker.ts
|
|
277
|
+
var CircuitBreaker = class {
|
|
278
|
+
constructor(options) {
|
|
279
|
+
this.options = options;
|
|
280
|
+
this.options.windowSize = options.windowSize || 6e4;
|
|
281
|
+
}
|
|
282
|
+
state = "CLOSED";
|
|
283
|
+
failures = 0;
|
|
284
|
+
successes = 0;
|
|
285
|
+
lastFailureTime = 0;
|
|
286
|
+
openedAt = 0;
|
|
287
|
+
failureTimestamps = [];
|
|
288
|
+
/**
|
|
289
|
+
* Execute an operation through the circuit breaker
|
|
290
|
+
*/
|
|
291
|
+
async execute(operation, fallback) {
|
|
292
|
+
if (this.state === "OPEN") {
|
|
293
|
+
if (Date.now() - this.openedAt >= this.options.timeout) {
|
|
294
|
+
this.state = "HALF_OPEN";
|
|
295
|
+
this.successes = 0;
|
|
296
|
+
} else {
|
|
297
|
+
if (fallback) {
|
|
298
|
+
return fallback();
|
|
299
|
+
}
|
|
300
|
+
throw new Error("Circuit breaker is OPEN - service unavailable");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const result = await operation();
|
|
305
|
+
this.onSuccess();
|
|
306
|
+
return result;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.onFailure();
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Handle successful operation
|
|
314
|
+
*/
|
|
315
|
+
onSuccess() {
|
|
316
|
+
this.failures = 0;
|
|
317
|
+
if (this.state === "HALF_OPEN") {
|
|
318
|
+
this.successes++;
|
|
319
|
+
if (this.successes >= this.options.successThreshold) {
|
|
320
|
+
this.state = "CLOSED";
|
|
321
|
+
this.successes = 0;
|
|
322
|
+
this.failureTimestamps = [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Handle failed operation
|
|
328
|
+
*/
|
|
329
|
+
onFailure() {
|
|
330
|
+
this.lastFailureTime = Date.now();
|
|
331
|
+
this.failureTimestamps.push(this.lastFailureTime);
|
|
332
|
+
const windowStart = this.lastFailureTime - this.options.windowSize;
|
|
333
|
+
this.failureTimestamps = this.failureTimestamps.filter((ts) => ts > windowStart);
|
|
334
|
+
this.failures = this.failureTimestamps.length;
|
|
335
|
+
if (this.state === "HALF_OPEN") {
|
|
336
|
+
this.state = "OPEN";
|
|
337
|
+
this.openedAt = Date.now();
|
|
338
|
+
this.successes = 0;
|
|
339
|
+
} else if (this.state === "CLOSED") {
|
|
340
|
+
if (this.failures >= this.options.failureThreshold) {
|
|
341
|
+
this.state = "OPEN";
|
|
342
|
+
this.openedAt = Date.now();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Get current state
|
|
348
|
+
*/
|
|
349
|
+
getState() {
|
|
350
|
+
return this.state;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get failure count in current window
|
|
354
|
+
*/
|
|
355
|
+
getFailureCount() {
|
|
356
|
+
return this.failures;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Reset the circuit breaker to CLOSED state
|
|
360
|
+
*/
|
|
361
|
+
reset() {
|
|
362
|
+
this.state = "CLOSED";
|
|
363
|
+
this.failures = 0;
|
|
364
|
+
this.successes = 0;
|
|
365
|
+
this.failureTimestamps = [];
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
276
369
|
// src/services/api.ts
|
|
277
|
-
var AGENT_VERSION = "1.0
|
|
370
|
+
var AGENT_VERSION = "1.1.0";
|
|
278
371
|
var ApiServiceImpl = class {
|
|
279
372
|
sessionId = null;
|
|
373
|
+
circuitBreaker = new CircuitBreaker({
|
|
374
|
+
failureThreshold: 5,
|
|
375
|
+
successThreshold: 2,
|
|
376
|
+
timeout: 3e4,
|
|
377
|
+
// 30s before trying half-open
|
|
378
|
+
windowSize: 6e4
|
|
379
|
+
// 60s failure window
|
|
380
|
+
});
|
|
381
|
+
/**
|
|
382
|
+
* Get authentication headers for API requests
|
|
383
|
+
* @returns Headers object with Authorization and Content-Type
|
|
384
|
+
* @throws Error if no authentication token is found
|
|
385
|
+
*/
|
|
280
386
|
async getHeaders() {
|
|
281
387
|
const token = await authService.getToken();
|
|
282
388
|
if (!token) {
|
|
@@ -287,29 +393,43 @@ var ApiServiceImpl = class {
|
|
|
287
393
|
"Content-Type": "application/json"
|
|
288
394
|
};
|
|
289
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Make an HTTP request to the API with circuit breaker protection
|
|
398
|
+
* @param endpoint - API endpoint path (e.g., '/api/user')
|
|
399
|
+
* @param options - Fetch options (method, body, headers, etc.)
|
|
400
|
+
* @returns Parsed JSON response
|
|
401
|
+
* @throws Error if request fails or circuit breaker is open
|
|
402
|
+
*/
|
|
290
403
|
async request(endpoint, options = {}) {
|
|
291
404
|
const url = `${config.getApiUrl()}${endpoint}`;
|
|
292
405
|
const headers = await this.getHeaders();
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
406
|
+
return this.circuitBreaker.execute(async () => {
|
|
407
|
+
try {
|
|
408
|
+
const response = await fetch(url, {
|
|
409
|
+
...options,
|
|
410
|
+
headers: {
|
|
411
|
+
...headers,
|
|
412
|
+
...options.headers
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
if (!response.ok) {
|
|
416
|
+
const errorText = await response.text();
|
|
417
|
+
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
|
299
418
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
throw
|
|
419
|
+
return await response.json();
|
|
420
|
+
} catch (error) {
|
|
421
|
+
logger.error("api", `Request to ${endpoint} failed`, error);
|
|
422
|
+
throw error;
|
|
304
423
|
}
|
|
305
|
-
|
|
306
|
-
} catch (error) {
|
|
307
|
-
logger.error("api", `Request to ${endpoint} failed`, error);
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
424
|
+
});
|
|
310
425
|
}
|
|
311
426
|
/**
|
|
312
427
|
* Retry wrapper with exponential backoff
|
|
428
|
+
* @param operation - Async operation to retry
|
|
429
|
+
* @param operationName - Name for logging
|
|
430
|
+
* @param maxRetries - Maximum number of retry attempts (default: 3)
|
|
431
|
+
* @returns Result of the operation
|
|
432
|
+
* @throws Last error if all retries fail
|
|
313
433
|
*/
|
|
314
434
|
async withRetry(operation, operationName, maxRetries = 3) {
|
|
315
435
|
let lastError;
|
|
@@ -332,6 +452,11 @@ var ApiServiceImpl = class {
|
|
|
332
452
|
}
|
|
333
453
|
throw lastError;
|
|
334
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Connect agent session to the API
|
|
457
|
+
* @returns Agent session data including session ID
|
|
458
|
+
* @throws Error if connection fails
|
|
459
|
+
*/
|
|
335
460
|
async connect() {
|
|
336
461
|
logger.info("api", "Connecting agent session...");
|
|
337
462
|
const os4 = `${process.platform}-${process.arch}`;
|
|
@@ -351,6 +476,10 @@ var ApiServiceImpl = class {
|
|
|
351
476
|
return session;
|
|
352
477
|
}, "Connect");
|
|
353
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Disconnect agent session from the API
|
|
481
|
+
* @param reason - Optional reason for disconnection
|
|
482
|
+
*/
|
|
354
483
|
async disconnect(reason) {
|
|
355
484
|
if (!this.sessionId) {
|
|
356
485
|
return;
|
|
@@ -366,6 +495,10 @@ var ApiServiceImpl = class {
|
|
|
366
495
|
this.sessionId = null;
|
|
367
496
|
logger.success("api", "Agent session disconnected");
|
|
368
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Send heartbeat to keep session alive
|
|
500
|
+
* @throws Error if no active session
|
|
501
|
+
*/
|
|
369
502
|
async heartbeat() {
|
|
370
503
|
if (!this.sessionId) {
|
|
371
504
|
throw new Error("No active session");
|
|
@@ -380,6 +513,11 @@ var ApiServiceImpl = class {
|
|
|
380
513
|
logger.debug("api", "Heartbeat sent");
|
|
381
514
|
}, "Heartbeat");
|
|
382
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Get pending commits for a project
|
|
518
|
+
* @param projectId - Project ID
|
|
519
|
+
* @returns Array of pending commits
|
|
520
|
+
*/
|
|
383
521
|
async getPendingCommits(projectId) {
|
|
384
522
|
logger.info("api", `Fetching pending commits for project ${projectId}`);
|
|
385
523
|
const response = await this.request(
|
|
@@ -395,6 +533,12 @@ var ApiServiceImpl = class {
|
|
|
395
533
|
logger.info("api", `Found ${commits.length} pending commits`);
|
|
396
534
|
return commits;
|
|
397
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Mark a commit as successfully executed
|
|
538
|
+
* @param commitId - Commit ID
|
|
539
|
+
* @param sha - Git commit SHA
|
|
540
|
+
* @returns Updated commit data
|
|
541
|
+
*/
|
|
398
542
|
async markCommitExecuted(commitId, sha) {
|
|
399
543
|
logger.info("api", `Marking commit ${commitId} as executed (SHA: ${sha})`);
|
|
400
544
|
return this.withRetry(async () => {
|
|
@@ -420,6 +564,11 @@ var ApiServiceImpl = class {
|
|
|
420
564
|
return commit;
|
|
421
565
|
}, "Mark commit executed");
|
|
422
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* Mark a commit as failed
|
|
569
|
+
* @param commitId - Commit ID
|
|
570
|
+
* @param error - Error message
|
|
571
|
+
*/
|
|
423
572
|
async markCommitFailed(commitId, error) {
|
|
424
573
|
logger.error("api", `Marking commit ${commitId} as failed: ${error}`);
|
|
425
574
|
await this.withRetry(async () => {
|
|
@@ -429,6 +578,11 @@ var ApiServiceImpl = class {
|
|
|
429
578
|
});
|
|
430
579
|
}, "Mark commit failed");
|
|
431
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Sync project repository information with the API
|
|
583
|
+
* @param projectId - Project ID
|
|
584
|
+
* @param data - Repository information (path, remote URL, branches)
|
|
585
|
+
*/
|
|
432
586
|
async syncProject(projectId, data) {
|
|
433
587
|
logger.info("api", `Syncing project ${projectId}`);
|
|
434
588
|
await this.withRetry(async () => {
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
apiService,
|
|
3
3
|
config,
|
|
4
4
|
logger
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-IAERVP6F.js";
|
|
6
6
|
|
|
7
7
|
// src/utils/process.ts
|
|
8
8
|
import fs from "fs";
|
|
@@ -104,6 +104,11 @@ var GitServiceImpl = class {
|
|
|
104
104
|
getGit(path3) {
|
|
105
105
|
return simpleGit(path3);
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if a directory is a git repository
|
|
109
|
+
* @param path - Directory path to check
|
|
110
|
+
* @returns True if directory is a git repository
|
|
111
|
+
*/
|
|
107
112
|
async isRepo(path3) {
|
|
108
113
|
try {
|
|
109
114
|
const git = this.getGit(path3);
|
|
@@ -113,6 +118,12 @@ var GitServiceImpl = class {
|
|
|
113
118
|
return false;
|
|
114
119
|
}
|
|
115
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Get comprehensive repository information
|
|
123
|
+
* @param path - Repository path
|
|
124
|
+
* @returns Repository info including branches, status, and last commit
|
|
125
|
+
* @throws Error if not a valid repository or no commits found
|
|
126
|
+
*/
|
|
116
127
|
async getRepoInfo(path3) {
|
|
117
128
|
try {
|
|
118
129
|
const git = this.getGit(path3);
|
package/dist/daemon/runner.js
CHANGED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
projectService,
|
|
6
6
|
removePidFile,
|
|
7
7
|
writePidFile
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-OHOFKJL7.js";
|
|
9
9
|
import {
|
|
10
10
|
apiService,
|
|
11
11
|
authService,
|
|
12
12
|
config,
|
|
13
13
|
logger
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-IAERVP6F.js";
|
|
15
15
|
|
|
16
16
|
// src/daemon/runner.ts
|
|
17
17
|
import "dotenv/config";
|
|
@@ -33,6 +33,10 @@ var WebSocketServiceImpl = class {
|
|
|
33
33
|
disconnectHandlers = [];
|
|
34
34
|
agentDisconnectedHandlers = [];
|
|
35
35
|
syncRequestedHandlers = [];
|
|
36
|
+
/**
|
|
37
|
+
* Connect to the WebSocket server
|
|
38
|
+
* @throws Error if connection fails or no auth token available
|
|
39
|
+
*/
|
|
36
40
|
async connect() {
|
|
37
41
|
try {
|
|
38
42
|
const token = await authService.getToken();
|
|
@@ -71,6 +75,10 @@ var WebSocketServiceImpl = class {
|
|
|
71
75
|
throw error;
|
|
72
76
|
}
|
|
73
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Disconnect from the WebSocket server
|
|
80
|
+
* Prevents automatic reconnection
|
|
81
|
+
*/
|
|
74
82
|
disconnect() {
|
|
75
83
|
this.isManualDisconnect = true;
|
|
76
84
|
if (this.reconnectTimer) {
|
|
@@ -91,9 +99,17 @@ var WebSocketServiceImpl = class {
|
|
|
91
99
|
logger.info("websocket", "WebSocket disconnected");
|
|
92
100
|
}
|
|
93
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if WebSocket is currently connected
|
|
104
|
+
* @returns True if connected and ready
|
|
105
|
+
*/
|
|
94
106
|
isConnected() {
|
|
95
107
|
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
96
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to user-specific channel for real-time updates
|
|
111
|
+
* @param userId - User ID to subscribe to
|
|
112
|
+
*/
|
|
97
113
|
subscribeToUserChannel(userId) {
|
|
98
114
|
this.userId = userId;
|
|
99
115
|
if (!this.isConnected()) {
|
|
@@ -109,6 +125,10 @@ var WebSocketServiceImpl = class {
|
|
|
109
125
|
}
|
|
110
126
|
});
|
|
111
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Register handler for commit approved events
|
|
130
|
+
* @param handler - Callback function
|
|
131
|
+
*/
|
|
112
132
|
onCommitApproved(handler) {
|
|
113
133
|
this.commitApprovedHandlers.push(handler);
|
|
114
134
|
}
|
|
@@ -214,10 +234,16 @@ var WebSocketServiceImpl = class {
|
|
|
214
234
|
logger.error("websocket", "Max reconnection attempts reached");
|
|
215
235
|
}
|
|
216
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Get reconnect delay with exponential backoff and jitter
|
|
239
|
+
* Jitter prevents thundering herd problem when many clients reconnect simultaneously
|
|
240
|
+
*/
|
|
217
241
|
getReconnectDelay() {
|
|
218
242
|
const delays = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
219
243
|
const index = Math.min(this.reconnectAttempts, delays.length - 1);
|
|
220
|
-
|
|
244
|
+
const baseDelay = delays[index];
|
|
245
|
+
const jitter = baseDelay * (Math.random() * 0.3);
|
|
246
|
+
return Math.floor(baseDelay + jitter);
|
|
221
247
|
}
|
|
222
248
|
};
|
|
223
249
|
var websocketService = new WebSocketServiceImpl();
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
projectService,
|
|
9
9
|
spawnDetached,
|
|
10
10
|
validatePidFile
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-OHOFKJL7.js";
|
|
12
12
|
import {
|
|
13
13
|
apiService,
|
|
14
14
|
authService,
|
|
15
15
|
config,
|
|
16
16
|
logger
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-IAERVP6F.js";
|
|
18
18
|
|
|
19
19
|
// src/index.ts
|
|
20
20
|
import "dotenv/config";
|
|
@@ -1328,7 +1328,7 @@ Current version: ${currentVersion}`));
|
|
|
1328
1328
|
}
|
|
1329
1329
|
|
|
1330
1330
|
// src/index.ts
|
|
1331
|
-
var AGENT_VERSION = "1.0
|
|
1331
|
+
var AGENT_VERSION = "1.1.0";
|
|
1332
1332
|
var program = new Command();
|
|
1333
1333
|
program.name("stint").description("Stint Agent - Local daemon for Stint Project Assistant").version(AGENT_VERSION, "-v, --version", "output the current version").addHelpText("after", `
|
|
1334
1334
|
${chalk12.bold("Examples:")}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gowelle/stint-agent",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Local agent for Stint - Project Assistant",
|
|
5
5
|
"author": "Gowelle John <gowelle.john@icloud.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@types/ws": "^8.5.10",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
56
56
|
"@typescript-eslint/parser": "^8.50.0",
|
|
57
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
57
58
|
"eslint": "^8.56.0",
|
|
58
59
|
"tsup": "^8.0.1",
|
|
59
60
|
"typescript": "^5.3.3",
|