@gowelle/stint-agent 1.0.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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/api-K3EUONWR.js +6 -0
- package/dist/chunk-5DWSNHS6.js +448 -0
- package/dist/chunk-PPODHVVP.js +408 -0
- package/dist/daemon/runner.js +484 -0
- package/dist/index.js +854 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gowelle John
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Stint Agent
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@gowelle/stint-agent)
|
|
4
|
+
[](https://www.npmjs.com/package/@gowelle/stint-agent)
|
|
5
|
+
[](https://github.com/gowelle/stint-agent/actions/workflows/ci.yml)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
|
|
9
|
+
The official CLI agent for [Stint](https://stint.codes) — a lightweight daemon that bridges the Stint web app and your local git repositories, enabling automatic commit execution and real-time repo syncing.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🔐 Secure authentication with OAuth
|
|
14
|
+
- 🔄 Real-time WebSocket connection to Stint
|
|
15
|
+
- 📦 Automatic commit execution
|
|
16
|
+
- 🔍 Repository status syncing
|
|
17
|
+
- 🖥️ Background daemon process
|
|
18
|
+
- 📝 Comprehensive logging
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g @gowelle/stint-agent
|
|
24
|
+
# or
|
|
25
|
+
pnpm add -g @gowelle/stint-agent
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Authenticate with Stint
|
|
32
|
+
stint login
|
|
33
|
+
|
|
34
|
+
# Check your authentication status
|
|
35
|
+
stint whoami
|
|
36
|
+
|
|
37
|
+
# Link a project
|
|
38
|
+
cd /path/to/your/project
|
|
39
|
+
stint link
|
|
40
|
+
|
|
41
|
+
# Start the daemon
|
|
42
|
+
stint daemon start
|
|
43
|
+
|
|
44
|
+
# Check daemon status
|
|
45
|
+
stint daemon status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
### Authentication
|
|
51
|
+
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `stint login` | Authenticate with Stint (opens browser for OAuth) |
|
|
55
|
+
| `stint logout` | Remove stored credentials |
|
|
56
|
+
| `stint whoami` | Show current user and machine information |
|
|
57
|
+
|
|
58
|
+
### Daemon
|
|
59
|
+
|
|
60
|
+
| Command | Description |
|
|
61
|
+
|---------|-------------|
|
|
62
|
+
| `stint daemon start` | Start background daemon |
|
|
63
|
+
| `stint daemon stop` | Stop daemon gracefully |
|
|
64
|
+
| `stint daemon status` | Check if daemon is running |
|
|
65
|
+
| `stint daemon logs [--lines N]` | View daemon logs (default: 50 lines) |
|
|
66
|
+
| `stint daemon restart` | Restart the daemon |
|
|
67
|
+
|
|
68
|
+
### Project Management
|
|
69
|
+
|
|
70
|
+
| Command | Description |
|
|
71
|
+
|---------|-------------|
|
|
72
|
+
| `stint link` | Link current directory to a Stint project |
|
|
73
|
+
| `stint unlink [--force]` | Remove project link |
|
|
74
|
+
| `stint status` | Show project, git, auth, and daemon status |
|
|
75
|
+
| `stint sync` | Manually sync repository information to server |
|
|
76
|
+
|
|
77
|
+
### Commit Operations
|
|
78
|
+
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `stint commits` | List pending commits for this repository |
|
|
82
|
+
| `stint commit <id>` | Execute a specific pending commit |
|
|
83
|
+
|
|
84
|
+
## Complete Workflow
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 1. Install and authenticate
|
|
88
|
+
npm install -g @gowelle/stint-agent
|
|
89
|
+
stint login
|
|
90
|
+
|
|
91
|
+
# 2. Link your project
|
|
92
|
+
cd /path/to/your/project
|
|
93
|
+
stint link
|
|
94
|
+
|
|
95
|
+
# 3. Start the daemon
|
|
96
|
+
stint daemon start
|
|
97
|
+
|
|
98
|
+
# 4. Check status
|
|
99
|
+
stint status
|
|
100
|
+
|
|
101
|
+
# Now commits approved in the web app will execute automatically!
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Troubleshooting
|
|
105
|
+
|
|
106
|
+
### "Not authenticated" error
|
|
107
|
+
|
|
108
|
+
Run `stint login` to authenticate with your Stint account.
|
|
109
|
+
|
|
110
|
+
### "Repository has uncommitted changes"
|
|
111
|
+
|
|
112
|
+
The agent requires a clean repository to execute commits:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
git stash # Temporarily stash changes
|
|
116
|
+
# or
|
|
117
|
+
git add . && git commit -m "message"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Daemon won't start
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
stint daemon status # Check if already running
|
|
124
|
+
stint daemon logs # Check logs for errors
|
|
125
|
+
stint daemon stop # Stop first
|
|
126
|
+
stint daemon start # Then start again
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### WebSocket connection issues
|
|
130
|
+
|
|
131
|
+
Check your network connection and firewall settings.
|
|
132
|
+
|
|
133
|
+
## Logging
|
|
134
|
+
|
|
135
|
+
Logs are stored in your system's config directory:
|
|
136
|
+
|
|
137
|
+
| Platform | Log Location |
|
|
138
|
+
|----------|--------------|
|
|
139
|
+
| **macOS** | `~/.config/stint/logs/` |
|
|
140
|
+
| **Linux** | `~/.config/stint/logs/` |
|
|
141
|
+
| **Windows** | `%USERPROFILE%\.config\stint\logs\` |
|
|
142
|
+
|
|
143
|
+
Log files:
|
|
144
|
+
- `agent.log` - General CLI operations
|
|
145
|
+
- `daemon.log` - Daemon process logs
|
|
146
|
+
- `error.log` - Error details
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git clone https://github.com/gowelle/stint-agent.git
|
|
152
|
+
cd stint-agent
|
|
153
|
+
pnpm install
|
|
154
|
+
pnpm build
|
|
155
|
+
pnpm dev # Watch mode
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Security
|
|
159
|
+
|
|
160
|
+
- Tokens are encrypted at rest using machine-specific keys
|
|
161
|
+
- All API communication uses HTTPS
|
|
162
|
+
- WebSocket connections are authenticated
|
|
163
|
+
- Git operations are restricted to linked directories
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT © [Gowelle John](https://github.com/gowelle)
|
|
168
|
+
|
|
169
|
+
## Support
|
|
170
|
+
|
|
171
|
+
For issues and questions, please [open an issue](https://github.com/gowelle/stint-agent/issues).
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// src/services/api.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
|
|
4
|
+
// src/utils/config.ts
|
|
5
|
+
import Conf from "conf";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import os from "os";
|
|
8
|
+
var DEFAULT_CONFIG = {
|
|
9
|
+
apiUrl: "https://stint.codes",
|
|
10
|
+
wsUrl: "wss://stint.codes/reverb",
|
|
11
|
+
reverbAppKey: "wtn6tu6lirfv6yflujk7",
|
|
12
|
+
projects: {}
|
|
13
|
+
};
|
|
14
|
+
var ConfigManager = class {
|
|
15
|
+
conf;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.conf = new Conf({
|
|
18
|
+
projectName: "stint",
|
|
19
|
+
defaults: {
|
|
20
|
+
...DEFAULT_CONFIG,
|
|
21
|
+
machineId: randomUUID(),
|
|
22
|
+
machineName: os.hostname()
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
get(key) {
|
|
27
|
+
return this.conf.get(key);
|
|
28
|
+
}
|
|
29
|
+
set(key, value) {
|
|
30
|
+
this.conf.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
getAll() {
|
|
33
|
+
return this.conf.store;
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
this.conf.clear();
|
|
37
|
+
}
|
|
38
|
+
// Token management
|
|
39
|
+
getToken() {
|
|
40
|
+
return this.conf.get("token");
|
|
41
|
+
}
|
|
42
|
+
setToken(token) {
|
|
43
|
+
this.conf.set("token", token);
|
|
44
|
+
}
|
|
45
|
+
clearToken() {
|
|
46
|
+
this.conf.delete("token");
|
|
47
|
+
}
|
|
48
|
+
// Machine info
|
|
49
|
+
getMachineId() {
|
|
50
|
+
return this.conf.get("machineId");
|
|
51
|
+
}
|
|
52
|
+
getMachineName() {
|
|
53
|
+
return this.conf.get("machineName");
|
|
54
|
+
}
|
|
55
|
+
// Project management
|
|
56
|
+
getProjects() {
|
|
57
|
+
return this.conf.get("projects") || {};
|
|
58
|
+
}
|
|
59
|
+
getProject(path2) {
|
|
60
|
+
const projects = this.getProjects();
|
|
61
|
+
return projects[path2];
|
|
62
|
+
}
|
|
63
|
+
setProject(path2, project) {
|
|
64
|
+
const projects = this.getProjects();
|
|
65
|
+
projects[path2] = project;
|
|
66
|
+
this.conf.set("projects", projects);
|
|
67
|
+
}
|
|
68
|
+
removeProject(path2) {
|
|
69
|
+
const projects = this.getProjects();
|
|
70
|
+
delete projects[path2];
|
|
71
|
+
this.conf.set("projects", projects);
|
|
72
|
+
}
|
|
73
|
+
// API URLs
|
|
74
|
+
getApiUrl() {
|
|
75
|
+
return this.conf.get("apiUrl");
|
|
76
|
+
}
|
|
77
|
+
getWsUrl() {
|
|
78
|
+
const environment = this.getEnvironment();
|
|
79
|
+
const reverbAppKey = this.getReverbAppKey();
|
|
80
|
+
let baseUrl;
|
|
81
|
+
if (environment === "development") {
|
|
82
|
+
baseUrl = "ws://localhost:8080";
|
|
83
|
+
} else {
|
|
84
|
+
baseUrl = this.conf.get("wsUrl") || "wss://stint.codes/reverb";
|
|
85
|
+
}
|
|
86
|
+
if (reverbAppKey && reverbAppKey.trim() !== "") {
|
|
87
|
+
const cleanBaseUrl2 = baseUrl.replace(/\/$/, "");
|
|
88
|
+
const baseWithoutReverb = cleanBaseUrl2.replace(/\/reverb$/, "");
|
|
89
|
+
return `${baseWithoutReverb}/app/${reverbAppKey}`;
|
|
90
|
+
}
|
|
91
|
+
const cleanBaseUrl = baseUrl.replace(/\/$/, "");
|
|
92
|
+
if (!cleanBaseUrl.includes("/reverb")) {
|
|
93
|
+
return `${cleanBaseUrl}/reverb`;
|
|
94
|
+
}
|
|
95
|
+
return cleanBaseUrl;
|
|
96
|
+
}
|
|
97
|
+
// Environment management
|
|
98
|
+
getEnvironment() {
|
|
99
|
+
const configEnv = this.conf.get("environment");
|
|
100
|
+
if (configEnv === "development" || configEnv === "production") {
|
|
101
|
+
return configEnv;
|
|
102
|
+
}
|
|
103
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
104
|
+
if (nodeEnv === "development" || nodeEnv === "dev") {
|
|
105
|
+
return "development";
|
|
106
|
+
}
|
|
107
|
+
return "production";
|
|
108
|
+
}
|
|
109
|
+
setEnvironment(environment) {
|
|
110
|
+
this.conf.set("environment", environment);
|
|
111
|
+
}
|
|
112
|
+
// Reverb App Key management
|
|
113
|
+
getReverbAppKey() {
|
|
114
|
+
return process.env.REVERB_APP_KEY || process.env.STINT_REVERB_APP_KEY || this.conf.get("reverbAppKey");
|
|
115
|
+
}
|
|
116
|
+
setReverbAppKey(reverbAppKey) {
|
|
117
|
+
this.conf.set("reverbAppKey", reverbAppKey);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var config = new ConfigManager();
|
|
121
|
+
|
|
122
|
+
// src/utils/crypto.ts
|
|
123
|
+
import crypto from "crypto";
|
|
124
|
+
import os2 from "os";
|
|
125
|
+
function getMachineKey() {
|
|
126
|
+
const machineInfo = `${os2.hostname()}-${os2.platform()}-${os2.arch()}`;
|
|
127
|
+
return crypto.createHash("sha256").update(machineInfo).digest();
|
|
128
|
+
}
|
|
129
|
+
var ALGORITHM = "aes-256-gcm";
|
|
130
|
+
var IV_LENGTH = 16;
|
|
131
|
+
var AUTH_TAG_LENGTH = 16;
|
|
132
|
+
function encrypt(text) {
|
|
133
|
+
const key = getMachineKey();
|
|
134
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
135
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
136
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
137
|
+
encrypted += cipher.final("hex");
|
|
138
|
+
const authTag = cipher.getAuthTag();
|
|
139
|
+
return iv.toString("hex") + authTag.toString("hex") + encrypted;
|
|
140
|
+
}
|
|
141
|
+
function decrypt(encryptedText) {
|
|
142
|
+
const key = getMachineKey();
|
|
143
|
+
const iv = Buffer.from(encryptedText.slice(0, IV_LENGTH * 2), "hex");
|
|
144
|
+
const authTag = Buffer.from(
|
|
145
|
+
encryptedText.slice(IV_LENGTH * 2, (IV_LENGTH + AUTH_TAG_LENGTH) * 2),
|
|
146
|
+
"hex"
|
|
147
|
+
);
|
|
148
|
+
const encrypted = encryptedText.slice((IV_LENGTH + AUTH_TAG_LENGTH) * 2);
|
|
149
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
150
|
+
decipher.setAuthTag(authTag);
|
|
151
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
152
|
+
decrypted += decipher.final("utf8");
|
|
153
|
+
return decrypted;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/utils/logger.ts
|
|
157
|
+
import fs from "fs";
|
|
158
|
+
import path from "path";
|
|
159
|
+
import os3 from "os";
|
|
160
|
+
var LOG_DIR = path.join(os3.homedir(), ".config", "stint", "logs");
|
|
161
|
+
var AGENT_LOG = path.join(LOG_DIR, "agent.log");
|
|
162
|
+
var ERROR_LOG = path.join(LOG_DIR, "error.log");
|
|
163
|
+
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
164
|
+
var MAX_LOG_FILES = 7;
|
|
165
|
+
var Logger = class {
|
|
166
|
+
ensureLogDir() {
|
|
167
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
168
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
rotateLog(logFile) {
|
|
172
|
+
if (!fs.existsSync(logFile)) return;
|
|
173
|
+
const stats = fs.statSync(logFile);
|
|
174
|
+
if (stats.size < MAX_LOG_SIZE) return;
|
|
175
|
+
for (let i = MAX_LOG_FILES - 1; i > 0; i--) {
|
|
176
|
+
const oldFile = `${logFile}.${i}`;
|
|
177
|
+
const newFile = `${logFile}.${i + 1}`;
|
|
178
|
+
if (fs.existsSync(oldFile)) {
|
|
179
|
+
if (i === MAX_LOG_FILES - 1) {
|
|
180
|
+
fs.unlinkSync(oldFile);
|
|
181
|
+
} else {
|
|
182
|
+
fs.renameSync(oldFile, newFile);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
fs.renameSync(logFile, `${logFile}.1`);
|
|
187
|
+
}
|
|
188
|
+
writeLog(level, category, message, logFile) {
|
|
189
|
+
this.ensureLogDir();
|
|
190
|
+
this.rotateLog(logFile);
|
|
191
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
192
|
+
const logLine = `[${timestamp}] ${level.padEnd(5)} [${category}] ${message}
|
|
193
|
+
`;
|
|
194
|
+
fs.appendFileSync(logFile, logLine);
|
|
195
|
+
}
|
|
196
|
+
info(category, message) {
|
|
197
|
+
this.writeLog("INFO", category, message, AGENT_LOG);
|
|
198
|
+
console.log(`\u2139 [${category}] ${message}`);
|
|
199
|
+
}
|
|
200
|
+
warn(category, message) {
|
|
201
|
+
this.writeLog("WARN", category, message, AGENT_LOG);
|
|
202
|
+
console.warn(`\u26A0 [${category}] ${message}`);
|
|
203
|
+
}
|
|
204
|
+
error(category, message, error) {
|
|
205
|
+
const fullMessage = error ? `${message}: ${error.message}` : message;
|
|
206
|
+
this.writeLog("ERROR", category, fullMessage, ERROR_LOG);
|
|
207
|
+
this.writeLog("ERROR", category, fullMessage, AGENT_LOG);
|
|
208
|
+
console.error(`\u2716 [${category}] ${fullMessage}`);
|
|
209
|
+
if (error?.stack) {
|
|
210
|
+
this.writeLog("ERROR", category, error.stack, ERROR_LOG);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
debug(category, message) {
|
|
214
|
+
if (process.env.DEBUG) {
|
|
215
|
+
this.writeLog("DEBUG", category, message, AGENT_LOG);
|
|
216
|
+
console.debug(`\u{1F41B} [${category}] ${message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
success(category, message) {
|
|
220
|
+
this.writeLog("INFO", category, message, AGENT_LOG);
|
|
221
|
+
console.log(`\u2713 [${category}] ${message}`);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var logger = new Logger();
|
|
225
|
+
|
|
226
|
+
// src/services/auth.ts
|
|
227
|
+
var AuthServiceImpl = class {
|
|
228
|
+
async saveToken(token) {
|
|
229
|
+
try {
|
|
230
|
+
const encryptedToken = encrypt(token);
|
|
231
|
+
config.setToken(encryptedToken);
|
|
232
|
+
logger.info("auth", "Token saved successfully");
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.error("auth", "Failed to save token", error);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async getToken() {
|
|
239
|
+
try {
|
|
240
|
+
const encryptedToken = config.getToken();
|
|
241
|
+
if (!encryptedToken) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
return decrypt(encryptedToken);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
logger.error("auth", "Failed to decrypt token", error);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async clearToken() {
|
|
251
|
+
config.clearToken();
|
|
252
|
+
logger.info("auth", "Token cleared");
|
|
253
|
+
}
|
|
254
|
+
async validateToken() {
|
|
255
|
+
const token = await this.getToken();
|
|
256
|
+
if (!token) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const { apiService: apiService2 } = await import("./api-K3EUONWR.js");
|
|
261
|
+
const user = await apiService2.getCurrentUser();
|
|
262
|
+
logger.info("auth", `Token validated for user: ${user.email}`);
|
|
263
|
+
return user;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
logger.warn("auth", "Token validation failed");
|
|
266
|
+
logger.error("auth", "Failed to validate token", error);
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
getMachineId() {
|
|
271
|
+
return config.getMachineId();
|
|
272
|
+
}
|
|
273
|
+
getMachineName() {
|
|
274
|
+
return config.getMachineName();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var authService = new AuthServiceImpl();
|
|
278
|
+
|
|
279
|
+
// src/services/api.ts
|
|
280
|
+
var require2 = createRequire(import.meta.url);
|
|
281
|
+
var packageJson = require2("../../package.json");
|
|
282
|
+
var AGENT_VERSION = packageJson.version;
|
|
283
|
+
var ApiServiceImpl = class {
|
|
284
|
+
sessionId = null;
|
|
285
|
+
async getHeaders() {
|
|
286
|
+
const token = await authService.getToken();
|
|
287
|
+
if (!token) {
|
|
288
|
+
throw new Error('No authentication token found. Please run "stint login" first.');
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
Authorization: `Bearer ${token}`,
|
|
292
|
+
"Content-Type": "application/json"
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async request(endpoint, options = {}) {
|
|
296
|
+
const url = `${config.getApiUrl()}${endpoint}`;
|
|
297
|
+
const headers = await this.getHeaders();
|
|
298
|
+
try {
|
|
299
|
+
const response = await fetch(url, {
|
|
300
|
+
...options,
|
|
301
|
+
headers: {
|
|
302
|
+
...headers,
|
|
303
|
+
...options.headers
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
const errorText = await response.text();
|
|
308
|
+
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
|
309
|
+
}
|
|
310
|
+
return await response.json();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error("api", `Request to ${endpoint} failed`, error);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Retry wrapper with exponential backoff
|
|
318
|
+
*/
|
|
319
|
+
async withRetry(operation, operationName, maxRetries = 3) {
|
|
320
|
+
let lastError;
|
|
321
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
322
|
+
try {
|
|
323
|
+
return await operation();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
lastError = error;
|
|
326
|
+
if (lastError.message.includes("401") || lastError.message.includes("403")) {
|
|
327
|
+
throw lastError;
|
|
328
|
+
}
|
|
329
|
+
if (attempt < maxRetries) {
|
|
330
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 5e3);
|
|
331
|
+
logger.warn("api", `${operationName} failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
|
|
332
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
333
|
+
} else {
|
|
334
|
+
logger.error("api", `${operationName} failed after ${maxRetries} attempts`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
throw lastError;
|
|
339
|
+
}
|
|
340
|
+
async connect() {
|
|
341
|
+
logger.info("api", "Connecting agent session...");
|
|
342
|
+
const os4 = `${process.platform}-${process.arch}`;
|
|
343
|
+
return this.withRetry(async () => {
|
|
344
|
+
const response = await this.request("/api/agent/connect", {
|
|
345
|
+
method: "POST",
|
|
346
|
+
body: JSON.stringify({
|
|
347
|
+
machine_id: authService.getMachineId(),
|
|
348
|
+
machine_name: authService.getMachineName(),
|
|
349
|
+
os: os4,
|
|
350
|
+
agent_version: AGENT_VERSION
|
|
351
|
+
})
|
|
352
|
+
});
|
|
353
|
+
const session = response.data;
|
|
354
|
+
this.sessionId = session.id;
|
|
355
|
+
logger.success("api", `Agent session connected: ${session.id}`);
|
|
356
|
+
return session;
|
|
357
|
+
}, "Connect");
|
|
358
|
+
}
|
|
359
|
+
async disconnect() {
|
|
360
|
+
if (!this.sessionId) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
logger.info("api", "Disconnecting agent session...");
|
|
364
|
+
await this.request("/api/agent/disconnect", {
|
|
365
|
+
method: "POST",
|
|
366
|
+
body: JSON.stringify({
|
|
367
|
+
session_id: this.sessionId
|
|
368
|
+
})
|
|
369
|
+
});
|
|
370
|
+
this.sessionId = null;
|
|
371
|
+
logger.success("api", "Agent session disconnected");
|
|
372
|
+
}
|
|
373
|
+
async heartbeat() {
|
|
374
|
+
if (!this.sessionId) {
|
|
375
|
+
throw new Error("No active session");
|
|
376
|
+
}
|
|
377
|
+
await this.withRetry(async () => {
|
|
378
|
+
await this.request("/api/agent/heartbeat", {
|
|
379
|
+
method: "POST",
|
|
380
|
+
body: JSON.stringify({
|
|
381
|
+
session_id: this.sessionId
|
|
382
|
+
})
|
|
383
|
+
});
|
|
384
|
+
logger.debug("api", "Heartbeat sent");
|
|
385
|
+
}, "Heartbeat");
|
|
386
|
+
}
|
|
387
|
+
async getPendingCommits(projectId) {
|
|
388
|
+
logger.info("api", `Fetching pending commits for project ${projectId}`);
|
|
389
|
+
const commits = await this.request(
|
|
390
|
+
`/api/agent/projects/${projectId}/pending-commits`
|
|
391
|
+
);
|
|
392
|
+
logger.info("api", `Found ${commits.length} pending commits`);
|
|
393
|
+
return commits;
|
|
394
|
+
}
|
|
395
|
+
async markCommitExecuted(commitId, sha) {
|
|
396
|
+
logger.info("api", `Marking commit ${commitId} as executed (SHA: ${sha})`);
|
|
397
|
+
return this.withRetry(async () => {
|
|
398
|
+
const commit = await this.request(
|
|
399
|
+
`/api/agent/commits/${commitId}/executed`,
|
|
400
|
+
{
|
|
401
|
+
method: "POST",
|
|
402
|
+
body: JSON.stringify({ sha })
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
logger.success("api", `Commit ${commitId} marked as executed`);
|
|
406
|
+
return commit;
|
|
407
|
+
}, "Mark commit executed");
|
|
408
|
+
}
|
|
409
|
+
async markCommitFailed(commitId, error) {
|
|
410
|
+
logger.error("api", `Marking commit ${commitId} as failed: ${error}`);
|
|
411
|
+
await this.withRetry(async () => {
|
|
412
|
+
await this.request(`/api/agent/commits/${commitId}/failed`, {
|
|
413
|
+
method: "POST",
|
|
414
|
+
body: JSON.stringify({ error })
|
|
415
|
+
});
|
|
416
|
+
}, "Mark commit failed");
|
|
417
|
+
}
|
|
418
|
+
async syncProject(projectId, data) {
|
|
419
|
+
logger.info("api", `Syncing project ${projectId}`);
|
|
420
|
+
await this.withRetry(async () => {
|
|
421
|
+
await this.request(`/api/agent/projects/${projectId}/sync`, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
body: JSON.stringify(data)
|
|
424
|
+
});
|
|
425
|
+
logger.success("api", `Project ${projectId} synced`);
|
|
426
|
+
}, "Sync project");
|
|
427
|
+
}
|
|
428
|
+
async getLinkedProjects() {
|
|
429
|
+
logger.info("api", "Fetching linked projects");
|
|
430
|
+
const projects = await this.request("/api/agent/projects");
|
|
431
|
+
logger.info("api", `Found ${projects.length} linked projects`);
|
|
432
|
+
return projects;
|
|
433
|
+
}
|
|
434
|
+
async getCurrentUser() {
|
|
435
|
+
logger.info("api", "Fetching current user");
|
|
436
|
+
const user = await this.request("/api/user");
|
|
437
|
+
logger.info("api", `Fetched user: ${user.email}`);
|
|
438
|
+
return user;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
var apiService = new ApiServiceImpl();
|
|
442
|
+
|
|
443
|
+
export {
|
|
444
|
+
config,
|
|
445
|
+
logger,
|
|
446
|
+
apiService,
|
|
447
|
+
authService
|
|
448
|
+
};
|