@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,315 @@
1
+ /**
2
+ * Configuration management utilities
3
+ */
4
+
5
+ import { RunnerConfig, EnvironmentConfig, RetryStrategy } from "../types";
6
+ import fs from "fs/promises";
7
+ import path from "path";
8
+ import { createLogger } from "./logger";
9
+ import { Command } from "commander";
10
+
11
+ const logger = createLogger("ConfigManager");
12
+
13
+ export class ConfigManager {
14
+ private static DEFAULT_CONFIG: Partial<RunnerConfig> = {
15
+ dataDir: "./data",
16
+ heartbeatInterval: 120000, // 2 minutes
17
+ retryStrategy: "exponential" as RetryStrategy,
18
+ retryIntervalSecs: 60,
19
+ retryDurationSecs: 900,
20
+ };
21
+
22
+ /**
23
+ * Load configuration without parsing command line arguments
24
+ * Used when arguments have already been parsed by the CLI
25
+ */
26
+ static async loadConfig(configPath?: string): Promise<RunnerConfig> {
27
+ // Validate required environment variables
28
+ this.validateEnvironment();
29
+
30
+ // Start with defaults
31
+ let config: Partial<RunnerConfig> = { ...this.DEFAULT_CONFIG };
32
+
33
+ // Determine config path - use provided path or default location
34
+ let effectiveConfigPath = configPath;
35
+ if (!effectiveConfigPath) {
36
+ // Try to use default config location
37
+ try {
38
+ const envPaths = require("env-paths").default || require("env-paths");
39
+ const paths = envPaths("northflare-runner", { suffix: "" });
40
+ const defaultConfigPath = path.join(paths.config, "config.json");
41
+
42
+ // Check if default config exists
43
+ const fs = require("fs");
44
+ if (fs.existsSync(defaultConfigPath)) {
45
+ effectiveConfigPath = defaultConfigPath;
46
+ logger.info(`Using default config file: ${defaultConfigPath}`);
47
+ }
48
+ } catch (error) {
49
+ // env-paths not available or error accessing default location
50
+ logger.debug("Could not check default config location:", error);
51
+ }
52
+ }
53
+
54
+ // Load from config file if we have a path
55
+ if (effectiveConfigPath) {
56
+ const fileConfig = await this.loadConfigFile(effectiveConfigPath);
57
+ config = { ...config, ...fileConfig };
58
+ }
59
+
60
+ // Override with environment variables
61
+ const envConfig = this.loadFromEnvironment();
62
+ config = { ...config, ...envConfig };
63
+
64
+ // Validate final configuration
65
+ this.validateConfig(config);
66
+
67
+ return config as RunnerConfig;
68
+ }
69
+
70
+ /**
71
+ * Parse command line arguments and load configuration
72
+ */
73
+ static async parseArgsAndLoadConfig(argv: string[]): Promise<RunnerConfig> {
74
+ const program = new Command();
75
+
76
+ program
77
+ .name("northflare-runner")
78
+ .description(
79
+ "Northflare Runner - Executes Claude agents for task processing"
80
+ )
81
+ .version("1.0.0")
82
+ .option("-c, --config <path>", "Path to configuration file")
83
+ .option(
84
+ "--retry-strategy <strategy>",
85
+ "Registration retry strategy (none, interval, exponential)",
86
+ "exponential"
87
+ )
88
+ .option(
89
+ "--retry-interval-secs <seconds>",
90
+ "Retry interval in seconds for interval strategy",
91
+ "60"
92
+ )
93
+ .option(
94
+ "--retry-duration-secs <seconds>",
95
+ "Max retry duration in seconds for exponential strategy",
96
+ "900"
97
+ )
98
+ .option("--data-dir <path>", "Data directory path", "./data")
99
+ .option(
100
+ "--heartbeat-interval <ms>",
101
+ "Heartbeat interval in milliseconds",
102
+ "120000"
103
+ );
104
+
105
+ program.parse(argv);
106
+ const options = program.opts();
107
+
108
+ // Validate required environment variables
109
+ this.validateEnvironment();
110
+
111
+ // Start with defaults
112
+ let config: Partial<RunnerConfig> = { ...this.DEFAULT_CONFIG };
113
+
114
+ // Load from config file if provided
115
+ if (options["config"]) {
116
+ const fileConfig = await this.loadConfigFile(options["config"]);
117
+ config = { ...config, ...fileConfig };
118
+ }
119
+
120
+ // Override with environment variables
121
+ const envConfig = this.loadFromEnvironment();
122
+ config = { ...config, ...envConfig };
123
+
124
+ // Override with CLI arguments (highest priority)
125
+ if (options["retryStrategy"]) {
126
+ config.retryStrategy = options["retryStrategy"] as RetryStrategy;
127
+ }
128
+ if (options["retryIntervalSecs"]) {
129
+ config.retryIntervalSecs = parseInt(options["retryIntervalSecs"]);
130
+ }
131
+ if (options["retryDurationSecs"]) {
132
+ config.retryDurationSecs = parseInt(options["retryDurationSecs"]);
133
+ }
134
+ if (options["dataDir"]) {
135
+ config.dataDir = options["dataDir"];
136
+ }
137
+ if (options["heartbeatInterval"]) {
138
+ config.heartbeatInterval = parseInt(options["heartbeatInterval"]);
139
+ }
140
+
141
+ // Validate final configuration
142
+ this.validateConfig(config);
143
+
144
+ return config as RunnerConfig;
145
+ }
146
+
147
+ /**
148
+ * Validate required environment variables
149
+ */
150
+ private static validateEnvironment(): void {
151
+ // Set default for NORTHFLARE_WORKSPACE_DIR if not provided
152
+ if (!process.env["NORTHFLARE_WORKSPACE_DIR"]) {
153
+ try {
154
+ const envPaths = require("env-paths").default || require("env-paths");
155
+ const paths = envPaths("northflare-runner", { suffix: "" });
156
+ process.env["NORTHFLARE_WORKSPACE_DIR"] = paths.data;
157
+ } catch (error) {
158
+ // Fallback to original default if env-paths is not available
159
+ process.env["NORTHFLARE_WORKSPACE_DIR"] = "/workspace";
160
+ }
161
+ }
162
+
163
+ // Set default for NORTHFLARE_ORCHESTRATOR_URL if not provided
164
+ if (!process.env["NORTHFLARE_ORCHESTRATOR_URL"]) {
165
+ process.env["NORTHFLARE_ORCHESTRATOR_URL"] = "https://api.northflare.ai";
166
+ }
167
+
168
+ const required = [
169
+ "NORTHFLARE_RUNNER_TOKEN",
170
+ "NORTHFLARE_WORKSPACE_DIR",
171
+ "NORTHFLARE_ORCHESTRATOR_URL",
172
+ ];
173
+
174
+ const missing = required.filter((key) => !process.env[key]);
175
+
176
+ if (missing.length > 0) {
177
+ throw new Error(
178
+ `Missing required environment variables: ${missing.join(", ")}\n` +
179
+ "Please set these environment variables before starting the runner."
180
+ );
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Load configuration from environment variables
186
+ */
187
+ private static loadFromEnvironment(): Partial<RunnerConfig> {
188
+ const config: Partial<RunnerConfig> = {
189
+ orchestratorUrl: process.env["NORTHFLARE_ORCHESTRATOR_URL"]!,
190
+ };
191
+
192
+ // Optional environment overrides
193
+ if (process.env["NORTHFLARE_DATA_DIR"]) {
194
+ config.dataDir = process.env["NORTHFLARE_DATA_DIR"];
195
+ }
196
+
197
+ return config;
198
+ }
199
+
200
+ /**
201
+ * Load configuration from file
202
+ */
203
+ private static async loadConfigFile(
204
+ configPath: string
205
+ ): Promise<Partial<RunnerConfig>> {
206
+ try {
207
+ const absolutePath = path.resolve(configPath);
208
+ const content = await fs.readFile(absolutePath, "utf-8");
209
+
210
+ // Support both JSON and YAML formats
211
+ if (configPath.endsWith(".json")) {
212
+ const config = JSON.parse(content);
213
+
214
+ // Log if runnerRepos are found in config
215
+ if (config.runnerRepos && Array.isArray(config.runnerRepos)) {
216
+ logger.info(
217
+ `Found ${config.runnerRepos.length} runner repos in config file`
218
+ );
219
+ }
220
+
221
+ return config;
222
+ } else if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
223
+ // For YAML support, we'd need to add a yaml parser dependency
224
+ throw new Error("YAML configuration files are not yet supported");
225
+ } else {
226
+ throw new Error("Configuration file must be .json format");
227
+ }
228
+ } catch (error) {
229
+ if ((error as any).code === "ENOENT") {
230
+ logger.warn(`Configuration file not found: ${configPath}`);
231
+ return {};
232
+ }
233
+ throw error;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Validate final configuration
239
+ */
240
+ private static validateConfig(config: Partial<RunnerConfig>): void {
241
+ // Note: Runner ID will be generated by server during registration
242
+
243
+ if (!config.orchestratorUrl) {
244
+ throw new Error("orchestratorUrl is required");
245
+ }
246
+
247
+ if (!config.dataDir) {
248
+ throw new Error("dataDir is required");
249
+ }
250
+
251
+ if (!config.heartbeatInterval || config.heartbeatInterval < 1000) {
252
+ throw new Error("heartbeatInterval must be at least 1000ms");
253
+ }
254
+
255
+ // Validate retry strategy
256
+ const validStrategies: RetryStrategy[] = [
257
+ "none",
258
+ "interval",
259
+ "exponential",
260
+ ];
261
+ if (
262
+ !config.retryStrategy ||
263
+ !validStrategies.includes(config.retryStrategy)
264
+ ) {
265
+ throw new Error(
266
+ `retryStrategy must be one of: ${validStrategies.join(", ")}`
267
+ );
268
+ }
269
+
270
+ if (
271
+ config.retryStrategy === "interval" &&
272
+ (!config.retryIntervalSecs || config.retryIntervalSecs < 1)
273
+ ) {
274
+ throw new Error(
275
+ "retryIntervalSecs must be at least 1 when using interval strategy"
276
+ );
277
+ }
278
+
279
+ if (
280
+ config.retryStrategy === "exponential" &&
281
+ (!config.retryDurationSecs || config.retryDurationSecs < 1)
282
+ ) {
283
+ throw new Error(
284
+ "retryDurationSecs must be at least 1 when using exponential strategy"
285
+ );
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Get all environment configuration
291
+ */
292
+ static getEnvironmentConfig(): EnvironmentConfig {
293
+ return {
294
+ NORTHFLARE_RUNNER_TOKEN: process.env["NORTHFLARE_RUNNER_TOKEN"]!,
295
+ NORTHFLARE_WORKSPACE_DIR: process.env["NORTHFLARE_WORKSPACE_DIR"]!,
296
+ NORTHFLARE_ORCHESTRATOR_URL: process.env["NORTHFLARE_ORCHESTRATOR_URL"]!,
297
+ DEBUG: process.env["DEBUG"],
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Save configuration file with updated data
303
+ */
304
+ static async saveConfigFile(configPath: string, config: any): Promise<void> {
305
+ try {
306
+ const absolutePath = path.resolve(configPath);
307
+ const content = JSON.stringify(config, null, 2);
308
+ await fs.writeFile(absolutePath, content, "utf-8");
309
+ logger.info(`Updated configuration file: ${absolutePath}`);
310
+ } catch (error) {
311
+ logger.error("Failed to save configuration file", error);
312
+ throw error;
313
+ }
314
+ }
315
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Console wrapper that suppresses output when not in debug mode
3
+ */
4
+
5
+ const isDebug = process.env["DEBUG"] === "true";
6
+
7
+ export const console = {
8
+ log: isDebug ? global.console.log.bind(global.console) : () => {},
9
+ warn: isDebug ? global.console.warn.bind(global.console) : () => {},
10
+ error: global.console.error.bind(global.console), // Always show errors
11
+ info: isDebug ? global.console.info.bind(global.console) : () => {},
12
+ debug: isDebug ? global.console.debug.bind(global.console) : () => {},
13
+ };
@@ -0,0 +1,22 @@
1
+ export function expandEnv(
2
+ obj: any,
3
+ env: Record<string, string | undefined> = process.env
4
+ ): any {
5
+ if (typeof obj === "string") {
6
+ return obj.replace(/\$\{([^}]+)\}/g, (match, key) => env[key] || match);
7
+ }
8
+
9
+ if (Array.isArray(obj)) {
10
+ return obj.map((item) => expandEnv(item, env));
11
+ }
12
+
13
+ if (obj && typeof obj === "object") {
14
+ const result: Record<string, any> = {};
15
+ for (const [key, value] of Object.entries(obj)) {
16
+ result[key] = expandEnv(value, env);
17
+ }
18
+ return result;
19
+ }
20
+
21
+ return obj;
22
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Logger configuration using Winston
3
+ */
4
+
5
+ import winston from "winston";
6
+ import path from "path";
7
+
8
+ // Custom log levels
9
+ const levels = {
10
+ error: 0,
11
+ warn: 1,
12
+ info: 2,
13
+ http: 3,
14
+ verbose: 4,
15
+ debug: 5,
16
+ silly: 6,
17
+ };
18
+
19
+ // Log colors
20
+ const colors = {
21
+ error: "red",
22
+ warn: "yellow",
23
+ info: "green",
24
+ http: "magenta",
25
+ verbose: "cyan",
26
+ debug: "blue",
27
+ silly: "gray",
28
+ };
29
+
30
+ winston.addColors(colors);
31
+
32
+ // Format for console output
33
+ const consoleFormat = winston.format.combine(
34
+ winston.format.colorize({ all: true }),
35
+ winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
36
+ winston.format.printf(({ timestamp, level, message, stack, ...metadata }) => {
37
+ let msg = `${timestamp} [${level}]: ${message}`;
38
+ // Only show stack traces and metadata in debug mode
39
+ if (process.env["DEBUG"] === "true") {
40
+ if (stack) {
41
+ msg += `\n${stack}`;
42
+ }
43
+ const filteredMetadata = { ...metadata };
44
+ delete filteredMetadata["component"]; // Keep component info
45
+ if (Object.keys(filteredMetadata).length > 0) {
46
+ msg += ` ${JSON.stringify(filteredMetadata)}`;
47
+ }
48
+ }
49
+ return msg;
50
+ })
51
+ );
52
+
53
+ // Format for file output
54
+ const fileFormat = winston.format.combine(
55
+ winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
56
+ winston.format.errors({ stack: true }),
57
+ winston.format.json()
58
+ );
59
+
60
+ // Create logger instance
61
+ export const logger = winston.createLogger({
62
+ level: process.env["DEBUG"] === "true" ? "debug" : "info",
63
+ levels,
64
+ transports: [
65
+ // Always include console transport for errors
66
+ new winston.transports.Console({
67
+ format: consoleFormat,
68
+ level: process.env["DEBUG"] === "true" ? "debug" : "error",
69
+ }),
70
+ ],
71
+ });
72
+
73
+ // Add file transports if log directory is configured
74
+ export function configureFileLogging(logDir: string): void {
75
+ // Error log file
76
+ logger.add(
77
+ new winston.transports.File({
78
+ filename: path.join(logDir, "error.log"),
79
+ level: "error",
80
+ format: fileFormat,
81
+ maxsize: 10 * 1024 * 1024, // 10MB
82
+ maxFiles: 5,
83
+ })
84
+ );
85
+
86
+ // Combined log file
87
+ logger.add(
88
+ new winston.transports.File({
89
+ filename: path.join(logDir, "combined.log"),
90
+ format: fileFormat,
91
+ maxsize: 50 * 1024 * 1024, // 50MB
92
+ maxFiles: 10,
93
+ })
94
+ );
95
+
96
+ // Debug log file (only in debug mode)
97
+ if (process.env["DEBUG"] === "true") {
98
+ logger.add(
99
+ new winston.transports.File({
100
+ filename: path.join(logDir, "debug.log"),
101
+ level: "debug",
102
+ format: fileFormat,
103
+ maxsize: 100 * 1024 * 1024, // 100MB
104
+ maxFiles: 3,
105
+ })
106
+ );
107
+ }
108
+ }
109
+
110
+ // Create child logger for specific components
111
+ export function createLogger(component: string): winston.Logger {
112
+ return logger.child({ component });
113
+ }
114
+
115
+ // Log unhandled errors
116
+ logger.exceptions.handle(
117
+ new winston.transports.File({
118
+ filename: "exceptions.log",
119
+ format: fileFormat,
120
+ })
121
+ );
122
+
123
+ logger.rejections.handle(
124
+ new winston.transports.File({
125
+ filename: "rejections.log",
126
+ format: fileFormat,
127
+ })
128
+ );
129
+
130
+ // Export default logger
131
+ export default logger;
@@ -0,0 +1,34 @@
1
+ import { query } from "./codex-sdk.js";
2
+
3
+ export default async function getAllOutput(prompt = "Hello!", options = {}) {
4
+ const defaultOptions = {
5
+ cwd: "/Users/toby/Code/hypervisual",
6
+ configOverrides: {
7
+ model: "gpt-5-codex",
8
+ model_reasoning_effort: "high",
9
+ sandbox_mode: "danger-full-access",
10
+ },
11
+ };
12
+
13
+ const q = query({
14
+ prompt,
15
+ options: { ...defaultOptions, ...options },
16
+ });
17
+
18
+ const messages = [];
19
+ for await (const message of q) {
20
+ messages.push(message);
21
+ console.log(message);
22
+
23
+ // Check if we've received a task completion
24
+ if (message.msg?.type === "task_complete") {
25
+ break;
26
+ }
27
+ }
28
+
29
+ // Send shutdown command and close
30
+ await q.shutdown();
31
+ await q.close();
32
+
33
+ return messages;
34
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * StatusLineManager - Manages persistent status line output for active Claude Code processes
3
+ *
4
+ * This component maintains a single-line status output that updates every minute to show
5
+ * the count of active Claude Code processes. It prevents the machine from entering idle
6
+ * state while processes are running and provides clear visual feedback without cluttering
7
+ * the terminal with multiple log lines.
8
+ */
9
+
10
+ export class StatusLineManager {
11
+ private intervalId?: NodeJS.Timeout;
12
+ private activeCount: number = 0;
13
+ private lastLine: string = "";
14
+ private isEnabled: boolean = true;
15
+
16
+ constructor() {
17
+ // Only enable status line when not in debug mode
18
+ this.isEnabled = process.env["DEBUG"] !== "true";
19
+ }
20
+
21
+ /**
22
+ * Updates the count of active conversations
23
+ */
24
+ updateActiveCount(count: number): void {
25
+ const wasZero = this.activeCount === 0;
26
+ const isZero = count === 0;
27
+ this.activeCount = count;
28
+
29
+ // Start interval when transitioning from 0 to active
30
+ if (wasZero && !isZero) {
31
+ this.startInterval();
32
+ }
33
+ // Stop interval when transitioning to 0
34
+ else if (!wasZero && isZero) {
35
+ this.stopInterval();
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Starts the status line interval
41
+ */
42
+ private startInterval(): void {
43
+ if (!this.isEnabled || this.intervalId) return;
44
+
45
+ // Show initial status immediately
46
+ this.showStatus();
47
+
48
+ // Update every minute on the minute
49
+ const now = new Date();
50
+ const msUntilNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
51
+
52
+ // First timeout to align with the minute
53
+ setTimeout(() => {
54
+ this.showStatus();
55
+
56
+ // Then set up regular interval
57
+ this.intervalId = setInterval(() => {
58
+ this.showStatus();
59
+ }, 60000); // Every 60 seconds
60
+ }, msUntilNextMinute);
61
+ }
62
+
63
+ /**
64
+ * Stops the status line interval
65
+ */
66
+ private stopInterval(): void {
67
+ if (this.intervalId) {
68
+ clearInterval(this.intervalId);
69
+ this.intervalId = undefined;
70
+ }
71
+
72
+ // Show final "No active" message
73
+ this.showStatus();
74
+ }
75
+
76
+ /**
77
+ * Shows the current status
78
+ */
79
+ private showStatus(): void {
80
+ if (!this.isEnabled) return;
81
+
82
+ const now = new Date();
83
+ const timeStr = now.toTimeString().slice(0, 5); // HH:MM format
84
+
85
+ let newLine: string;
86
+ if (this.activeCount === 0) {
87
+ newLine = "No active Claude Code processes";
88
+ } else if (this.activeCount === 1) {
89
+ newLine = `${timeStr} 1 active Claude Code process`;
90
+ } else {
91
+ newLine = `${timeStr} ${this.activeCount} active Claude Code processes`;
92
+ }
93
+
94
+ // If this is the first status line, position it
95
+ if (!this.lastLine) {
96
+ // Move to start of line and save cursor position
97
+ process.stdout.write('\r');
98
+ } else {
99
+ // Clear the current line
100
+ process.stdout.write('\r' + ' '.repeat(this.lastLine.length) + '\r');
101
+ }
102
+
103
+ // Write the new status without newline
104
+ process.stdout.write(newLine);
105
+ this.lastLine = newLine;
106
+ }
107
+
108
+ /**
109
+ * Cleans up the status line manager
110
+ */
111
+ dispose(): void {
112
+ this.stopInterval();
113
+ // Add a newline to ensure next output starts on a new line
114
+ if (this.lastLine) {
115
+ process.stdout.write('\n');
116
+ }
117
+ }
118
+ }
119
+
120
+ // Singleton instance
121
+ export const statusLineManager = new StatusLineManager();
package/test-debug.sh ADDED
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+
3
+ # Test script to demonstrate debug logging in the runner
4
+
5
+ echo "Starting runner in debug mode to demonstrate enhanced logging..."
6
+ echo "This will show:"
7
+ echo " - runnerUid on initialization"
8
+ echo " - lastProcessedAt state for ElectricSQL connection"
9
+ echo " - ElectricSQL connection details"
10
+ echo " - Message processing decisions with ownership info"
11
+ echo ""
12
+ echo "Set DEBUG=true to enable verbose logging"
13
+ echo ""
14
+
15
+ # Run the runner with debug mode enabled
16
+ DEBUG=true npm run start -- --config ./config.json 2>&1 | head -100
17
+
18
+ echo ""
19
+ echo "Note: The runner will display debug logs including:"
20
+ echo " - Runner UID and ownership details during registration"
21
+ echo " - LastProcessedAt watermark for message filtering"
22
+ echo " - ElectricSQL collection creation with connection details"
23
+ echo " - Message processing decisions with reasons"
24
+ echo ""
25
+ echo "To run the runner with debug logging enabled, use:"
26
+ echo " DEBUG=true npm run start"