@gowelle/stint-agent 1.0.1 → 1.0.3

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
@@ -34,7 +34,7 @@ stint login
34
34
  # Check your authentication status
35
35
  stint whoami
36
36
 
37
- # Link a project
37
+ # Link a project (or create a new one)
38
38
  cd /path/to/your/project
39
39
  stint link
40
40
 
@@ -47,6 +47,13 @@ stint daemon status
47
47
 
48
48
  ## Commands
49
49
 
50
+ ### General
51
+
52
+ | Command | Description |
53
+ |---------|-------------|
54
+ | `stint --version`, `stint -V` | Show current agent version |
55
+ | `stint --help`, `stint -h` | Show help information |
56
+
50
57
  ### Authentication
51
58
 
52
59
  | Command | Description |
@@ -69,7 +76,7 @@ stint daemon status
69
76
 
70
77
  | Command | Description |
71
78
  |---------|-------------|
72
- | `stint link` | Link current directory to a Stint project |
79
+ | `stint link` | Link current directory to a Stint project (or create a new one) |
73
80
  | `stint unlink [--force]` | Remove project link |
74
81
  | `stint status` | Show project, git, auth, and daemon status |
75
82
  | `stint sync` | Manually sync repository information to server |
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  apiService
3
- } from "./chunk-CCM25AHM.js";
3
+ } from "./chunk-NMOUYAW2.js";
4
4
  export {
5
5
  apiService
6
6
  };
@@ -2,7 +2,7 @@ import {
2
2
  apiService,
3
3
  config,
4
4
  logger
5
- } from "./chunk-CCM25AHM.js";
5
+ } from "./chunk-NMOUYAW2.js";
6
6
 
7
7
  // src/utils/process.ts
8
8
  import fs from "fs";
@@ -322,17 +322,11 @@ var CommitQueueProcessor = class {
322
322
  throw new Error(`Directory ${projectPath} is not a git repository`);
323
323
  }
324
324
  const status = await gitService.getStatus(projectPath);
