@litmers/cursorflow-orchestrator 0.1.20 → 0.1.28

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 (224) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +87 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +125 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +397 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +97 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +154 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1136 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +1443 -1217
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +871 -499
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
@@ -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
+
@@ -1,10 +1,33 @@
1
1
  /**
2
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]
3
11
  */
4
12
 
5
- import * as logger from './logger';
13
+ import { COLORS } from './log-constants';
6
14
  import { ParsedMessage, stripAnsi } from './enhanced-logger';
7
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
+
8
31
  /**
9
32
  * Format a single parsed message into a human-readable string (compact or multi-line)
10
33
  */
@@ -14,40 +37,75 @@ export function formatMessageForConsole(
14
37
  includeTimestamp?: boolean;
15
38
  laneLabel?: string;
16
39
  compact?: boolean;
40
+ context?: string;
17
41
  } = {}
18
42
  ): string {
19
- const { includeTimestamp = true, laneLabel = '', compact = false } = options;
43
+ const { includeTimestamp = true, laneLabel = '', compact = false, context = '' } = options;
20
44
  const ts = includeTimestamp ? new Date(msg.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
21
- const tsPrefix = ts ? `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ` : '';
22
- const labelPrefix = laneLabel ? `${logger.COLORS.magenta}${laneLabel.padEnd(12)}${logger.COLORS.reset} ` : '';
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} ` : '';
23
53
 
24
54
  let typePrefix = '';
25
55
  let content = msg.content;
26
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
+
27
83
  switch (msg.type) {
28
84
  case 'user':
29
- typePrefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
30
- if (compact) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
85
+ typePrefix = `${COLORS.cyan}🧑 USER${COLORS.reset}`;
86
+ if (!useBox) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
31
87
  break;
32
88
  case 'assistant':
33
- typePrefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
34
- if (compact) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
89
+ typePrefix = `${COLORS.green}🤖 ASST${COLORS.reset}`;
90
+ if (!useBox) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
35
91
  break;
36
92
  case 'tool':
37
- typePrefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
93
+ // Tool calls are always compact and gray
94
+ typePrefix = `${COLORS.gray}🔧 TOOL${COLORS.reset}`;
38
95
  const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
39
96
  if (toolMatch) {
40
- const [, name, args] = toolMatch;
97
+ const [, rawName, args] = toolMatch;
98
+ const name = simplifyToolName(rawName!);
41
99
  try {
42
100
  const parsedArgs = JSON.parse(args!);
43
101
  let argStr = '';
44
- if (name === 'read_file' && parsedArgs.target_file) {
102
+ if (rawName === 'read_file' && parsedArgs.target_file) {
45
103
  argStr = parsedArgs.target_file;
46
- } else if (name === 'run_terminal_cmd' && parsedArgs.command) {
104
+ } else if (rawName === 'run_terminal_cmd' && parsedArgs.command) {
47
105
  argStr = parsedArgs.command;
48
- } else if (name === 'write' && parsedArgs.file_path) {
106
+ } else if (rawName === 'write' && parsedArgs.file_path) {
49
107
  argStr = parsedArgs.file_path;
50
- } else if (name === 'search_replace' && parsedArgs.file_path) {
108
+ } else if (rawName === 'search_replace' && parsedArgs.file_path) {
51
109
  argStr = parsedArgs.file_path;
52
110
  } else {
53
111
  const keys = Object.keys(parsedArgs);
@@ -55,46 +113,71 @@ export function formatMessageForConsole(
55
113
  argStr = String(parsedArgs[keys[0]]).substring(0, 50);
56
114
  }
57
115
  }
58
- content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
116
+ content = `${COLORS.gray}${name}${COLORS.reset}(${COLORS.gray}${argStr}${COLORS.reset})`;
59
117
  } catch {
60
- content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
118
+ content = `${COLORS.gray}${name}${COLORS.reset}: ${args}`;
61
119
  }
62
120
  }
63
121
  break;
64
122
  case 'tool_result':
65
- typePrefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
123
+ // Tool results are always compact and gray
124
+ typePrefix = `${COLORS.gray}📄 RESL${COLORS.reset}`;
66
125
  const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
67
- content = resMatch ? `${resMatch[1]} OK` : '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
+ }
68
132
  break;
69
133
  case 'result':
70
- typePrefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
134
+ case 'success':
135
+ typePrefix = `${COLORS.green}✅ DONE${COLORS.reset}`;
71
136
  break;
72
137
  case 'system':
73
- typePrefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
138
+ typePrefix = `${COLORS.gray}⚙️ SYS${COLORS.reset}`;
74
139
  break;
75
140
  case 'thinking':
76
- typePrefix = `${logger.COLORS.gray}🤔 THNK${logger.COLORS.reset}`;
77
- if (compact) content = content.replace(/\n/g, ' ').substring(0, 100) + (content.length > 100 ? '...' : '');
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}`;
78
153
  break;
79
154
  }
