@northflare/runner 0.0.1

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.
Files changed (154) hide show
  1. package/DEBUG_LOGGING.md +60 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION_PLAN.md +52 -0
  4. package/README.md +220 -0
  5. package/SDK_IMPLEMENTATION_GUIDE.md +1036 -0
  6. package/bin/northflare-runner +367 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/coverage-final.json +12 -0
  10. package/coverage/favicon.png +0 -0
  11. package/coverage/index.html +176 -0
  12. package/coverage/lib/index.html +116 -0
  13. package/coverage/lib/preload-script.js.html +964 -0
  14. package/coverage/prettify.css +1 -0
  15. package/coverage/prettify.js +2 -0
  16. package/coverage/sort-arrow-sprite.png +0 -0
  17. package/coverage/sorter.js +196 -0
  18. package/coverage/src/collections/index.html +116 -0
  19. package/coverage/src/collections/runner-messages.ts.html +312 -0
  20. package/coverage/src/components/claude-manager.ts.html +1290 -0
  21. package/coverage/src/components/index.html +146 -0
  22. package/coverage/src/components/message-handler.ts.html +730 -0
  23. package/coverage/src/components/repository-manager.ts.html +841 -0
  24. package/coverage/src/index.html +131 -0
  25. package/coverage/src/index.ts.html +448 -0
  26. package/coverage/src/runner.ts.html +1239 -0
  27. package/coverage/src/utils/config.ts.html +780 -0
  28. package/coverage/src/utils/console.ts.html +121 -0
  29. package/coverage/src/utils/index.html +161 -0
  30. package/coverage/src/utils/logger.ts.html +475 -0
  31. package/coverage/src/utils/status-line.ts.html +445 -0
  32. package/dist/collections/runner-messages.d.ts +52 -0
  33. package/dist/collections/runner-messages.d.ts.map +1 -0
  34. package/dist/collections/runner-messages.js +161 -0
  35. package/dist/collections/runner-messages.js.map +1 -0
  36. package/dist/components/claude-manager.d.ts +39 -0
  37. package/dist/components/claude-manager.d.ts.map +1 -0
  38. package/dist/components/claude-manager.js +783 -0
  39. package/dist/components/claude-manager.js.map +1 -0
  40. package/dist/components/claude-sdk-manager.d.ts +47 -0
  41. package/dist/components/claude-sdk-manager.d.ts.map +1 -0
  42. package/dist/components/claude-sdk-manager.js +1088 -0
  43. package/dist/components/claude-sdk-manager.js.map +1 -0
  44. package/dist/components/enhanced-repository-manager.d.ts +134 -0
  45. package/dist/components/enhanced-repository-manager.d.ts.map +1 -0
  46. package/dist/components/enhanced-repository-manager.js +602 -0
  47. package/dist/components/enhanced-repository-manager.js.map +1 -0
  48. package/dist/components/message-handler-sse.d.ts +46 -0
  49. package/dist/components/message-handler-sse.d.ts.map +1 -0
  50. package/dist/components/message-handler-sse.js +734 -0
  51. package/dist/components/message-handler-sse.js.map +1 -0
  52. package/dist/components/message-handler.d.ts +35 -0
  53. package/dist/components/message-handler.d.ts.map +1 -0
  54. package/dist/components/message-handler.js +689 -0
  55. package/dist/components/message-handler.js.map +1 -0
  56. package/dist/components/repository-manager.d.ts +51 -0
  57. package/dist/components/repository-manager.d.ts.map +1 -0
  58. package/dist/components/repository-manager.js +295 -0
  59. package/dist/components/repository-manager.js.map +1 -0
  60. package/dist/index.d.ts +9 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +166 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/runner-sse.d.ts +57 -0
  65. package/dist/runner-sse.d.ts.map +1 -0
  66. package/dist/runner-sse.js +698 -0
  67. package/dist/runner-sse.js.map +1 -0
  68. package/dist/runner.d.ts +51 -0
  69. package/dist/runner.d.ts.map +1 -0
  70. package/dist/runner.js +530 -0
  71. package/dist/runner.js.map +1 -0
  72. package/dist/services/RunnerAPIClient.d.ts +30 -0
  73. package/dist/services/RunnerAPIClient.d.ts.map +1 -0
  74. package/dist/services/RunnerAPIClient.js +112 -0
  75. package/dist/services/RunnerAPIClient.js.map +1 -0
  76. package/dist/services/SSEClient.d.ts +60 -0
  77. package/dist/services/SSEClient.d.ts.map +1 -0
  78. package/dist/services/SSEClient.js +204 -0
  79. package/dist/services/SSEClient.js.map +1 -0
  80. package/dist/types/claude.d.ts +45 -0
  81. package/dist/types/claude.d.ts.map +1 -0
  82. package/dist/types/claude.js +6 -0
  83. package/dist/types/claude.js.map +1 -0
  84. package/dist/types/index.d.ts +47 -0
  85. package/dist/types/index.d.ts.map +1 -0
  86. package/dist/types/index.js +23 -0
  87. package/dist/types/index.js.map +1 -0
  88. package/dist/types/messages.d.ts +31 -0
  89. package/dist/types/messages.d.ts.map +1 -0
  90. package/dist/types/messages.js +6 -0
  91. package/dist/types/messages.js.map +1 -0
  92. package/dist/types/runner-interface.d.ts +24 -0
  93. package/dist/types/runner-interface.d.ts.map +1 -0
  94. package/dist/types/runner-interface.js +6 -0
  95. package/dist/types/runner-interface.js.map +1 -0
  96. package/dist/utils/StateManager.d.ts +52 -0
  97. package/dist/utils/StateManager.d.ts.map +1 -0
  98. package/dist/utils/StateManager.js +162 -0
  99. package/dist/utils/StateManager.js.map +1 -0
  100. package/dist/utils/config.d.ts +41 -0
  101. package/dist/utils/config.d.ts.map +1 -0
  102. package/dist/utils/config.js +250 -0
  103. package/dist/utils/config.js.map +1 -0
  104. package/dist/utils/console.d.ts +11 -0
  105. package/dist/utils/console.d.ts.map +1 -0
  106. package/dist/utils/console.js +15 -0
  107. package/dist/utils/console.js.map +1 -0
  108. package/dist/utils/expand-env.d.ts +2 -0
  109. package/dist/utils/expand-env.d.ts.map +1 -0
  110. package/dist/utils/expand-env.js +20 -0
  111. package/dist/utils/expand-env.js.map +1 -0
  112. package/dist/utils/logger.d.ts +9 -0
  113. package/dist/utils/logger.d.ts.map +1 -0
  114. package/dist/utils/logger.js +108 -0
  115. package/dist/utils/logger.js.map +1 -0
  116. package/dist/utils/status-line.d.ts +37 -0
  117. package/dist/utils/status-line.d.ts.map +1 -0
  118. package/dist/utils/status-line.js +113 -0
  119. package/dist/utils/status-line.js.map +1 -0
  120. package/docs/claude-manager.md +91 -0
  121. package/exceptions.log +22 -0
  122. package/lib/preload-script.js +293 -0
  123. package/package.json +55 -0
  124. package/rejections.log +63 -0
  125. package/runner.log +488 -0
  126. package/src/components/claude-sdk-manager.ts +1354 -0
  127. package/src/components/enhanced-repository-manager.ts +823 -0
  128. package/src/components/message-handler-sse.ts +1011 -0
  129. package/src/components/repository-manager.ts +337 -0
  130. package/src/index.ts +166 -0
  131. package/src/runner-sse.ts +847 -0
  132. package/src/services/RunnerAPIClient.ts +135 -0
  133. package/src/services/SSEClient.ts +258 -0
  134. package/src/types/claude.ts +55 -0
  135. package/src/types/computer-name.d.ts +4 -0
  136. package/src/types/index.ts +63 -0
  137. package/src/types/messages.ts +39 -0
  138. package/src/types/runner-interface.ts +34 -0
  139. package/src/utils/StateManager.ts +187 -0
  140. package/src/utils/codex-sdk.js +448 -0
  141. package/src/utils/config.ts +315 -0
  142. package/src/utils/console.ts +13 -0
  143. package/src/utils/expand-env.ts +22 -0
  144. package/src/utils/logger.ts +131 -0
  145. package/src/utils/sdk-demo.js +34 -0
  146. package/src/utils/status-line.ts +121 -0
  147. package/test-debug.sh +26 -0
  148. package/tests/retry-strategies.test.ts +410 -0
  149. package/tests/sdk-integration.test.ts +329 -0
  150. package/tests/sdk-streaming.test.ts +1180 -0
  151. package/tests/setup.ts +5 -0
  152. package/tests/test-claude-manager.ts +120 -0
  153. package/tsconfig.json +36 -0
  154. package/vitest.config.ts +27 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * RepositoryManager - Manages Git repository checkouts and workspace isolation