325
- const hasChanges = status.staged.length > 0 || status.unstaged.length > 0 || status.untracked.length > 0;
326
- if (hasChanges) {
327
- throw new Error("Repository has uncommitted changes. Please commit or stash them first.");
328
- }
329
- if (commit.files && commit.files.length > 0) {
330
- logger.info("queue", `Staging specific files: ${commit.files.join(", ")}`);
331
- await gitService.stageFiles(projectPath, commit.files);
332
- } else {
333
- logger.info("queue", "Staging all changes");
334
- await gitService.stageAll(projectPath);
325
+ const hasStagedChanges = status.staged.length > 0;
326
+ if (!hasStagedChanges) {
327
+ throw new Error('No staged changes to commit. Please stage files using "git add" before committing.');
335
328
  }
329
+ logger.info("queue", `Committing ${status.staged.length} staged files.`);
336
330
  logger.info("queue", `Creating commit with message: "${commit.message}"`);
337
331
  const sha = await gitService.commit(projectPath, commit.message);
338
332
  logger.success("queue", `Commit created successfully: ${sha}`);
@@ -254,7 +254,7 @@ var AuthServiceImpl = class {
254
254
  return null;
255
255
  }
256
256
  try {
257
- const { apiService: apiService2 } = await import("./api-GH4C2AFN.js");
257
+ const { apiService: apiService2 } = await import("./api-37DCZYLF.js");
258
258
  const user = await apiService2.getCurrentUser();
259
259
  logger.info("auth", `Token validated for user: ${user.email}`);
260
260
  return user;
@@ -274,7 +274,7 @@ var AuthServiceImpl = class {
274
274
  var authService = new AuthServiceImpl();
275
275
 
276
276
  // src/services/api.ts
277
- var AGENT_VERSION = "1.0.1";
277
+ var AGENT_VERSION = "1.0.3";
278
278
  var ApiServiceImpl = class {
279
279
  sessionId = null;
280
280
  async getHeaders() {
@@ -351,7 +351,7 @@ var ApiServiceImpl = class {
351
351
  return session;
352
352
  }, "Connect");
353
353
  }
354
- async disconnect() {
354
+ async disconnect(reason) {
355
355
  if (!this.sessionId) {
356
356
  return;
357
357
  }
@@ -359,7 +359,8 @@ var ApiServiceImpl = class {
359
359
  await this.request("/api/agent/disconnect", {
360
360
  method: "POST",
361
361
  body: JSON.stringify({
362
- session_id: this.sessionId
362
+ session_id: this.sessionId,
363
+ reason: reason || "Agent disconnected"
363
364
  })
364
365
  });
365
366
  this.sessionId = null;
@@ -381,22 +382,40 @@ var ApiServiceImpl = class {
381
382
  }
382
383
  async getPendingCommits(projectId) {
383
384
  logger.info("api", `Fetching pending commits for project ${projectId}`);
384
- const commits = await this.request(
385
- `/api/agent/projects/${projectId}/pending-commits`
385
+ const response = await this.request(
386
+ `/api/agent/pending-commits?project_id=${projectId}`
386
387
  );
388
+ const commits = response.data.map((item) => ({
389
+ id: item.id,
390
+ projectId: item.project_id || item.projectId,
391
+ message: item.message,
392
+ files: item.files,
393
+ createdAt: item.created_at || item.createdAt
394
+ }));
387
395
  logger.info("api", `Found ${commits.length} pending commits`);
388
396
  return commits;
389
397
  }
390
398
  async markCommitExecuted(commitId, sha) {
391
399
  logger.info("api", `Marking commit ${commitId} as executed (SHA: ${sha})`);
392
400
  return this.withRetry(async () => {
393
- const commit = await this.request(
401
+ const response = await this.request(
394
402
  `/api/agent/commits/${commitId}/executed`,
395
403
  {
396
404
  method: "POST",
397
405
  body: JSON.stringify({ sha })
398
406
  }
399
407
  );
408
+ const data = response.data;
409
+ const commit = {
410
+ id: data.id,
411
+ projectId: data.project_id || data.projectId,
412
+ message: data.message,
413
+ sha: data.sha,
414
+ status: data.status,
415
+ createdAt: data.created_at || data.createdAt,
416
+ executedAt: data.executed_at || data.executedAt,
417
+ error: data.error
418
+ };
400
419
  logger.success("api", `Commit ${commitId} marked as executed`);
401
420
  return commit;
402
421
  }, "Mark commit executed");
@@ -422,7 +441,8 @@ var ApiServiceImpl = class {
422
441
  }
423
442
  async getLinkedProjects() {
424
443
  logger.info("api", "Fetching linked projects");
425
- const projects = await this.request("/api/agent/projects");
444
+ const response = await this.request("/api/agent/projects");
445
+ const projects = response.data;
426
446
  logger.info("api", `Found ${projects.length} linked projects`);
427
447
  return projects;
428
448
  }
@@ -432,6 +452,16 @@ var ApiServiceImpl = class {
432
452
  logger.info("api", `Fetched user: ${user.email}`);
433
453
  return user;
434
454
  }
455
+ async createProject(data) {
456
+ logger.info("api", `Creating project: ${data.name}`);
457
+ const response = await this.request("/api/agent/projects", {
458
+ method: "POST",
459
+ body: JSON.stringify(data)
460
+ });
461
+ const project = response.data;
462
+ logger.success("api", `Created project: ${project.name} (${project.id})`);
463
+ return project;
464
+ }
435
465
  };
436
466
  var apiService = new ApiServiceImpl();
437
467
 
@@ -5,13 +5,13 @@ import {
5
5
  projectService,
6
6
  removePidFile,
7
7
  writePidFile
8
- } from "../chunk-TCWMYJPP.js";
8
+ } from "../chunk-EVWSJ3RU.js";
9
9
  import {
10
10
  apiService,
11
11
  authService,
12
12
  config,
13
13
  logger
14
- } from "../chunk-CCM25AHM.js";
14
+ } from "../chunk-NMOUYAW2.js";
15
15
 
16
16
  // src/daemon/runner.ts
17
17
  import "dotenv/config";
@@ -27,8 +27,12 @@ var WebSocketServiceImpl = class {
27
27
  isManualDisconnect = false;
28
28
  // Event handlers
29
29
  commitApprovedHandlers = [];
30
+ commitPendingHandlers = [];
31
+ suggestionCreatedHandlers = [];
30
32
  projectUpdatedHandlers = [];
31
33
  disconnectHandlers = [];
34
+ agentDisconnectedHandlers = [];
35
+ syncRequestedHandlers = [];
32
36
  async connect() {
33
37
  try {
34
38
  const token = await authService.getToken();
@@ -108,12 +112,24 @@ var WebSocketServiceImpl = class {
108
112
  onCommitApproved(handler) {
109
113
  this.commitApprovedHandlers.push(handler);
110
114
  }
115
+ onCommitPending(handler) {
116
+ this.commitPendingHandlers.push(handler);
117
+ }
118
+ onSuggestionCreated(handler) {
119
+ this.suggestionCreatedHandlers.push(handler);
120
+ }
111
121
  onProjectUpdated(handler) {
112
122
  this.projectUpdatedHandlers.push(handler);
113
123
  }
114
124
  onDisconnect(handler) {
115
125
  this.disconnectHandlers.push(handler);
116
126
  }
127
+ onAgentDisconnected(handler) {
128
+ this.agentDisconnectedHandlers.push(handler);
129
+ }
130
+ onSyncRequested(handler) {
131
+ this.syncRequestedHandlers.push(handler);
132
+ }
117
133
  sendMessage(message) {
118
134
  if (!this.isConnected()) {
119
135
  logger.warn("websocket", "Cannot send message: not connected");
@@ -139,6 +155,18 @@ var WebSocketServiceImpl = class {
139
155
  this.commitApprovedHandlers.forEach((handler) => handler(commit, project));
140
156
  return;
141
157
  }
158
+ if (message.event === "commit.pending") {
159
+ const { pendingCommit } = message.data;
160
+ logger.info("websocket", `Commit pending: ${pendingCommit.id}`);
161
+ this.commitPendingHandlers.forEach((handler) => handler(pendingCommit));
162
+ return;
163
+ }
164
+ if (message.event === "suggestion.created") {
165
+ const { suggestion } = message.data;
166
+ logger.info("websocket", `Suggestion created: ${suggestion.id}`);
167
+ this.suggestionCreatedHandlers.forEach((handler) => handler(suggestion));
168
+ return;
169
+ }
142
170
  if (message.event === "project.updated") {
143
171
  const { project } = message.data;
144
172
  logger.info("websocket", `Project updated: ${project.id}`);
@@ -148,6 +176,13 @@ var WebSocketServiceImpl = class {
148
176
  if (message.event === "sync.requested") {
149
177
  const { projectId } = message.data;
150
178
  logger.info("websocket", `Sync requested for project: ${projectId}`);
179
+ this.syncRequestedHandlers.forEach((handler) => handler(projectId));
180
+ return;
181
+ }
182
+ if (message.event === "agent.disconnected") {
183
+ const reason = message.data?.reason || "Server requested disconnect";
184
+ logger.warn("websocket", `Agent disconnected by server: ${reason}`);
185
+ this.agentDisconnectedHandlers.forEach((handler) => handler(reason));
151
186
  return;
152
187
  }
153
188
  logger.debug("websocket", `Unhandled event: ${message.event}`);
@@ -358,6 +393,19 @@ var FileWatcher = class {
358
393
  addProject(projectPath, projectId) {
359
394
  this.watchProject(projectPath, projectId);
360
395
  }
396
+ /**
397
+ * Sync a project by ID (called when server requests a sync)
398
+ */
399
+ async syncProjectById(projectId) {
400
+ const linkedProjects = projectService.getAllLinkedProjects();
401
+ for (const [projectPath, linkedProject] of Object.entries(linkedProjects)) {
402
+ if (linkedProject.projectId === projectId) {
403
+ await this.performSync(projectPath, projectId);
404
+ return;
405
+ }
406
+ }
407
+ logger.warn("watcher", `Cannot sync: project ${projectId} not found in linked projects`);
408
+ }
361
409
  /**
362
410
  * Remove a project from watching (called when a project is unlinked)
363
411
  */
@@ -377,9 +425,32 @@ var FileWatcher = class {
377
425
  }
378
426
  };
379
427
 
428
+ // src/utils/notify.ts
429
+ import notifier from "node-notifier";
430
+ function notify(options) {
431
+ try {
432
+ notifier.notify({
433
+ title: options.title,
434
+ message: options.message,
435
+ open: options.open,
436
+ sound: true,
437
+ wait: false,
438
+ appID: "Stint Agent"
439
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
440
+ }, (error) => {
441
+ if (error) {
442
+ logger.error("notify", "Failed to send notification", error);
443
+ }
444
+ });
445
+ } catch (error) {
446
+ logger.error("notify", "Failed to send notification", error);
447
+ }
448
+ }
449
+
380
450
  // src/daemon/index.ts
381
451
  var heartbeatInterval = null;
382
452
  var isShuttingDown = false;
453
+ var shutdownReason;
383
454
  var fileWatcher = new FileWatcher();
384
455
  async function startDaemon() {
385
456
  logger.info("daemon", "Starting daemon...");
@@ -406,6 +477,30 @@ async function startDaemon() {
406
477
  websocketService.onDisconnect(() => {
407
478
  logger.warn("daemon", "WebSocket disconnected, will attempt to reconnect");
408
479
  });
480
+ websocketService.onAgentDisconnected(async (reason) => {
481
+ logger.warn("daemon", `Server disconnected agent: ${reason}`);
482
+ logger.info("daemon", "Initiating graceful shutdown...");
483
+ await shutdown(`Server: ${reason}`);
484
+ process.exit(0);
485
+ });
486
+ websocketService.onSuggestionCreated((suggestion) => {
487
+ logger.info("daemon", `Suggestion created: ${suggestion.title} (${suggestion.priority})`);
488
+ notify({
489
+ title: "New Suggestion",
490
+ message: `${suggestion.title}
491
+ Priority: ${suggestion.priority}`,
492
+ open: `https://stint.codes/projects/${suggestion.project_id}/suggestions/${suggestion.id}`
493
+ // Hypothetical URL structure
494
+ });
495
+ });
496
+ websocketService.onSyncRequested(async (projectId) => {
497
+ logger.info("daemon", `Server requested sync for project: ${projectId}`);
498
+ try {
499
+ await fileWatcher.syncProjectById(projectId);
500
+ } catch (error) {
501
+ logger.error("daemon", `Failed to sync project ${projectId}`, error);
502
+ }
503
+ });
409
504
  setupSignalHandlers();
410
505
  startHeartbeat();
411
506
  fileWatcher.start();
@@ -443,15 +538,16 @@ function setupSignalHandlers() {
443
538
  signals.forEach((signal) => {
444
539
  process.on(signal, async () => {
445
540
  logger.info("daemon", `Received ${signal}, shutting down...`);
446
- await shutdown();
541
+ await shutdown(`Signal: ${signal}`);
447
542
  process.exit(0);
448
543
  });
449
544
  });
450
545
  logger.info("daemon", "Signal handlers registered");
451
546
  }
452
- async function shutdown() {
547
+ async function shutdown(reason) {
453
548
  if (isShuttingDown) return;
454
549
  isShuttingDown = true;
550
+ shutdownReason = reason;
455
551
  logger.info("daemon", "Shutting down daemon...");
456
552
  stopHeartbeat();
457
553
  try {
@@ -467,7 +563,7 @@ async function shutdown() {
467
563
  logger.error("daemon", "Failed to disconnect from WebSocket", error);
468
564
  }
469
565
  try {
470
- await apiService.disconnect();
566
+ await apiService.disconnect(shutdownReason);
471
567
  logger.info("daemon", "Disconnected from API");
472
568
  } catch (error) {
473
569
  logger.error("daemon", "Failed to disconnect from API", error);
package/dist/index.js CHANGED
@@ -8,13 +8,13 @@ import {
8
8
  projectService,
9
9
  spawnDetached,
10
10
  validatePidFile
11
- } from "./chunk-TCWMYJPP.js";
11
+ } from "./chunk-EVWSJ3RU.js";
12
12
  import {
13
13
  apiService,
14
14
  authService,
15
15
  config,
16
16
  logger
17
- } from "./chunk-CCM25AHM.js";
17
+ } from "./chunk-NMOUYAW2.js";
18
18
 
19
19
  // src/index.ts
20
20
  import "dotenv/config";
@@ -44,82 +44,245 @@ function startCallbackServer(expectedState, timeoutMs = 5 * 60 * 1e3) {
44
44
  callbackReject = reject2;
45
45
  });
46
46
  server.on("request", (req, res) => {
47
+ res.setHeader("Access-Control-Allow-Origin", "*");
48
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
49
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-XSRF-TOKEN, X-Requested-With, X-Inertia, X-Inertia-Version");
50
+ res.setHeader("Access-Control-Expose-Headers", "X-Inertia-Location");
51
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
52
+ if (req.method === "OPTIONS") {
53
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
54
+ res.writeHead(200);
55
+ res.end();
56
+ return;
57
+ }
47
58
  if (req.url?.startsWith("/auth/callback")) {
48
59
  try {
49
60
  const url = new URL(req.url, `http://${req.headers.host}`);
50
61
  const token = url.searchParams.get("token");
51
62
  const state = url.searchParams.get("state");
52
63
  const error = url.searchParams.get("error");
64
+ const next = url.searchParams.get("next");
65
+ const accept = req.headers.accept || "";
66
+ const isInertia = req.headers["x-inertia"] === "true";
67
+ const isJsonRequest = accept.includes("application/json") || isInertia;
53
68
  if (error) {
54
69
  const errorDescription = url.searchParams.get("error_description") || error;
55
- const escapedError = escapeHtml(errorDescription);
56
- res.writeHead(400, { "Content-Type": "text/html" });
57
- res.end(`
58
- <html>
59
- <head><title>Authentication Failed</title></head>
60
- <body>
61
- <h1>Authentication Failed</h1>
62
- <p>${escapedError}</p>
63
- <p>You can close this window.</p>
64
- </body>
65
- </html>
66
- `);
70
+ if (isJsonRequest) {
71
+ res.writeHead(400, { "Content-Type": "application/json" });
72
+ res.end(JSON.stringify({
73
+ status: "error",
74
+ error,
75
+ message: errorDescription
76
+ }));
77
+ } else {
78
+ const escapedError = escapeHtml(errorDescription);
79
+ res.writeHead(400, { "Content-Type": "text/html" });
80
+ res.end(`
81
+ <html>
82
+ <head><title>Authentication Failed</title></head>
83
+ <body>
84
+ <h1>Authentication Failed</h1>
85
+ <p>${escapedError}</p>
86
+ <p>You can close this window.</p>
87
+ </body>
88
+ </html>
89
+ `);
90
+ }
67
91
  callbackReject(new Error(`OAuth error: ${errorDescription}`));
68
92
  return;
69
93
  }
70
94
  if (state !== expectedState) {
71
- res.writeHead(400, { "Content-Type": "text/html" });
72
- res.end(`
73
- <html>
74
- <head><title>Authentication Failed</title></head>
75
- <body>
76
- <h1>Authentication Failed</h1>
77
- <p>Invalid state parameter. Security validation failed.</p>
78
- <p>You can close this window.</p>
79
- </body>
80
- </html>
81
- `);
95
+ if (isJsonRequest) {
96
+ res.writeHead(400, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify({
98
+ status: "error",
99
+ error: "invalid_state",
100
+ message: "State parameter mismatch"
101
+ }));
102
+ } else {
103
+ res.writeHead(400, { "Content-Type": "text/html" });
104
+ res.end(`
105
+ <html>
106
+ <head><title>Authentication Failed</title></head>
107
+ <body>
108
+ <h1>Authentication Failed</h1>
109
+ <p>Invalid state parameter. Security validation failed.</p>
110
+ <p>You can close this window.</p>
111
+ </body>
112
+ </html>
113
+ `);
114
+ }
82
115
  callbackReject(new Error("State parameter mismatch"));
83
116
  return;
84
117
  }
85
118
  if (!token) {
86
- res.writeHead(400, { "Content-Type": "text/html" });
87
- res.end(`
88
- <html>
89
- <head><title>Authentication Failed</title></head>
90
- <body>
91
- <h1>Authentication Failed</h1>
92
- <p>No token provided in callback.</p>
93
- <p>You can close this window.</p>
94
- </body>
95
- </html>
96
- `);
119
+ if (isJsonRequest) {
120
+ res.writeHead(400, { "Content-Type": "application/json" });
121
+ res.end(JSON.stringify({
122
+ status: "error",
123
+ error: "missing_token",
124
+ message: "No token provided"
125
+ }));
126
+ } else {
127
+ res.writeHead(400, { "Content-Type": "text/html" });
128
+ res.end(`
129
+ <html>
130
+ <head><title>Authentication Failed</title></head>
131
+ <body>
132
+ <h1>Authentication Failed</h1>
133
+ <p>No token provided in callback.</p>
134
+ <p>You can close this window.</p>
135
+ </body>
136
+ </html>
137
+ `);
138
+ }
97
139
  callbackReject(new Error("No token in callback"));
98
140
  return;
99
141
  }
100
- res.writeHead(200, { "Content-Type": "text/html" });
101
- res.end(`
142
+ if (isInertia && next) {
143
+ res.writeHead(409, { "X-Inertia-Location": next });
144
+ res.end();
145
+ } else if (isJsonRequest) {
146
+ res.writeHead(200, { "Content-Type": "application/json" });
147
+ res.end(JSON.stringify({
148
+ status: "success",
149
+ message: "Authentication successful",
150
+ user: "Authenticated",
151
+ next: next || void 0
152
+ }));
153
+ } else if (next) {
154
+ res.writeHead(302, { "Location": next });
155
+ res.end();
156
+ } else {
157
+ res.writeHead(200, { "Content-Type": "text/html" });
158
+ res.end(`
159
+ <!DOCTYPE html>
102
160
  <html>
103
- <head><title>Authentication Successful</title></head>
161
+ <head>
162
+ <title>Stint - Authentication Successful</title>
163
+ <style>
164
+ body {
165
+ background-color: #0f172a;
166
+ color: #e2e8f0;
167
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
168
+ display: flex;
169
+ justify-content: center;
170
+ align-items: center;
171
+ height: 100vh;
172
+ margin: 0;
173
+ }
174
+ .container {
175
+ background-color: #1e293b;
176
+ padding: 2.5rem;
177
+ border-radius: 1rem;
178
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
179
+ text-align: center;
180
+ max-width: 28rem;
181
+ width: 100%;
182
+ border: 1px solid #334155;
183
+ }
184
+ h1 {
185
+ color: #38bdf8;
186
+ margin-top: 0;
187
+ font-size: 1.5rem;
188
+ margin-bottom: 1rem;
189
+ }
190
+ p {
191
+ color: #94a3b8;
192
+ line-height: 1.5;
193
+ margin-bottom: 1.5rem;
194
+ }
195
+ .icon {
196
+ color: #22c55e;
197
+ width: 3rem;
198
+ height: 3rem;
199
+ margin-bottom: 1.5rem;
200
+ }
201
+ .btn {
202
+ background-color: #38bdf8;
203
+ color: #0f172a;
204
+ padding: 0.75rem 1.5rem;
205
+ border-radius: 0.5rem;
206
+ text-decoration: none;
207
+ font-weight: 600;
208
+ display: inline-block;
209
+ transition: background-color 0.2s;
210
+ border: none;
211
+ cursor: pointer;
212
+ }
213
+ .btn:hover {
214
+ background-color: #0ea5e9;
215
+ }
216
+ </style>
217
+ </head>
104
218
  <body>
105
- <h1>Authentication Successful!</h1>
106
- <p>You have been successfully authenticated. You can close this window.</p>
219
+ <div class="container">
220
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
221
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
222
+ </svg>
223
+ <h1>Authentication Successful</h1>
224
+ <p>You have successfully logged in to Stint. You can now close this window and return to your terminal.</p>
225
+ <button onclick="window.close()" class="btn">Close Window</button>
226
+ </div>
227
+ <script>
228
+ // Attempt to close the window automatically after 3 seconds
229
+ setTimeout(function() {
230
+ window.close();
231
+ }, 3000);
232
+ </script>
107
233
  </body>
108
234
  </html>
109
- `);
235
+ `);
236
+ }
110
237
  callbackResolve(token);
111
238
  } catch (error) {
112
- res.writeHead(500, { "Content-Type": "text/html" });
113
- res.end(`
114
- <html>
115
- <head><title>Error</title></head>
116
- <body>
117
- <h1>Error</h1>
118
- <p>An error occurred processing the callback.</p>
119
- <p>You can close this window.</p>
120
- </body>
121
- </html>
122
- `);
239
+ const isJsonRequest = req.headers.accept?.includes("application/json");
240
+ if (isJsonRequest) {
241
+ res.writeHead(500, { "Content-Type": "application/json" });
242
+ res.end(JSON.stringify({
243
+ status: "error",
244
+ message: "Internal server error processing callback"
245
+ }));
246
+ } else {
247
+ res.writeHead(500, { "Content-Type": "text/html" });
248
+ res.end(`
249
+ <!DOCTYPE html>
250
+ <html>
251
+ <head>
252
+ <title>Stint - Error</title>
253
+ <style>
254
+ body {
255
+ background-color: #0f172a;
256
+ color: #e2e8f0;
257
+ font-family: sans-serif;
258
+ display: flex;
259
+ justify-content: center;
260
+ align-items: center;
261
+ height: 100vh;
262
+ margin: 0;
263
+ }
264
+ .container {
265
+ background-color: #1e293b;
266
+ padding: 2rem;
267
+ border-radius: 0.5rem;
268
+ text-align: center;
269
+ max-width: 24rem;
270
+ border: 1px solid #334155;
271
+ }
272
+ h1 { color: #f43f5e; margin-top: 0; }
273
+ p { color: #94a3b8; }
274
+ </style>
275
+ </head>
276
+ <body>
277
+ <div class="container">
278
+ <h1>Authentication Error</h1>
279
+ <p>An error occurred processing the callback.</p>
280
+ <p>Please try logging in again.</p>
281
+ </div>
282
+ </body>
283
+ </html>
284
+ `);
285
+ }
123
286
  callbackReject(error);
124
287
  }
125
288
  } else {
@@ -134,7 +297,7 @@ function startCallbackServer(expectedState, timeoutMs = 5 * 60 * 1e3) {
134
297
  reject(error);
135
298
  }
136
299
  });
137
- server.listen(0, "127.0.0.1", () => {
300
+ server.listen(0, "localhost", () => {
138
301
  const address = server.address();
139
302
  const port = address.port;
140
303
  timeout = setTimeout(() => {
@@ -144,7 +307,7 @@ function startCallbackServer(expectedState, timeoutMs = 5 * 60 * 1e3) {
144
307
  callbackPromise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout)).finally(() => {
145
308
  setTimeout(() => {
146
309
  server.close();
147
- }, 100);
310
+ }, 500);
148
311
  });
149
312
  resolve({
150
313
  server,
@@ -283,9 +446,10 @@ function registerWhoamiCommand(program2) {
283
446
  }
284
447
 
285
448
  // src/commands/link.ts
286
- import { select } from "@inquirer/prompts";
449
+ import { select, input } from "@inquirer/prompts";
287
450
  import ora4 from "ora";
288
451
  import chalk4 from "chalk";
452
+ import path from "path";
289
453
  import process2 from "process";
290
454
  function registerLinkCommand(program2) {
291
455
  program2.command("link").description("Link current directory to a Stint project").action(async () => {
@@ -317,17 +481,59 @@ function registerLinkCommand(program2) {
317
481
  return;
318
482
  }
319
483
  spinner.succeed("Ready to link");
320
- const selectedProjectId = await select({
484
+ const choices = projects.map((project) => ({
485
+ name: project.name,
486
+ value: project.id,
487
+ description: `ID: ${project.id}`
488
+ }));
489
+ const CREATE_NEW_PROJECT = "create-new-project";
490
+ choices.push({
491
+ name: "\u2795 Create new project",
492
+ value: CREATE_NEW_PROJECT,
493
+ description: "Create a new project on Stint"
494
+ });
495
+ const selectedAction = await select({
321
496
  message: "Select a project to link:",
322
- choices: projects.map((project) => ({
323
- name: `${project.name}${project.description ? ` - ${project.description}` : ""}`,
324
- value: project.id,
325
- description: `ID: ${project.id}`
326
- }))
497
+ choices
327
498
  });
499
+ let selectedProjectId = selectedAction;
500
+ let selectedProject = projects.find((p) => p.id === selectedProjectId);
501
+ if (selectedAction === CREATE_NEW_PROJECT) {
502
+ const name = await input({
503
+ message: "Project name:",
504
+ default: path.basename(cwd),
505
+ validate: (input2) => input2.trim().length > 0 || "Project name is required"
506
+ });
507
+ const description = await input({
508
+ message: "Description (optional):"
509
+ });
510
+ const createSpinner = ora4("Creating project...").start();
511
+ try {
512
+ let repoInfo = null;
513
+ if (isRepo) {
514
+ try {
515
+ repoInfo = await gitService.getRepoInfo(cwd);
516
+ } catch (e) {
517
+ logger.warn("link", `Failed to get repo info for creation metadata: ${e.message}`);
518
+ }
519
+ }
520
+ const newProject = await apiService.createProject({
521
+ name,
522
+ description: description || void 0,
523
+ repo_path: cwd,
524
+ remote_url: repoInfo?.remoteUrl || void 0,
525
+ default_branch: repoInfo?.currentBranch || void 0
526
+ });
527
+ createSpinner.succeed("Project created successfully!");
528
+ selectedProjectId = newProject.id;
529
+ selectedProject = newProject;
530
+ } catch (error) {
531
+ createSpinner.fail("Failed to create project");
532
+ throw error;
533
+ }
534
+ }
328
535
  const linkSpinner = ora4("Linking project...").start();
329
536
  await projectService.linkProject(cwd, selectedProjectId);
330
- const selectedProject = projects.find((p) => p.id === selectedProjectId);
331
537
  linkSpinner.succeed("Project linked successfully!");
332
538
  console.log(chalk4.green(`
333
539
  \u2713 Linked to ${chalk4.bold(selectedProject?.name || selectedProjectId)}`));
@@ -400,6 +606,8 @@ Directory ${cwd} is no longer linked.
400
606
  import ora6 from "ora";
401
607
  import chalk6 from "chalk";
402
608
  import process4 from "process";
609
+ import path2 from "path";
610
+ import os from "os";
403
611
  function registerStatusCommand(program2) {
404
612
  program2.command("status").description("Show linked project and connection status").action(async () => {
405
613
  const spinner = ora6("Gathering status...").start();
@@ -459,7 +667,15 @@ function registerStatusCommand(program2) {
459
667
  }
460
668
  console.log(chalk6.blue("\n\u2699\uFE0F Daemon:"));
461
669
  console.log(chalk6.gray("\u2500".repeat(50)));
462
- console.log(`${chalk6.bold("Status:")} ${chalk6.gray("Not running (Phase 3)")}`);
670
+ const { valid, pid } = validatePidFile();
671
+ if (valid && pid) {
672
+ console.log(`${chalk6.bold("Status:")} ${chalk6.green("\u2713 Running")}`);
673
+ console.log(`${chalk6.bold("PID:")} ${pid}`);
674
+ console.log(`${chalk6.bold("Logs:")} ${path2.join(os.homedir(), ".config", "stint", "logs", "daemon.log")}`);
675
+ } else {
676
+ console.log(`${chalk6.bold("Status:")} ${chalk6.yellow("Not running")}`);
677
+ console.log(chalk6.gray('Run "stint daemon start" to start the background agent.'));
678
+ }
463
679
  console.log();
464
680
  logger.info("status", "Status command executed");
465
681
  } catch (error) {
@@ -517,8 +733,8 @@ function registerSyncCommand(program2) {
517
733
  import ora8 from "ora";
518
734
  import chalk8 from "chalk";
519
735
  import fs from "fs";
520
- import path from "path";
521
- import os from "os";
736
+ import path3 from "path";
737
+ import os2 from "os";
522
738
  import { fileURLToPath } from "url";
523
739
  import { dirname } from "path";
524
740
  var __filename = fileURLToPath(import.meta.url);
@@ -544,7 +760,7 @@ function registerDaemonCommands(program2) {
544
760
  console.log(chalk8.gray('Run "stint login" first.\n'));
545
761
  process.exit(1);
546
762
  }
547
- const runnerPath = path.join(__dirname, "daemon", "runner.js");
763
+ const runnerPath = path3.join(__dirname, "daemon", "runner.js");
548
764
  if (!fs.existsSync(runnerPath)) {
549
765
  throw new Error(`Daemon runner not found at ${runnerPath}`);
550
766
  }
@@ -558,7 +774,7 @@ function registerDaemonCommands(program2) {
558
774
  console.log(chalk8.green(`
559
775
  \u2713 Daemon is running in the background`));
560
776
  console.log(chalk8.gray(`PID: ${daemonPid}`));
561
- console.log(chalk8.gray(`Logs: ${path.join(os.homedir(), ".config", "stint", "logs", "daemon.log")}
777
+ console.log(chalk8.gray(`Logs: ${path3.join(os2.homedir(), ".config", "stint", "logs", "daemon.log")}
562
778
  `));
563
779
  logger.success("daemon", `Daemon started with PID ${daemonPid}`);
564
780
  } catch (error) {
@@ -619,7 +835,7 @@ function registerDaemonCommands(program2) {
619
835
  console.log(`${chalk8.bold("Status:")} ${chalk8.green("\u2713 Running")}`);
620
836
  console.log(`${chalk8.bold("PID:")} ${pid}`);
621
837
  console.log(`${chalk8.bold("PID File:")} ${getPidFilePath()}`);
622
- console.log(`${chalk8.bold("Logs:")} ${path.join(os.homedir(), ".config", "stint", "logs", "daemon.log")}`);
838
+ console.log(`${chalk8.bold("Logs:")} ${path3.join(os2.homedir(), ".config", "stint", "logs", "daemon.log")}`);
623
839
  } else {
624
840
  console.log(`${chalk8.bold("Status:")} ${chalk8.yellow("Not running")}`);
625
841
  console.log(chalk8.gray('Run "stint daemon start" to start the daemon.'));
@@ -637,7 +853,7 @@ function registerDaemonCommands(program2) {
637
853
  });
638
854
  daemon.command("logs").description("Tail daemon logs").option("-n, --lines <number>", "Number of lines to show", "50").action(async (options) => {
639
855
  try {
640
- const logFile = path.join(os.homedir(), ".config", "stint", "logs", "daemon.log");
856
+ const logFile = path3.join(os2.homedir(), ".config", "stint", "logs", "daemon.log");
641
857
  if (!fs.existsSync(logFile)) {
642
858
  console.log(chalk8.yellow("\n\u26A0 No daemon logs found."));
643
859
  console.log(chalk8.gray("The daemon has not been started yet.\n"));
@@ -693,7 +909,7 @@ Log file: ${logFile}`));
693
909
  if (!user) {
694
910
  throw new Error("Not authenticated");
695
911
  }
696
- const runnerPath = path.join(__dirname, "daemon", "runner.js");
912
+ const runnerPath = path3.join(__dirname, "daemon", "runner.js");
697
913
  const daemonPid = spawnDetached("node", [runnerPath]);
698
914
  await new Promise((resolve) => setTimeout(resolve, 1e3));
699
915
  if (!isProcessRunning(daemonPid)) {
@@ -714,6 +930,7 @@ Log file: ${logFile}`));
714
930
  // src/commands/commit.ts
715
931
  import ora9 from "ora";
716
932
  import chalk9 from "chalk";
933
+ import { confirm as confirm2 } from "@inquirer/prompts";
717
934
  import process6 from "process";
718
935
  function registerCommitCommands(program2) {
719
936
  program2.command("commits").description("List pending commits for the current project").action(async () => {
@@ -759,7 +976,7 @@ function registerCommitCommands(program2) {
759
976
  }
760
977
  });
761
978
  program2.command("commit <id>").description("Execute a specific pending commit").action(async (id) => {
762
- const spinner = ora9("Executing commit...").start();
979
+ const spinner = ora9("Checking repository status...").start();
763
980
  try {
764
981
  const cwd = process6.cwd();
765
982
  const linkedProject = projectService.getLinkedProject(cwd);
@@ -769,6 +986,14 @@ function registerCommitCommands(program2) {
769
986
  console.log(chalk9.gray('Run "stint link" first to link this directory.\n'));
770
987
  process6.exit(1);
771
988
  }
989
+ const status = await gitService.getStatus(cwd);
990
+ if (status.staged.length === 0) {
991
+ spinner.fail("No staged changes");
992
+ console.log(chalk9.yellow("\n\u26A0 No staged changes detected."));
993
+ console.log(chalk9.gray("Please stage the files you want to commit first."));
994
+ console.log(chalk9.gray(" git add <files>\n"));
995
+ process6.exit(1);
996
+ }
772
997
  spinner.text = "Fetching commit details...";
773
998
  const commits = await apiService.getPendingCommits(linkedProject.projectId);
774
999
  const commit = commits.find((c) => c.id.startsWith(id));
@@ -780,7 +1005,24 @@ function registerCommitCommands(program2) {
780
1005
  console.log(chalk9.gray('Run "stint commits" to see available commits.\n'));
781
1006
  process6.exit(1);
782
1007
  }
783
- spinner.text = `Executing commit: ${commit.message}`;
1008
+ spinner.stop();
1009
+ console.log(chalk9.blue("\n\u{1F4CB} Staged changes to commit:"));
1010
+ console.log(chalk9.gray("\u2500".repeat(40)));
1011
+ status.staged.forEach((file) => {
1012
+ console.log(chalk9.green(` + ${file}`));
1013
+ });
1014
+ console.log();
1015
+ console.log(`${chalk9.bold("Message:")} ${commit.message}`);
1016
+ console.log();
1017
+ const confirmed = await confirm2({
1018
+ message: "Are you sure you want to commit these changes?",
1019
+ default: true
1020
+ });
1021
+ if (!confirmed) {
1022
+ console.log(chalk9.yellow("\nCommit cancelled.\n"));
1023
+ return;
1024
+ }
1025
+ const execSpinner = ora9("Executing commit...").start();
784
1026
  const project = {
785
1027
  id: linkedProject.projectId,
786
1028
  name: "Current Project",
@@ -789,7 +1031,7 @@ function registerCommitCommands(program2) {
789
1031
  updatedAt: ""
790
1032
  };
791
1033
  const sha = await commitQueue.executeCommit(commit, project);
792
- spinner.succeed("Commit executed successfully!");
1034
+ execSpinner.succeed("Commit executed successfully!");
793
1035
  console.log(chalk9.green("\n\u2713 Commit executed"));
794
1036
  console.log(chalk9.gray("\u2500".repeat(50)));
795
1037
  console.log(`${chalk9.bold("Commit ID:")} ${commit.id}`);
@@ -798,7 +1040,9 @@ function registerCommitCommands(program2) {
798
1040
  console.log();
799
1041
  logger.success("commit", `Executed commit ${commit.id} -> ${sha}`);
800
1042
  } catch (error) {
801
- spinner.fail("Commit execution failed");
1043
+ if (ora9().isSpinning) {
1044
+ ora9().fail("Commit execution failed");
1045
+ }
802
1046
  logger.error("commit", "Failed to execute commit", error);
803
1047
  console.error(chalk9.red(`
804
1048
  \u2716 Error: ${error.message}
@@ -818,8 +1062,9 @@ function getTimeAgo(date) {
818
1062
  }
819
1063
 
820
1064
  // src/index.ts
1065
+ var AGENT_VERSION = "1.0.3";
821
1066
  var program = new Command();
822
- program.name("stint").description("Stint Agent - Local daemon for Stint Project Assistant").version("1.0.0").addHelpText("after", `
1067
+ program.name("stint").description("Stint Agent - Local daemon for Stint Project Assistant").version(AGENT_VERSION, "-V, --version", "output the current version").addHelpText("after", `
823
1068
  ${chalk10.bold("Examples:")}
824
1069
  ${chalk10.cyan("$")} stint login ${chalk10.gray("# Authenticate with Stint")}
825
1070
  ${chalk10.cyan("$")} stint link ${chalk10.gray("# Link current directory to a project")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gowelle/stint-agent",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Local agent for Stint - Project Assistant",
5
5
  "author": "Gowelle John <gowelle.john@icloud.com>",
6
6
  "license": "MIT",
@@ -36,12 +36,13 @@
36
36
  "prepublishOnly": "npm run build"
37
37
  },
38
38
  "dependencies": {
39
- "@inquirer/prompts": "^5.0.0",
39
+ "@inquirer/prompts": "^5.5.0",
40
40
  "chalk": "^5.3.0",
41
41
  "commander": "^12.0.0",
42
42
  "conf": "^12.0.0",
43
43
  "dotenv": "^17.2.3",
44
44
  "node-fetch": "^3.3.2",
45
+ "node-notifier": "^10.0.1",
45
46
  "open": "^10.0.0",
46
47
  "ora": "^8.0.1",
47
48
  "simple-git": "^3.22.0",
@@ -49,6 +50,7 @@
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/node": "^20.11.0",
53
+ "@types/node-notifier": "^8.0.5",
52
54
  "@types/ws": "^8.5.10",
53
55
  "@typescript-eslint/eslint-plugin": "^8.50.0",
54
56
  "@typescript-eslint/parser": "^8.50.0",
@@ -60,4 +62,4 @@
60
62
  "engines": {
61
63
  "node": ">=20.0.0"
62
64
  }
63
- }
65
+ }