80
155
 
81
156
  if (!typePrefix) return `${tsPrefix}${labelPrefix}${content}`;
82
157
 
83
- if (compact) {
84
- return `${tsPrefix}${labelPrefix}${typePrefix} ${content}`;
158
+ // Compact format (single line)
159
+ if (!useBox) {
160
+ return `${tsPrefix}${labelPrefix}${typePrefix.padEnd(12)} ${content}`;
85
161
  }
86
162
 
87
- // Multi-line box format (as seen in orchestrator)
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
88
165
  const lines = content.split('\n');
89
166
  const fullPrefix = `${tsPrefix}${labelPrefix}`;
90
- const header = `${typePrefix} ┌${'─'.repeat(60)}`;
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)}`;
91
174
  let result = `${fullPrefix}${header}\n`;
92
175
 
93
- const indent = ' '.repeat(stripAnsi(typePrefix).length);
176
+ const indent = ' '.repeat(visualWidth);
94
177
  for (const line of lines) {
95
- result += `${fullPrefix}${indent} │ ${line}\n`;
178
+ result += `${fullPrefix}${indent}│ ${line}\n`;
96
179
  }
97
- result += `${fullPrefix}${indent} └${'─'.repeat(60)}`;
180
+ result += `${fullPrefix}${indent}└${'─'.repeat(boxWidth)}`;
98
181
 
99
182
  return result;
100
183
  }
@@ -113,8 +196,8 @@ export function formatPotentialJsonMessage(message: string): string {
113
196
  if (!json.type) return message;
114
197
 
115
198
  // Convert JSON to a ParsedMessage-like structure for formatting
116
- let content = trimmed;
117
- let type = 'system';
199
+ let content: string;
200
+ let type: string;
118
201
 
119
202
  if (json.type === 'thinking' && json.text) {
120
203
  content = json.text;
@@ -132,13 +215,14 @@ export function formatPotentialJsonMessage(message: string): string {
132
215
  .join('');
133
216
  type = 'user';
134
217
  } else if (json.type === 'tool_call' && json.subtype === 'started') {
135
- const toolName = Object.keys(json.tool_call)[0] || 'unknown';
136
- const args = json.tool_call[toolName]?.args || {};
137
- content = `[Tool: ${toolName}] ${JSON.stringify(args)}`;
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)}`;
138
222
  type = 'tool';
139
223
  } else if (json.type === 'tool_call' && json.subtype === 'completed') {
140
- const toolName = Object.keys(json.tool_call)[0] || 'unknown';
141
- content = `[Tool Result: ${toolName}]`;
224
+ const rawToolName = Object.keys(json.tool_call)[0] || 'unknown';
225
+ content = `[Tool Result: ${rawToolName}]`;
142
226
  type = 'tool_result';
143
227
  } else if (json.type === 'result') {
144
228
  content = json.result || 'Task completed';
@@ -159,4 +243,3 @@ export function formatPotentialJsonMessage(message: string): string {
159
243
  return message;
160
244
  }
161
245
  }
162
-
@@ -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
+ }