3
+ *
4
+ * This component handles all Git operations for the runner, including:
5
+ * - Cloning repositories with authentication
6
+ * - Updating repositories to latest state
7
+ * - Workspace isolation via separate directories
8
+ * - Cleanup of old repositories
9
+ * - Error handling for Git operations
10
+ */
11
+
12
+ import { IRunnerApp } from "../types/runner-interface";
13
+ import { WorkspaceRepository } from "../types";
14
+ import path from "path";
15
+ import fs from "fs/promises";
16
+ import { mkdirSync } from "fs";
17
+ import simpleGit, { SimpleGit, GitError } from "simple-git";
18
+ import { console } from "../utils/console";
19
+
20
+ export class RepositoryManager {
21
+ private repositories: Map<string, WorkspaceRepository>;
22
+ protected repoBasePath: string;
23
+ private runner: IRunnerApp;
24
+ private git: SimpleGit;
25
+
26
+ constructor(runner: IRunnerApp) {
27
+ this.runner = runner;
28
+ this.repositories = new Map();
29
+
30
+ // Use workspaceDir from config if available, otherwise use environment variable
31
+ this.repoBasePath =
32
+ runner.config_.workspaceDir ||
33
+ process.env["NORTHFLARE_WORKSPACE_DIR"] ||
34
+ "/tmp/northflare-workspace";
35
+ if (!this.repoBasePath) {
36
+ throw new Error(
37
+ "NORTHFLARE_WORKSPACE_DIR environment variable must be set"
38
+ );
39
+ }
40
+
41
+ // Ensure workspace directory exists before initializing simple-git
42
+ try {
43
+ mkdirSync(this.repoBasePath, { recursive: true });
44
+ } catch (error) {
45
+ console.error(
46
+ `Failed to create workspace directory ${this.repoBasePath}:`,
47
+ error
48
+ );
49
+ throw error;
50
+ }
51
+
52
+ // Initialize simple-git
53
+ this.git = simpleGit({
54
+ baseDir: this.repoBasePath,
55
+ binary: "git",
56
+ maxConcurrentProcesses: 3,
57
+ config: ["user.name=Northflare", "user.email=runner@northflare.ai"],
58
+ });
59
+ }
60
+
61
+ async checkoutRepository(
62
+ workspaceId: string,
63
+ repoUrl: string,
64
+ branch: string,
65
+ githubToken?: string
66
+ ): Promise<string> {
67
+ const existing = this.repositories.get(workspaceId);
68
+ if (
69
+ existing &&
70
+ existing.repoUrl === repoUrl &&
71
+ existing.branch === branch
72
+ ) {
73
+ await this.updateRepository(existing, githubToken);
74
+ return existing.localPath;
75
+ }
76
+
77
+ const localPath = path.join(this.repoBasePath, workspaceId);
78
+
79
+ // Clean up existing directory if it exists
80
+ await this.cleanupDirectory(localPath);
81
+
82
+ // Clone with authentication
83
+ const authUrl = this.getAuthenticatedUrl(repoUrl, githubToken);
84
+
85
+ try {
86
+ console.log(`Cloning repository for workspace ${workspaceId}...`);
87
+ await this.executeGit(["clone", "--branch", branch, authUrl, localPath]);
88
+
89
+ // Configure the repository
90
+ const repoGit = simpleGit(localPath);
91
+ await repoGit.addConfig("credential.helper", "store");
92
+
93
+ const repo: WorkspaceRepository = {
94
+ workspaceId,
95
+ repoUrl,
96
+ branch,
97
+ localPath,
98
+ lastAccessed: new Date(),
99
+ };
100
+
101
+ this.repositories.set(workspaceId, repo);
102
+ console.log(
103
+ `Successfully cloned repository for workspace ${workspaceId}`
104
+ );
105
+ return localPath;
106
+ } catch (error) {
107
+ console.error(
108
+ `Failed to clone repository for workspace ${workspaceId}:`,
109
+ error
110
+ );
111
+ await this.cleanupDirectory(localPath);
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Checkout a local repository (bypasses Git operations)
118
+ * Used for local workspaces that point to existing directories on the filesystem
119
+ */
120
+ async checkoutLocalRepository(
121
+ workspaceId: string,
122
+ localRepoPath: string
123
+ ): Promise<string> {
124
+ console.log(
125
+ `Using local repository for workspace ${workspaceId}: ${localRepoPath}`
126
+ );
127
+
128
+ // Verify the path exists
129
+ try {
130
+ const stats = await fs.stat(localRepoPath);
131
+ if (!stats.isDirectory()) {
132
+ throw new Error(`Path is not a directory: ${localRepoPath}`);
133
+ }
134
+ } catch (error) {
135
+ console.error(`Local repository path not found: ${localRepoPath}`);
136
+ throw new Error(`Local repository path not found: ${localRepoPath}`);
137
+ }
138
+
139
+ // Store in repositories map for consistency
140
+ const repo: WorkspaceRepository = {
141
+ workspaceId,
142
+ repoUrl: `file://${localRepoPath}`, // Use file:// protocol to indicate local
143
+ branch: "local", // Local repos don't have branches in the same sense
144
+ localPath: localRepoPath,
145
+ lastAccessed: new Date(),
146
+ };
147
+
148
+ this.repositories.set(workspaceId, repo);
149
+ console.log(
150
+ `Successfully set up local repository for workspace ${workspaceId}`
151
+ );
152
+ return localRepoPath;
153
+ }
154
+
155
+ async getWorkspacePath(workspaceId: string): Promise<string> {
156
+ const localPath = path.join(this.repoBasePath, workspaceId);
157
+ await this.ensureDirectory(localPath);
158
+ return localPath;
159
+ }
160
+
161
+ private async updateRepository(
162
+ repo: WorkspaceRepository,
163
+ githubToken?: string
164
+ ): Promise<void> {
165
+ repo.lastAccessed = new Date();
166
+
167
+ // Skip Git operations for local repositories
168
+ if (repo.repoUrl.startsWith("file://")) {
169
+ console.log(
170
+ `Local repository for workspace ${repo.workspaceId} - no update needed`
171
+ );
172
+ return;
173
+ }
174
+
175
+ const repoGit = simpleGit(repo.localPath);
176
+
177
+ try {
178
+ console.log(`Updating repository for workspace ${repo.workspaceId}...`);
179
+
180
+ // Update remote URL if token has changed
181
+ if (githubToken) {
182
+ const authUrl = this.getAuthenticatedUrl(repo.repoUrl, githubToken);
183
+ await repoGit.remote(["set-url", "origin", authUrl]);
184
+ }
185
+
186
+ // Fetch latest changes
187
+ await repoGit.fetch("origin");
188
+
189
+ // Reset to latest branch state
190
+ await repoGit.reset(["--hard", `origin/${repo.branch}`]);
191
+
192
+ // Clean untracked files
193
+ await repoGit.clean("f", ["-d"]);
194
+
195
+ console.log(
196
+ `Successfully updated repository for workspace ${repo.workspaceId}`
197
+ );
198
+ } catch (error) {
199
+ console.error(
200
+ `Failed to update repository for workspace ${repo.workspaceId}:`,
201
+ error
202
+ );
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ async cleanupRepository(workspaceId: string): Promise<void> {
208
+ const repo = this.repositories.get(workspaceId);
209
+ if (!repo) return;
210
+
211
+ console.log(`Cleaning up repository for workspace ${workspaceId}...`);
212
+
213
+ // For local repositories, only remove from tracking, don't delete the directory
214
+ if (repo.repoUrl.startsWith("file://")) {
215
+ console.log(`Local repository - removing from tracking only`);
216
+ this.repositories.delete(workspaceId);
217
+ return;
218
+ }
219
+
220
+ // Remove local directory for cloned repositories
221
+ await this.cleanupDirectory(repo.localPath);
222
+
223
+ this.repositories.delete(workspaceId);
224
+ }
225
+
226
+ async cleanupOldRepositories(
227
+ maxAge: number = 7 * 24 * 60 * 60 * 1000
228
+ ): Promise<void> {
229
+ const now = new Date();
230
+ const toCleanup: string[] = [];
231
+
232
+ for (const [workspaceId, repo] of this.repositories.entries()) {
233
+ const age = now.getTime() - repo.lastAccessed.getTime();
234
+ if (age > maxAge) {
235
+ toCleanup.push(workspaceId);
236
+ }
237
+ }
238
+
239
+ for (const workspaceId of toCleanup) {
240
+ await this.cleanupRepository(workspaceId);
241
+ }
242
+
243
+ console.log(`Cleaned up ${toCleanup.length} old repositories`);
244
+ }
245
+
246
+ protected async executeGit(args: string[], cwd?: string): Promise<void> {
247
+ const git = cwd ? simpleGit(cwd) : this.git;
248
+ try {
249
+ await git.raw(args);
250
+ } catch (error) {
251
+ if (error instanceof GitError) {
252
+ throw new Error(`Git operation failed: ${error.message}`);
253
+ }
254
+ throw error;
255
+ }
256
+ }
257
+
258
+ protected getAuthenticatedUrl(repoUrl: string, githubToken?: string): string {
259
+ if (!githubToken) {
260
+ return repoUrl;
261
+ }
262
+
263
+ // Handle different URL formats
264
+ if (repoUrl.startsWith("https://")) {
265
+ // HTTPS URL
266
+ return repoUrl.replace("https://", `https://${githubToken}@`);
267
+ } else if (repoUrl.startsWith("git@")) {
268
+ // SSH URL - convert to HTTPS with token
269
+ const httpsUrl = repoUrl
270
+ .replace("git@github.com:", "https://github.com/")
271
+ .replace(".git", "");
272
+ return `https://${githubToken}@${httpsUrl.replace("https://", "")}.git`;
273
+ }
274
+
275
+ return repoUrl;
276
+ }
277
+
278
+ protected async ensureDirectory(dirPath: string): Promise<void> {
279
+ try {
280
+ await fs.mkdir(dirPath, { recursive: true });
281
+ } catch (error) {
282
+ console.error(`Failed to create directory ${dirPath}:`, error);
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ protected async cleanupDirectory(dirPath: string): Promise<void> {
288
+ try {
289
+ const stats = await fs.stat(dirPath).catch(() => null);
290
+ if (stats) {
291
+ await fs.rm(dirPath, { recursive: true, force: true });
292
+ }
293
+ } catch (error) {
294
+ console.error(`Failed to cleanup directory ${dirPath}:`, error);
295
+ // Don't throw - cleanup errors are not critical
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Get statistics about managed repositories
301
+ */
302
+ getRepositoryStats(): {
303
+ total: number;
304
+ workspaceIds: string[];
305
+ oldestAccess: Date | null;
306
+ newestAccess: Date | null;
307
+ } {
308
+ const repos = Array.from(this.repositories.values());
309
+
310
+ return {
311
+ total: repos.length,
312
+ workspaceIds: Array.from(this.repositories.keys()),
313
+ oldestAccess:
314
+ repos.length > 0
315
+ ? new Date(Math.min(...repos.map((r) => r.lastAccessed.getTime())))
316
+ : null,
317
+ newestAccess:
318
+ repos.length > 0
319
+ ? new Date(Math.max(...repos.map((r) => r.lastAccessed.getTime())))
320
+ : null,
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Check if a workspace has an existing repository
326
+ */
327
+ hasRepository(workspaceId: string): boolean {
328
+ return this.repositories.has(workspaceId);
329
+ }
330
+
331
+ /**
332
+ * Get repository info for a workspace
333
+ */
334
+ getRepository(workspaceId: string): WorkspaceRepository | undefined {
335
+ return this.repositories.get(workspaceId);
336
+ }
337
+ }
package/src/index.ts ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Main entry point for the Northflare Runner App
3
+ */
4
+
5
+ import { RunnerApp } from "./runner-sse";
6
+ import { ConfigManager } from "./utils/config";
7
+ import { logger, configureFileLogging } from "./utils/logger";
8
+ import path from "path";
9
+ import fs from "fs/promises";
10
+
11
+ let runner: RunnerApp | null = null;
12
+
13
+ async function main() {
14
+ try {
15
+ logger.info("Starting Northflare Runner...");
16
+
17
+ // Load configuration (args already parsed by CLI)
18
+ let configPath = process.argv[2]; // This is set by the CLI if --config was provided
19
+
20
+ // If no config path provided, check for default location
21
+ if (!configPath) {
22
+ try {
23
+ const envPaths = require("env-paths").default || require("env-paths");
24
+ const paths = envPaths("northflare-runner", { suffix: "" });
25
+ const defaultConfigPath = path.join(paths.config, "config.json");
26
+
27
+ // Check if default config exists
28
+ if (
29
+ await fs
30
+ .access(defaultConfigPath)
31
+ .then(() => true)
32
+ .catch(() => false)
33
+ ) {
34
+ configPath = defaultConfigPath;
35
+ }
36
+ } catch (error) {
37
+ // env-paths not available or error accessing default location
38
+ }
39
+ }
40
+
41
+ const config = await ConfigManager.loadConfig(configPath);
42
+
43
+ // Set up file logging
44
+ const logDir = path.join(config.dataDir, "logs");
45
+ await fs.mkdir(logDir, { recursive: true });
46
+ configureFileLogging(logDir);
47
+
48
+ logger.info("Configuration loaded", {
49
+ orchestratorUrl: config.orchestratorUrl,
50
+ retryStrategy: config.retryStrategy,
51
+ retryIntervalSecs: config.retryIntervalSecs,
52
+ retryDurationSecs: config.retryDurationSecs,
53
+ });
54
+
55
+ // Additional debug logging
56
+ if (process.env["DEBUG"] === "true") {
57
+ logger.debug("Debug mode enabled - verbose logging active", {
58
+ dataDir: config.dataDir,
59
+ heartbeatInterval: config.heartbeatInterval,
60
+ nodeVersion: process.version,
61
+ platform: process.platform,
62
+ pid: process.pid,
63
+ });
64
+ }
65
+
66
+ // Create and start runner - pass the resolved config path
67
+ runner = new RunnerApp(config, configPath);
68
+ await runner.start();
69
+
70
+ // Set up graceful shutdown handlers
71
+ setupShutdownHandlers();
72
+
73
+ logger.info("Northflare Runner is running", {
74
+ runnerId: runner.getRunnerId() || "pending registration",
75
+ });
76
+
77
+ // Log additional details in debug mode
78
+ if (process.env["DEBUG"] === "true" && runner) {
79
+ logger.debug("Runner started with full details", {
80
+ runnerId: runner.getRunnerId(),
81
+ runnerUid: runner.getRunnerUid(),
82
+ lastProcessedAt: runner.getLastProcessedAt()?.toISOString() || "null",
83
+ isActiveRunner: runner.getIsActiveRunner(),
84
+ uptime: process.uptime(),
85
+ memoryUsage: process.memoryUsage(),
86
+ });
87
+ }
88
+ } catch (error) {
89
+ logger.error("Failed to start runner:", error);
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ function setupShutdownHandlers() {
95
+ let isShuttingDown = false;
96
+
97
+ // Handle various shutdown signals
98
+ const signals: NodeJS.Signals[] = ["SIGTERM", "SIGINT", "SIGHUP"];
99
+
100
+ for (const signal of signals) {
101
+ process.on(signal, async () => {
102
+ if (isShuttingDown) {
103
+ logger.info(`Already shutting down, ignoring ${signal}`);
104
+ return;
105
+ }
106
+ isShuttingDown = true;
107
+ logger.info(`Received ${signal}, shutting down gracefully...`);
108
+ await shutdown();
109
+ });
110
+ }
111
+
112
+ // Handle uncaught exceptions
113
+ process.on("uncaughtException", async (error) => {
114
+ logger.error("Uncaught exception:", error);
115
+ if (!isShuttingDown) {
116
+ isShuttingDown = true;
117
+ await shutdown(1);
118
+ }
119
+ });
120
+
121
+ // Handle unhandled promise rejections
122
+ process.on("unhandledRejection", async (reason, promise) => {
123
+ logger.error("Unhandled rejection", { promise, reason });
124
+ if (!isShuttingDown) {
125
+ isShuttingDown = true;
126
+ await shutdown(1);
127
+ }
128
+ });
129
+
130
+ // Handle process warnings
131
+ process.on("warning", (warning) => {
132
+ logger.warn("Process warning", {
133
+ name: warning.name,
134
+ message: warning.message,
135
+ stack: warning.stack,
136
+ });
137
+ });
138
+ }
139
+
140
+ async function shutdown(exitCode: number = 0) {
141
+ if (runner) {
142
+ try {
143
+ await runner.stop();
144
+ logger.info("Runner stopped successfully");
145
+ } catch (error) {
146
+ logger.error("Error during shutdown:", error);
147
+ exitCode = 1;
148
+ }
149
+ }
150
+
151
+ process.exit(exitCode);
152
+ }
153
+
154
+ // Start the runner if this is the main module
155
+ if (require.main === module) {
156
+ main().catch((error) => {
157
+ logger.error("Fatal error:", error);
158
+ process.exit(1);
159
+ });
160
+ }
161
+
162
+ // Export for programmatic usage
163
+ export { RunnerApp } from "./runner-sse";
164
+ export { ConfigManager } from "./utils/config";
165
+ export * from "./types";
166
+ export { main };