@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26

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 (234) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -7
  3. package/commands/cursorflow-clean.md +19 -0
  4. package/commands/cursorflow-runs.md +59 -0
  5. package/commands/cursorflow-stop.md +55 -0
  6. package/dist/cli/clean.js +178 -6
  7. package/dist/cli/clean.js.map +1 -1
  8. package/dist/cli/index.js +12 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/init.js +8 -7
  11. package/dist/cli/init.js.map +1 -1
  12. package/dist/cli/logs.js +126 -77
  13. package/dist/cli/logs.js.map +1 -1
  14. package/dist/cli/monitor.d.ts +7 -0
  15. package/dist/cli/monitor.js +1021 -202
  16. package/dist/cli/monitor.js.map +1 -1
  17. package/dist/cli/prepare.js +39 -21
  18. package/dist/cli/prepare.js.map +1 -1
  19. package/dist/cli/resume.js +268 -163
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +11 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/runs.d.ts +5 -0
  24. package/dist/cli/runs.js +214 -0
  25. package/dist/cli/runs.js.map +1 -0
  26. package/dist/cli/setup-commands.js +0 -0
  27. package/dist/cli/signal.js +8 -8
  28. package/dist/cli/signal.js.map +1 -1
  29. package/dist/cli/stop.d.ts +5 -0
  30. package/dist/cli/stop.js +215 -0
  31. package/dist/cli/stop.js.map +1 -0
  32. package/dist/cli/tasks.d.ts +10 -0
  33. package/dist/cli/tasks.js +165 -0
  34. package/dist/cli/tasks.js.map +1 -0
  35. package/dist/core/auto-recovery.d.ts +212 -0
  36. package/dist/core/auto-recovery.js +737 -0
  37. package/dist/core/auto-recovery.js.map +1 -0
  38. package/dist/core/failure-policy.d.ts +156 -0
  39. package/dist/core/failure-policy.js +488 -0
  40. package/dist/core/failure-policy.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +16 -2
  42. package/dist/core/orchestrator.js +439 -105
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/reviewer.d.ts +2 -0
  45. package/dist/core/reviewer.js +2 -0
  46. package/dist/core/reviewer.js.map +1 -1
  47. package/dist/core/runner.d.ts +33 -10
  48. package/dist/core/runner.js +374 -164
  49. package/dist/core/runner.js.map +1 -1
  50. package/dist/services/logging/buffer.d.ts +67 -0
  51. package/dist/services/logging/buffer.js +309 -0
  52. package/dist/services/logging/buffer.js.map +1 -0
  53. package/dist/services/logging/console.d.ts +89 -0
  54. package/dist/services/logging/console.js +169 -0
  55. package/dist/services/logging/console.js.map +1 -0
  56. package/dist/services/logging/file-writer.d.ts +71 -0
  57. package/dist/services/logging/file-writer.js +516 -0
  58. package/dist/services/logging/file-writer.js.map +1 -0
  59. package/dist/services/logging/formatter.d.ts +39 -0
  60. package/dist/services/logging/formatter.js +227 -0
  61. package/dist/services/logging/formatter.js.map +1 -0
  62. package/dist/services/logging/index.d.ts +11 -0
  63. package/dist/services/logging/index.js +30 -0
  64. package/dist/services/logging/index.js.map +1 -0
  65. package/dist/services/logging/parser.d.ts +31 -0
  66. package/dist/services/logging/parser.js +222 -0
  67. package/dist/services/logging/parser.js.map +1 -0
  68. package/dist/services/process/index.d.ts +59 -0
  69. package/dist/services/process/index.js +257 -0
  70. package/dist/services/process/index.js.map +1 -0
  71. package/dist/types/agent.d.ts +20 -0
  72. package/dist/types/agent.js +6 -0
  73. package/dist/types/agent.js.map +1 -0
  74. package/dist/types/config.d.ts +65 -0
  75. package/dist/types/config.js +6 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/events.d.ts +125 -0
  78. package/dist/types/events.js +6 -0
  79. package/dist/types/events.js.map +1 -0
  80. package/dist/types/index.d.ts +12 -0
  81. package/dist/types/index.js +37 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/lane.d.ts +43 -0
  84. package/dist/types/lane.js +6 -0
  85. package/dist/types/lane.js.map +1 -0
  86. package/dist/types/logging.d.ts +71 -0
  87. package/dist/types/logging.js +16 -0
  88. package/dist/types/logging.js.map +1 -0
  89. package/dist/types/review.d.ts +17 -0
  90. package/dist/types/review.js +6 -0
  91. package/dist/types/review.js.map +1 -0
  92. package/dist/types/run.d.ts +32 -0
  93. package/dist/types/run.js +6 -0
  94. package/dist/types/run.js.map +1 -0
  95. package/dist/types/task.d.ts +71 -0
  96. package/dist/types/task.js +6 -0
  97. package/dist/types/task.js.map +1 -0
  98. package/dist/ui/components.d.ts +134 -0
  99. package/dist/ui/components.js +389 -0
  100. package/dist/ui/components.js.map +1 -0
  101. package/dist/ui/log-viewer.d.ts +49 -0
  102. package/dist/ui/log-viewer.js +449 -0
  103. package/dist/ui/log-viewer.js.map +1 -0
  104. package/dist/utils/checkpoint.d.ts +87 -0
  105. package/dist/utils/checkpoint.js +317 -0
  106. package/dist/utils/checkpoint.js.map +1 -0
  107. package/dist/utils/config.d.ts +4 -0
  108. package/dist/utils/config.js +18 -8
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/cursor-agent.js.map +1 -1
  111. package/dist/utils/dependency.d.ts +74 -0
  112. package/dist/utils/dependency.js +420 -0
  113. package/dist/utils/dependency.js.map +1 -0
  114. package/dist/utils/doctor.js +17 -11
  115. package/dist/utils/doctor.js.map +1 -1
  116. package/dist/utils/enhanced-logger.d.ts +10 -33
  117. package/dist/utils/enhanced-logger.js +108 -20
  118. package/dist/utils/enhanced-logger.js.map +1 -1
  119. package/dist/utils/git.d.ts +121 -0
  120. package/dist/utils/git.js +484 -11
  121. package/dist/utils/git.js.map +1 -1
  122. package/dist/utils/health.d.ts +91 -0
  123. package/dist/utils/health.js +556 -0
  124. package/dist/utils/health.js.map +1 -0
  125. package/dist/utils/lock.d.ts +95 -0
  126. package/dist/utils/lock.js +332 -0
  127. package/dist/utils/lock.js.map +1 -0
  128. package/dist/utils/log-buffer.d.ts +17 -0
  129. package/dist/utils/log-buffer.js +14 -0
  130. package/dist/utils/log-buffer.js.map +1 -0
  131. package/dist/utils/log-constants.d.ts +23 -0
  132. package/dist/utils/log-constants.js +28 -0
  133. package/dist/utils/log-constants.js.map +1 -0
  134. package/dist/utils/log-formatter.d.ts +25 -0
  135. package/dist/utils/log-formatter.js +237 -0
  136. package/dist/utils/log-formatter.js.map +1 -0
  137. package/dist/utils/log-service.d.ts +19 -0
  138. package/dist/utils/log-service.js +47 -0
  139. package/dist/utils/log-service.js.map +1 -0
  140. package/dist/utils/logger.d.ts +46 -27
  141. package/dist/utils/logger.js +82 -60
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/path.d.ts +19 -0
  144. package/dist/utils/path.js +77 -0
  145. package/dist/utils/path.js.map +1 -0
  146. package/dist/utils/process-manager.d.ts +21 -0
  147. package/dist/utils/process-manager.js +138 -0
  148. package/dist/utils/process-manager.js.map +1 -0
  149. package/dist/utils/retry.d.ts +121 -0
  150. package/dist/utils/retry.js +374 -0
  151. package/dist/utils/retry.js.map +1 -0
  152. package/dist/utils/run-service.d.ts +88 -0
  153. package/dist/utils/run-service.js +412 -0
  154. package/dist/utils/run-service.js.map +1 -0
  155. package/dist/utils/state.d.ts +62 -3
  156. package/dist/utils/state.js +317 -11
  157. package/dist/utils/state.js.map +1 -1
  158. package/dist/utils/task-service.d.ts +82 -0
  159. package/dist/utils/task-service.js +348 -0
  160. package/dist/utils/task-service.js.map +1 -0
  161. package/dist/utils/template.d.ts +14 -0
  162. package/dist/utils/template.js +122 -0
  163. package/dist/utils/template.js.map +1 -0
  164. package/dist/utils/types.d.ts +2 -271
  165. package/dist/utils/types.js +16 -0
  166. package/dist/utils/types.js.map +1 -1
  167. package/package.json +38 -23
  168. package/scripts/ai-security-check.js +0 -1
  169. package/scripts/local-security-gate.sh +0 -0
  170. package/scripts/monitor-lanes.sh +94 -0
  171. package/scripts/patches/test-cursor-agent.js +0 -1
  172. package/scripts/release.sh +0 -0
  173. package/scripts/setup-security.sh +0 -0
  174. package/scripts/stream-logs.sh +72 -0
  175. package/scripts/verify-and-fix.sh +0 -0
  176. package/src/cli/clean.ts +187 -6
  177. package/src/cli/index.ts +12 -1
  178. package/src/cli/init.ts +8 -7
  179. package/src/cli/logs.ts +124 -77
  180. package/src/cli/monitor.ts +1815 -898
  181. package/src/cli/prepare.ts +41 -21
  182. package/src/cli/resume.ts +753 -626
  183. package/src/cli/run.ts +12 -5
  184. package/src/cli/runs.ts +212 -0
  185. package/src/cli/setup-commands.ts +0 -0
  186. package/src/cli/signal.ts +8 -7
  187. package/src/cli/stop.ts +209 -0
  188. package/src/cli/tasks.ts +154 -0
  189. package/src/core/auto-recovery.ts +909 -0
  190. package/src/core/failure-policy.ts +592 -0
  191. package/src/core/orchestrator.ts +1131 -704
  192. package/src/core/reviewer.ts +4 -0
  193. package/src/core/runner.ts +444 -180
  194. package/src/services/logging/buffer.ts +326 -0
  195. package/src/services/logging/console.ts +193 -0
  196. package/src/services/logging/file-writer.ts +526 -0
  197. package/src/services/logging/formatter.ts +268 -0
  198. package/src/services/logging/index.ts +16 -0
  199. package/src/services/logging/parser.ts +232 -0
  200. package/src/services/process/index.ts +261 -0
  201. package/src/types/agent.ts +24 -0
  202. package/src/types/config.ts +79 -0
  203. package/src/types/events.ts +156 -0
  204. package/src/types/index.ts +29 -0
  205. package/src/types/lane.ts +56 -0
  206. package/src/types/logging.ts +96 -0
  207. package/src/types/review.ts +20 -0
  208. package/src/types/run.ts +37 -0
  209. package/src/types/task.ts +79 -0
  210. package/src/ui/components.ts +430 -0
  211. package/src/ui/log-viewer.ts +485 -0
  212. package/src/utils/checkpoint.ts +374 -0
  213. package/src/utils/config.ts +18 -8
  214. package/src/utils/cursor-agent.ts +1 -1
  215. package/src/utils/dependency.ts +482 -0
  216. package/src/utils/doctor.ts +18 -11
  217. package/src/utils/enhanced-logger.ts +122 -60
  218. package/src/utils/git.ts +517 -11
  219. package/src/utils/health.ts +596 -0
  220. package/src/utils/lock.ts +346 -0
  221. package/src/utils/log-buffer.ts +28 -0
  222. package/src/utils/log-constants.ts +26 -0
  223. package/src/utils/log-formatter.ts +245 -0
  224. package/src/utils/log-service.ts +49 -0
  225. package/src/utils/logger.ts +100 -51
  226. package/src/utils/path.ts +45 -0
  227. package/src/utils/process-manager.ts +100 -0
  228. package/src/utils/retry.ts +413 -0
  229. package/src/utils/run-service.ts +433 -0
  230. package/src/utils/state.ts +385 -11
  231. package/src/utils/task-service.ts +370 -0
  232. package/src/utils/template.ts +92 -0
  233. package/src/utils/types.ts +2 -314
  234. package/templates/basic.json +21 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Enhanced file-based locking with async support and stale lock detection
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { safeJoin } from './path';
8
+
9
+ export interface LockInfo {
10
+ pid: number;
11
+ timestamp: number;
12
+ operation: string;
13
+ hostname?: string;
14
+ }
15
+
16
+ export interface LockOptions {
17
+ /** Maximum time to wait for lock in milliseconds */
18
+ timeoutMs?: number;
19
+ /** Delay between retry attempts in milliseconds */
20
+ retryDelayMs?: number;
21
+ /** Lock is considered stale after this many milliseconds */
22
+ staleTimeoutMs?: number;
23
+ /** Description of the operation for logging */
24
+ operation?: string;
25
+ }
26
+
27
+ export const DEFAULT_LOCK_OPTIONS: Required<LockOptions> = {
28
+ timeoutMs: 30000,
29
+ retryDelayMs: 100,
30
+ staleTimeoutMs: 60000,
31
+ operation: 'unknown',
32
+ };
33
+
34
+ /**
35
+ * Check if a process is still running
36
+ */
37
+ export function isProcessRunning(pid: number): boolean {
38
+ try {
39
+ // Sending signal 0 checks if process exists without actually signaling it
40
+ process.kill(pid, 0);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get the lock directory for a given base path
49
+ */
50
+ export function getLockDir(basePath: string): string {
51
+ return safeJoin(basePath, '_cursorflow', 'locks');
52
+ }
53
+
54
+ /**
55
+ * Ensure lock directory exists
56
+ */
57
+ export function ensureLockDir(basePath: string): string {
58
+ const lockDir = getLockDir(basePath);
59
+ if (!fs.existsSync(lockDir)) {
60
+ fs.mkdirSync(lockDir, { recursive: true });
61
+ }
62
+ return lockDir;
63
+ }
64
+
65
+ /**
66
+ * Read lock file info
67
+ */
68
+ export function readLockInfo(lockFile: string): LockInfo | null {
69
+ try {
70
+ if (!fs.existsSync(lockFile)) {
71
+ return null;
72
+ }
73
+ const content = fs.readFileSync(lockFile, 'utf8');
74
+ return JSON.parse(content) as LockInfo;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Check if a lock is stale
82
+ */
83
+ export function isLockStale(lockInfo: LockInfo, staleTimeoutMs: number): boolean {
84
+ // Check if process is dead
85
+ if (!isProcessRunning(lockInfo.pid)) {
86
+ return true;
87
+ }
88
+
89
+ // Check if lock has expired
90
+ if (Date.now() - lockInfo.timestamp > staleTimeoutMs) {
91
+ return true;
92
+ }
93
+
94
+ return false;
95
+ }
96
+
97
+ /**
98
+ * Try to acquire a lock synchronously (for backward compatibility)
99
+ */
100
+ export function tryAcquireLockSync(lockFile: string, options: LockOptions = {}): boolean {
101
+ const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };
102
+
103
+ // Check for existing lock
104
+ const existingLock = readLockInfo(lockFile);
105
+ if (existingLock) {
106
+ if (!isLockStale(existingLock, opts.staleTimeoutMs)) {
107
+ return false;
108
+ }
109
+ // Stale lock - remove it
110
+ try {
111
+ fs.unlinkSync(lockFile);
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ // Try to create lock atomically
118
+ const lockInfo: LockInfo = {
119
+ pid: process.pid,
120
+ timestamp: Date.now(),
121
+ operation: opts.operation,
122
+ hostname: require('os').hostname(),
123
+ };
124
+
125
+ try {
126
+ fs.writeFileSync(lockFile, JSON.stringify(lockInfo, null, 2), { flag: 'wx' });
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Release a lock synchronously
135
+ */
136
+ export function releaseLockSync(lockFile: string): void {
137
+ try {
138
+ const lockInfo = readLockInfo(lockFile);
139
+ // Only release if we own the lock
140
+ if (lockInfo && lockInfo.pid === process.pid) {
141
+ fs.unlinkSync(lockFile);
142
+ }
143
+ } catch {
144
+ // Ignore errors
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Acquire a lock with async waiting
150
+ */
151
+ export async function acquireLock(lockFile: string, options: LockOptions = {}): Promise<boolean> {
152
+ const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };
153
+ const startTime = Date.now();
154
+
155
+ while (Date.now() - startTime < opts.timeoutMs) {
156
+ if (tryAcquireLockSync(lockFile, opts)) {
157
+ return true;
158
+ }
159
+
160
+ // Wait before retrying with jitter
161
+ const jitter = Math.random() * opts.retryDelayMs * 0.5;
162
+ await new Promise(resolve => setTimeout(resolve, opts.retryDelayMs + jitter));
163
+ }
164
+
165
+ return false;
166
+ }
167
+
168
+ /**
169
+ * Release a lock
170
+ */
171
+ export async function releaseLock(lockFile: string): Promise<void> {
172
+ releaseLockSync(lockFile);
173
+ }
174
+
175
+ /**
176
+ * Execute a function while holding a lock
177
+ */
178
+ export async function withLock<T>(
179
+ lockFile: string,
180
+ fn: () => Promise<T>,
181
+ options: LockOptions = {}
182
+ ): Promise<T> {
183
+ const acquired = await acquireLock(lockFile, options);
184
+ if (!acquired) {
185
+ throw new Error(`Failed to acquire lock: ${lockFile} (timeout: ${options.timeoutMs || DEFAULT_LOCK_OPTIONS.timeoutMs}ms)`);
186
+ }
187
+
188
+ try {
189
+ return await fn();
190
+ } finally {
191
+ await releaseLock(lockFile);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Execute a synchronous function while holding a lock
197
+ */
198
+ export function withLockSync<T>(
199
+ lockFile: string,
200
+ fn: () => T,
201
+ options: LockOptions = {}
202
+ ): T {
203
+ const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };
204
+ const startTime = Date.now();
205
+
206
+ // Busy wait for lock (synchronous)
207
+ while (Date.now() - startTime < opts.timeoutMs) {
208
+ if (tryAcquireLockSync(lockFile, opts)) {
209
+ try {
210
+ return fn();
211
+ } finally {
212
+ releaseLockSync(lockFile);
213
+ }
214
+ }
215
+
216
+ // Sync sleep
217
+ const end = Date.now() + opts.retryDelayMs;
218
+ while (Date.now() < end) { /* busy wait */ }
219
+ }
220
+
221
+ throw new Error(`Failed to acquire lock: ${lockFile} (timeout: ${opts.timeoutMs}ms)`);
222
+ }
223
+
224
+ /**
225
+ * Clean up stale locks in a directory
226
+ */
227
+ export function cleanStaleLocks(lockDir: string, staleTimeoutMs: number = DEFAULT_LOCK_OPTIONS.staleTimeoutMs): number {
228
+ if (!fs.existsSync(lockDir)) {
229
+ return 0;
230
+ }
231
+
232
+ let cleaned = 0;
233
+ const files = fs.readdirSync(lockDir);
234
+
235
+ for (const file of files) {
236
+ if (!file.endsWith('.lock')) continue;
237
+
238
+ const lockFile = safeJoin(lockDir, file);
239
+ const lockInfo = readLockInfo(lockFile);
240
+
241
+ if (lockInfo && isLockStale(lockInfo, staleTimeoutMs)) {
242
+ try {
243
+ fs.unlinkSync(lockFile);
244
+ cleaned++;
245
+ } catch {
246
+ // Ignore
247
+ }
248
+ }
249
+ }
250
+
251
+ return cleaned;
252
+ }
253
+
254
+ /**
255
+ * Get status of all locks in a directory
256
+ */
257
+ export function getLockStatus(lockDir: string): Array<{ file: string; info: LockInfo; stale: boolean }> {
258
+ if (!fs.existsSync(lockDir)) {
259
+ return [];
260
+ }
261
+
262
+ const result: Array<{ file: string; info: LockInfo; stale: boolean }> = [];
263
+ const files = fs.readdirSync(lockDir);
264
+
265
+ for (const file of files) {
266
+ if (!file.endsWith('.lock')) continue;
267
+
268
+ const lockFile = safeJoin(lockDir, file);
269
+ const lockInfo = readLockInfo(lockFile);
270
+
271
+ if (lockInfo) {
272
+ result.push({
273
+ file,
274
+ info: lockInfo,
275
+ stale: isLockStale(lockInfo, DEFAULT_LOCK_OPTIONS.staleTimeoutMs),
276
+ });
277
+ }
278
+ }
279
+
280
+ return result;
281
+ }
282
+
283
+ /**
284
+ * Named lock manager for managing multiple locks
285
+ */
286
+ export class LockManager {
287
+ private readonly basePath: string;
288
+ private readonly heldLocks: Set<string> = new Set();
289
+
290
+ constructor(basePath: string) {
291
+ this.basePath = basePath;
292
+ ensureLockDir(basePath);
293
+
294
+ // Register cleanup on process exit
295
+ process.on('exit', () => this.releaseAll());
296
+ process.on('SIGINT', () => {
297
+ this.releaseAll();
298
+ process.exit(130);
299
+ });
300
+ process.on('SIGTERM', () => {
301
+ this.releaseAll();
302
+ process.exit(143);
303
+ });
304
+ }
305
+
306
+ private getLockPath(name: string): string {
307
+ return safeJoin(getLockDir(this.basePath), `${name}.lock`);
308
+ }
309
+
310
+ async acquire(name: string, options: LockOptions = {}): Promise<boolean> {
311
+ const lockPath = this.getLockPath(name);
312
+ const acquired = await acquireLock(lockPath, options);
313
+ if (acquired) {
314
+ this.heldLocks.add(name);
315
+ }
316
+ return acquired;
317
+ }
318
+
319
+ async release(name: string): Promise<void> {
320
+ const lockPath = this.getLockPath(name);
321
+ await releaseLock(lockPath);
322
+ this.heldLocks.delete(name);
323
+ }
324
+
325
+ releaseAll(): void {
326
+ for (const name of this.heldLocks) {
327
+ const lockPath = this.getLockPath(name);
328
+ releaseLockSync(lockPath);
329
+ }
330
+ this.heldLocks.clear();
331
+ }
332
+
333
+ async withLock<T>(name: string, fn: () => Promise<T>, options: LockOptions = {}): Promise<T> {
334
+ const lockPath = this.getLockPath(name);
335
+ return withLock(lockPath, fn, options);
336
+ }
337
+
338
+ cleanStale(staleTimeoutMs?: number): number {
339
+ return cleanStaleLocks(getLockDir(this.basePath), staleTimeoutMs);
340
+ }
341
+
342
+ getStatus(): Array<{ file: string; info: LockInfo; stale: boolean }> {
343
+ return getLockStatus(getLockDir(this.basePath));
344
+ }
345
+ }
346
+
@@ -0,0 +1,28 @@
1
+ /**
2
+ * LogBufferService - Scrollable log buffer for TUI monitor
3
+ *
4
+ * This file is kept for backward compatibility.
5
+ * New code should import from '../services/logging/buffer' directly.
6
+ */
7
+
8
+ // Re-export everything from the new services/logging/buffer module
9
+ export {
10
+ LogBufferService,
11
+ createLogBuffer,
12
+ type LogBufferOptions,
13
+ type LogFilter,
14
+ type LogBufferState,
15
+ } from '../services/logging/buffer';
16
+
17
+ // Re-export types from types/logging for backward compatibility
18
+ export type { JsonLogEntry, BufferedLogEntry } from '../types/logging';
19
+
20
+ /**
21
+ * Log viewport interface for backward compatibility
22
+ */
23
+ export interface LogViewport {
24
+ entries: import('../types/logging').BufferedLogEntry[];
25
+ totalCount: number;
26
+ offset: number;
27
+ visibleCount: number;
28
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared log constants to avoid circular dependencies
3
+ */
4
+
5
+ export const COLORS = {
6
+ reset: '\x1b[0m',
7
+ red: '\x1b[31m',
8
+ yellow: '\x1b[33m',
9
+ green: '\x1b[32m',
10
+ blue: '\x1b[34m',
11
+ cyan: '\x1b[36m',
12
+ magenta: '\x1b[35m',
13
+ gray: '\x1b[90m',
14
+ bold: '\x1b[1m',
15
+ white: '\x1b[37m',
16
+ };
17
+
18
+ export enum LogLevel {
19
+ error = 0,
20
+ warn = 1,
21
+ info = 2,
22
+ success = 2,
23
+ progress = 2,
24
+ debug = 3,
25
+ }
26
+
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Utility for formatting log messages for console display
3
+ *
4
+ * Format: [HH:MM:SS] [lane-task] ICON TYPE content
5
+ *
6
+ * Rules:
7
+ * - Box format only for: user, assistant, system, result
8
+ * - Compact format for: tool, tool_result, thinking (gray/dim)
9
+ * - Tool names simplified: ShellToolCall → Shell
10
+ * - Lane labels max 16 chars: [01-types-tests]
11
+ */
12
+
13
+ import { COLORS } from './log-constants';
14
+ import { ParsedMessage, stripAnsi } from './enhanced-logger';
15
+
16
+ // Types that should use box format
17
+ const BOX_TYPES = new Set(['user', 'assistant', 'system', 'result']);
18
+
19
+ /**
20
+ * Simplify tool names (ShellToolCall → Shell, etc.)
21
+ */
22
+ function simplifyToolName(name: string): string {
23
+ // Remove common suffixes
24
+ return name
25
+ .replace(/ToolCall$/i, '')
26
+ .replace(/Tool$/i, '')
27
+ .replace(/^run_terminal_cmd$/i, 'shell')
28
+ .replace(/^search_replace$/i, 'edit');
29
+ }
30
+
31
+ /**
32
+ * Format a single parsed message into a human-readable string (compact or multi-line)
33
+ */
34
+ export function formatMessageForConsole(
35
+ msg: ParsedMessage,
36
+ options: {
37
+ includeTimestamp?: boolean;
38
+ laneLabel?: string;
39
+ compact?: boolean;
40
+ context?: string;
41
+ } = {}
42
+ ): string {
43
+ const { includeTimestamp = true, laneLabel = '', compact = false, context = '' } = options;
44
+ const ts = includeTimestamp ? new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
45
+ const tsPrefix = ts ? `${COLORS.gray}[${ts}]${COLORS.reset} ` : '';
46
+
47
+ // Handle context (e.g. from logger.info) - max 16 chars
48
+ const effectiveLaneLabel = laneLabel || (context ? `[${context}]` : '');
49
+ const truncatedLabel = effectiveLaneLabel.length > 16
50
+ ? effectiveLaneLabel.substring(0, 16)
51
+ : effectiveLaneLabel;
52
+ const labelPrefix = truncatedLabel ? `${COLORS.magenta}${truncatedLabel.padEnd(16)}${COLORS.reset} ` : '';
53
+
54
+ let typePrefix = '';
55
+ let content = msg.content;
56
+
57
+ // Determine if we should use box format
58
+ // Box format only for: user, assistant, system, result (and only when not compact)
59
+ const useBox = !compact && BOX_TYPES.has(msg.type);
60
+
61
+ // Clean up wrapped prompts for user messages to hide internal instructions
62
+ if (msg.type === 'user') {
63
+ const contextMarker = '### 🛠 Environment & Context';
64
+ const instructionsMarker = '### 📝 Final Instructions';
65
+
66
+ if (content.includes(contextMarker)) {
67
+ const parts = content.split('---\n');
68
+ if (parts.length >= 3) {
69
+ content = parts[1]!.trim();
70
+ } else {
71
+ content = content.split(contextMarker).pop() || content;
72
+ content = content.split(instructionsMarker)[0] || content;
73
+ content = content.replace(/^.*---\n/s, '').trim();
74
+ }
75
+ }
76
+ }
77
+
78
+ // For thinking: collapse multiple newlines into single space
79
+ if (msg.type === 'thinking') {
80
+ content = content.replace(/\n\s*\n/g, ' ').replace(/\n/g, ' ').trim();
81
+ }
82
+
83
+ switch (msg.type) {
84
+ case 'user':
85
+ typePrefix = `${COLORS.cyan}🧑 USER${COLORS.reset}`;
86
+ if (!useBox) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
87
+ break;
88
+ case 'assistant':
89
+ typePrefix = `${COLORS.green}🤖 ASST${COLORS.reset}`;
90
+ if (!useBox) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
91
+ break;
92
+ case 'tool':
93
+ // Tool calls are always compact and gray
94
+ typePrefix = `${COLORS.gray}🔧 TOOL${COLORS.reset}`;
95
+ const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
96
+ if (toolMatch) {
97
+ const [, rawName, args] = toolMatch;
98
+ const name = simplifyToolName(rawName!);
99
+ try {
100
+ const parsedArgs = JSON.parse(args!);
101
+ let argStr = '';
102
+ if (rawName === 'read_file' && parsedArgs.target_file) {
103
+ argStr = parsedArgs.target_file;
104
+ } else if (rawName === 'run_terminal_cmd' && parsedArgs.command) {
105
+ argStr = parsedArgs.command;
106
+ } else if (rawName === 'write' && parsedArgs.file_path) {
107
+ argStr = parsedArgs.file_path;
108
+ } else if (rawName === 'search_replace' && parsedArgs.file_path) {
109
+ argStr = parsedArgs.file_path;
110
+ } else {
111
+ const keys = Object.keys(parsedArgs);
112
+ if (keys.length > 0) {
113
+ argStr = String(parsedArgs[keys[0]]).substring(0, 50);
114
+ }
115
+ }
116
+ content = `${COLORS.gray}${name}${COLORS.reset}(${COLORS.gray}${argStr}${COLORS.reset})`;
117
+ } catch {
118
+ content = `${COLORS.gray}${name}${COLORS.reset}: ${args}`;
119
+ }
120
+ }
121
+ break;
122
+ case 'tool_result':
123
+ // Tool results are always compact and gray
124
+ typePrefix = `${COLORS.gray}📄 RESL${COLORS.reset}`;
125
+ const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
126
+ if (resMatch) {
127
+ const simpleName = simplifyToolName(resMatch[1]!);
128
+ content = `${COLORS.gray}${simpleName} OK${COLORS.reset}`;
129
+ } else {
130
+ content = `${COLORS.gray}result${COLORS.reset}`;
131
+ }
132
+ break;
133
+ case 'result':
134
+ case 'success':
135
+ typePrefix = `${COLORS.green}✅ DONE${COLORS.reset}`;
136
+ break;
137
+ case 'system':
138
+ typePrefix = `${COLORS.gray}⚙️ SYS${COLORS.reset}`;
139
+ break;
140
+ case 'thinking':
141
+ // Thinking is always compact and gray
142
+ typePrefix = `${COLORS.gray}🤔 THNK${COLORS.reset}`;
143
+ content = `${COLORS.gray}${content.substring(0, 100)}${content.length > 100 ? '...' : ''}${COLORS.reset}`;
144
+ break;
145
+ case 'info':
146
+ typePrefix = `${COLORS.cyan}ℹ️ INFO${COLORS.reset}`;
147
+ break;
148
+ case 'warn':
149
+ typePrefix = `${COLORS.yellow}⚠️ WARN${COLORS.reset}`;
150
+ break;
151
+ case 'error':
152
+ typePrefix = `${COLORS.red}❌ ERR${COLORS.reset}`;
153
+ break;
154
+ }
155
+
156
+ if (!typePrefix) return `${tsPrefix}${labelPrefix}${content}`;
157
+
158
+ // Compact format (single line)
159
+ if (!useBox) {
160
+ return `${tsPrefix}${labelPrefix}${typePrefix.padEnd(12)} ${content}`;
161
+ }
162
+
163
+ // Multi-line box format (only for user, assistant, system, result)
164
+ // Emoji width is 2, so we need to account for that in indent calculation
165
+ const lines = content.split('\n');
166
+ const fullPrefix = `${tsPrefix}${labelPrefix}`;
167
+ const strippedPrefix = stripAnsi(typePrefix);
168
+ // Count emojis (they take 2 terminal columns but 1-2 chars in string)
169
+ const emojiCount = (strippedPrefix.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2300}-\u{23FF}]|[\u{2B50}-\u{2B55}]|[\u{231A}-\u{231B}]|[\u{23E9}-\u{23F3}]|[\u{23F8}-\u{23FA}]|✅|❌|⚙️|ℹ️|⚠️|🔧|📄|🤔|🧑|🤖/gu) || []).length;
170
+ const visualWidth = strippedPrefix.length + emojiCount; // emoji adds 1 extra width
171
+
172
+ const boxWidth = 60;
173
+ const header = `${typePrefix}┌${'─'.repeat(boxWidth)}`;
174
+ let result = `${fullPrefix}${header}\n`;
175
+
176
+ const indent = ' '.repeat(visualWidth);
177
+ for (const line of lines) {
178
+ result += `${fullPrefix}${indent}│ ${line}\n`;
179
+ }
180
+ result += `${fullPrefix}${indent}└${'─'.repeat(boxWidth)}`;
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * Detect and format a message that might be a raw JSON string from cursor-agent
187
+ */
188
+ export function formatPotentialJsonMessage(message: string): string {
189
+ const trimmed = message.trim();
190
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
191
+ return message;
192
+ }
193
+
194
+ try {
195
+ const json = JSON.parse(trimmed);
196
+ if (!json.type) return message;
197
+
198
+ // Convert JSON to a ParsedMessage-like structure for formatting
199
+ let content: string;
200
+ let type: string;
201
+
202
+ if (json.type === 'thinking' && json.text) {
203
+ content = json.text;
204
+ type = 'thinking';
205
+ } else if (json.type === 'assistant' && json.message?.content) {
206
+ content = json.message.content
207
+ .filter((c: any) => c.type === 'text')
208
+ .map((c: any) => c.text)
209
+ .join('');
210
+ type = 'assistant';
211
+ } else if (json.type === 'user' && json.message?.content) {
212
+ content = json.message.content
213
+ .filter((c: any) => c.type === 'text')
214
+ .map((c: any) => c.text)
215
+ .join('');
216
+ type = 'user';
217
+ } else if (json.type === 'tool_call' && json.subtype === 'started') {
218
+ const rawToolName = Object.keys(json.tool_call)[0] || 'unknown';
219
+ const args = json.tool_call[rawToolName]?.args || {};
220
+ // Tool name will be simplified in formatMessageForConsole
221
+ content = `[Tool: ${rawToolName}] ${JSON.stringify(args)}`;
222
+ type = 'tool';
223
+ } else if (json.type === 'tool_call' && json.subtype === 'completed') {
224
+ const rawToolName = Object.keys(json.tool_call)[0] || 'unknown';
225
+ content = `[Tool Result: ${rawToolName}]`;
226
+ type = 'tool_result';
227
+ } else if (json.type === 'result') {
228
+ content = json.result || 'Task completed';
229
+ type = 'result';
230
+ } else {
231
+ // Unknown type, return as is
232
+ return message;
233
+ }
234
+
235
+ return formatMessageForConsole({
236
+ type: type as any,
237
+ role: type,
238
+ content,
239
+ timestamp: json.timestamp_ms || Date.now()
240
+ }, { includeTimestamp: false, compact: true });
241
+
242
+ } catch {
243
+ return message;
244
+ }
245
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Log Service - Helpers for log processing and filtering
3
+ */
4
+
5
+ import { JsonLogEntry } from './enhanced-logger';
6
+ import { LogImportance } from './types';
7
+
8
+ export interface MergedLogEntry extends JsonLogEntry {
9
+ laneName: string;
10
+ laneColor: string;
11
+ }
12
+
13
+ export class LogService {
14
+ /**
15
+ * Determine importance level of a log entry
16
+ */
17
+ static getLogImportance(entry: JsonLogEntry): LogImportance {
18
+ if (entry.level === 'error') return LogImportance.CRITICAL;
19
+ if (entry.level === 'stderr') return LogImportance.HIGH;
20
+
21
+ const msg = (entry.message || '').toLowerCase();
22
+ if (msg.includes('error') || msg.includes('fail')) return LogImportance.HIGH;
23
+ if (msg.includes('warn')) return LogImportance.MEDIUM;
24
+ if (msg.includes('success') || msg.includes('done') || msg.includes('completed')) return LogImportance.LOW;
25
+
26
+ if (entry.level === 'debug') return LogImportance.DEBUG;
27
+ return LogImportance.INFO;
28
+ }
29
+
30
+ /**
31
+ * Check if an entry meets the minimum importance level
32
+ */
33
+ static meetsImportanceLevel(entry: JsonLogEntry, minLevel: LogImportance): boolean {
34
+ const entryLevel = this.getLogImportance(entry);
35
+ const levels = [
36
+ LogImportance.DEBUG,
37
+ LogImportance.INFO,
38
+ LogImportance.LOW,
39
+ LogImportance.MEDIUM,
40
+ LogImportance.HIGH,
41
+ LogImportance.CRITICAL
42
+ ];
43
+
44
+ const entryIdx = levels.indexOf(entryLevel);
45
+ const minIdx = levels.indexOf(minLevel);
46
+
47
+ return entryIdx >= minIdx;
48
+ }
49
+ }