@virtengine/openfleet 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
package/lib/logger.mjs
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.mjs — Centralized logging for openfleet.
|
|
3
|
+
*
|
|
4
|
+
* Provides leveled logging that separates human-facing CLI output from
|
|
5
|
+
* debug/trace noise. All messages are always written to the log file;
|
|
6
|
+
* only messages at or above the configured console level appear in the terminal.
|
|
7
|
+
*
|
|
8
|
+
* Levels (lowest to highest):
|
|
9
|
+
* TRACE → DEBUG → INFO → WARN → ERROR → SILENT
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { createLogger, setLogLevel, setLogFile } from "./lib/logger.mjs";
|
|
13
|
+
*
|
|
14
|
+
* const log = createLogger("monitor");
|
|
15
|
+
* log.info("Task completed"); // Shown in terminal (default)
|
|
16
|
+
* log.debug("Cache hit ratio: 0.95"); // Hidden in terminal, written to log file
|
|
17
|
+
* log.trace("Processing line 42"); // Only in log file at TRACE level
|
|
18
|
+
* log.error("Fatal: no config"); // Always shown
|
|
19
|
+
*
|
|
20
|
+
* The module prefix (e.g. [monitor]) is automatically prepended.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { dirname } from "node:path";
|
|
25
|
+
import { stripAnsi } from "../utils.mjs";
|
|
26
|
+
|
|
27
|
+
// ── Log levels ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export const LogLevel = /** @type {const} */ ({
|
|
30
|
+
TRACE: 0,
|
|
31
|
+
DEBUG: 1,
|
|
32
|
+
INFO: 2,
|
|
33
|
+
WARN: 3,
|
|
34
|
+
ERROR: 4,
|
|
35
|
+
SILENT: 5,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** @type {Record<string, number>} */
|
|
39
|
+
const LEVEL_MAP = {
|
|
40
|
+
trace: LogLevel.TRACE,
|
|
41
|
+
debug: LogLevel.DEBUG,
|
|
42
|
+
info: LogLevel.INFO,
|
|
43
|
+
warn: LogLevel.WARN,
|
|
44
|
+
error: LogLevel.ERROR,
|
|
45
|
+
silent: LogLevel.SILENT,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── Global state ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** @type {number} Console output threshold — messages below this level are suppressed in terminal */
|
|
51
|
+
let consoleLevel = LogLevel.INFO;
|
|
52
|
+
|
|
53
|
+
/** @type {number} File output threshold — messages below this level are not written to log file */
|
|
54
|
+
let fileLevel = LogLevel.DEBUG;
|
|
55
|
+
|
|
56
|
+
/** @type {string|null} Path to the log file */
|
|
57
|
+
let logFilePath = null;
|
|
58
|
+
|
|
59
|
+
/** @type {string|null} Path to the dedicated error/stderr log file */
|
|
60
|
+
let errorLogFilePath = null;
|
|
61
|
+
|
|
62
|
+
/** @type {boolean} Whether the log file directory has been ensured */
|
|
63
|
+
let logDirEnsured = false;
|
|
64
|
+
|
|
65
|
+
/** @type {boolean} Whether the error log directory has been ensured */
|
|
66
|
+
let errorLogDirEnsured = false;
|
|
67
|
+
|
|
68
|
+
/** @type {Set<string>} Modules to always show at DEBUG level even when console is at INFO */
|
|
69
|
+
const verboseModules = new Set();
|
|
70
|
+
|
|
71
|
+
// ── Configuration ───────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set the minimum console log level.
|
|
75
|
+
* @param {string|number} level - Level name ("trace", "debug", "info", "warn", "error", "silent") or LogLevel value
|
|
76
|
+
*/
|
|
77
|
+
export function setConsoleLevel(level) {
|
|
78
|
+
if (typeof level === "string") {
|
|
79
|
+
consoleLevel = LEVEL_MAP[level.toLowerCase()] ?? LogLevel.INFO;
|
|
80
|
+
} else {
|
|
81
|
+
consoleLevel = level;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Set the minimum file log level.
|
|
87
|
+
* @param {string|number} level
|
|
88
|
+
*/
|
|
89
|
+
export function setFileLevel(level) {
|
|
90
|
+
if (typeof level === "string") {
|
|
91
|
+
fileLevel = LEVEL_MAP[level.toLowerCase()] ?? LogLevel.DEBUG;
|
|
92
|
+
} else {
|
|
93
|
+
fileLevel = level;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Set the log file path. All messages at or above the file level are appended here.
|
|
99
|
+
* @param {string|null} path
|
|
100
|
+
*/
|
|
101
|
+
export function setLogFile(path) {
|
|
102
|
+
logFilePath = path;
|
|
103
|
+
logDirEnsured = false;
|
|
104
|
+
ensureLogFile(logFilePath, (value) => {
|
|
105
|
+
logDirEnsured = value;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Set the dedicated error log file path.
|
|
111
|
+
* WARN, ERROR, uncaughtException/unhandledRejection, and raw stderr writes are appended here.
|
|
112
|
+
* @param {string|null} path
|
|
113
|
+
*/
|
|
114
|
+
export function setErrorLogFile(path) {
|
|
115
|
+
errorLogFilePath = path;
|
|
116
|
+
errorLogDirEnsured = false;
|
|
117
|
+
ensureLogFile(errorLogFilePath, (value) => {
|
|
118
|
+
errorLogDirEnsured = value;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the current error log file path.
|
|
124
|
+
* @returns {string|null}
|
|
125
|
+
*/
|
|
126
|
+
export function getErrorLogFilePath() {
|
|
127
|
+
return errorLogFilePath;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parse CLI args for logging flags and configure accordingly.
|
|
132
|
+
* Call this once at startup before any logging.
|
|
133
|
+
*
|
|
134
|
+
* Flags:
|
|
135
|
+
* --quiet Only show errors and warnings
|
|
136
|
+
* --verbose Show debug messages too
|
|
137
|
+
* --trace Show everything
|
|
138
|
+
* --silent No console output
|
|
139
|
+
* --log-level X Set explicit level
|
|
140
|
+
*
|
|
141
|
+
* @param {string[]} argv
|
|
142
|
+
*/
|
|
143
|
+
export function configureFromArgs(argv) {
|
|
144
|
+
if (argv.includes("--silent")) {
|
|
145
|
+
setConsoleLevel(LogLevel.SILENT);
|
|
146
|
+
} else if (argv.includes("--quiet") || argv.includes("-q")) {
|
|
147
|
+
setConsoleLevel(LogLevel.WARN);
|
|
148
|
+
} else if (argv.includes("--trace")) {
|
|
149
|
+
setConsoleLevel(LogLevel.TRACE);
|
|
150
|
+
} else if (argv.includes("--verbose") || argv.includes("-V")) {
|
|
151
|
+
setConsoleLevel(LogLevel.DEBUG);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const levelIdx = argv.indexOf("--log-level");
|
|
155
|
+
if (levelIdx !== -1 && argv[levelIdx + 1]) {
|
|
156
|
+
setConsoleLevel(argv[levelIdx + 1]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Enable verbose (DEBUG-level) output for specific modules even at INFO level.
|
|
162
|
+
* @param {string[]} modules
|
|
163
|
+
*/
|
|
164
|
+
export function setVerboseModules(modules) {
|
|
165
|
+
verboseModules.clear();
|
|
166
|
+
for (const m of modules) verboseModules.add(m.toLowerCase());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get the current console log level.
|
|
171
|
+
* @returns {number}
|
|
172
|
+
*/
|
|
173
|
+
export function getConsoleLevel() {
|
|
174
|
+
return consoleLevel;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Timestamp ───────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function timestamp() {
|
|
180
|
+
const d = new Date();
|
|
181
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
182
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
183
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
184
|
+
return `${hh}:${mm}:${ss}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function datestamp() {
|
|
188
|
+
return new Date().toISOString();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function ensureLogFile(path, markEnsured) {
|
|
192
|
+
if (!path) return;
|
|
193
|
+
try {
|
|
194
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
195
|
+
appendFileSync(path, "");
|
|
196
|
+
if (markEnsured) markEnsured(true);
|
|
197
|
+
} catch {
|
|
198
|
+
if (markEnsured) markEnsured(false);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── File writing ────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
function writeToFile(levelName, module, msg) {
|
|
205
|
+
if (!logFilePath) return;
|
|
206
|
+
if (!logDirEnsured) {
|
|
207
|
+
try {
|
|
208
|
+
mkdirSync(dirname(logFilePath), { recursive: true });
|
|
209
|
+
} catch {
|
|
210
|
+
/* best effort */
|
|
211
|
+
}
|
|
212
|
+
logDirEnsured = true;
|
|
213
|
+
}
|
|
214
|
+
const clean = typeof msg === "string" ? stripAnsi(msg) : String(msg);
|
|
215
|
+
const line = `${datestamp()} [${levelName}] [${module}] ${clean}\n`;
|
|
216
|
+
try {
|
|
217
|
+
appendFileSync(logFilePath, line);
|
|
218
|
+
} catch {
|
|
219
|
+
/* best effort */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Write a message to the dedicated error log file.
|
|
225
|
+
* Called for WARN, ERROR, and raw stderr writes.
|
|
226
|
+
* @param {string} levelName
|
|
227
|
+
* @param {string} module
|
|
228
|
+
* @param {string} msg
|
|
229
|
+
*/
|
|
230
|
+
function writeToErrorFile(levelName, module, msg) {
|
|
231
|
+
if (!errorLogFilePath) return;
|
|
232
|
+
if (!errorLogDirEnsured) {
|
|
233
|
+
try {
|
|
234
|
+
mkdirSync(dirname(errorLogFilePath), { recursive: true });
|
|
235
|
+
} catch {
|
|
236
|
+
/* best effort */
|
|
237
|
+
}
|
|
238
|
+
errorLogDirEnsured = true;
|
|
239
|
+
}
|
|
240
|
+
const clean = typeof msg === "string" ? stripAnsi(msg) : String(msg);
|
|
241
|
+
const line = `${datestamp()} [${levelName}] [${module}] ${clean}\n`;
|
|
242
|
+
try {
|
|
243
|
+
appendFileSync(errorLogFilePath, line);
|
|
244
|
+
} catch {
|
|
245
|
+
/* best effort */
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Logger factory ──────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @typedef {Object} Logger
|
|
253
|
+
* @property {(...args: any[]) => void} error - Always shown in terminal
|
|
254
|
+
* @property {(...args: any[]) => void} warn - Shown at WARN+ level
|
|
255
|
+
* @property {(...args: any[]) => void} info - Shown at INFO+ level (default)
|
|
256
|
+
* @property {(...args: any[]) => void} debug - Shown at DEBUG+ level or via --verbose
|
|
257
|
+
* @property {(...args: any[]) => void} trace - Shown at TRACE+ level or via --trace
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Create a logger for a specific module.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} module - Module name (e.g. "monitor", "fleet", "telegram-bot")
|
|
264
|
+
* @returns {Logger}
|
|
265
|
+
*/
|
|
266
|
+
export function createLogger(module) {
|
|
267
|
+
const prefix = `[${module}]`;
|
|
268
|
+
const moduleLower = module.toLowerCase();
|
|
269
|
+
|
|
270
|
+
function emit(level, levelName, consoleFn, args) {
|
|
271
|
+
const msg = args
|
|
272
|
+
.map((a) => (typeof a === "string" ? a : String(a)))
|
|
273
|
+
.join(" ");
|
|
274
|
+
|
|
275
|
+
// Always write to file if above file threshold
|
|
276
|
+
if (level >= fileLevel) {
|
|
277
|
+
writeToFile(levelName, module, msg);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Console output: check level threshold
|
|
281
|
+
// Module-specific verbose override: show DEBUG for this module even at INFO level
|
|
282
|
+
const effectiveLevel =
|
|
283
|
+
level === LogLevel.DEBUG && verboseModules.has(moduleLower)
|
|
284
|
+
? LogLevel.INFO
|
|
285
|
+
: level;
|
|
286
|
+
|
|
287
|
+
if (effectiveLevel >= consoleLevel) {
|
|
288
|
+
const ts = timestamp();
|
|
289
|
+
consoleFn(` ${ts} ${prefix} ${msg}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
error: (...args) => emit(LogLevel.ERROR, "ERROR", console.error, args),
|
|
295
|
+
warn: (...args) => emit(LogLevel.WARN, "WARN", console.warn, args),
|
|
296
|
+
info: (...args) => emit(LogLevel.INFO, "INFO", console.log, args),
|
|
297
|
+
debug: (...args) => emit(LogLevel.DEBUG, "DEBUG", console.log, args),
|
|
298
|
+
trace: (...args) => emit(LogLevel.TRACE, "TRACE", console.log, args),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Console interceptor ─────────────────────────────────────────────────────
|
|
303
|
+
//
|
|
304
|
+
// Intercepts console.log / console.warn / console.error globally and routes
|
|
305
|
+
// messages through the leveled logger. This lets us filter 700+ existing
|
|
306
|
+
// console.* calls without touching every call-site.
|
|
307
|
+
//
|
|
308
|
+
// Classification rules:
|
|
309
|
+
// 1. Messages starting with `[tag] ` are auto-classified by tag + keywords.
|
|
310
|
+
// 2. Messages without a tag prefix pass through as INFO (human-facing output).
|
|
311
|
+
// 3. console.warn → WARN, console.error → ERROR (always pass through).
|
|
312
|
+
//
|
|
313
|
+
|
|
314
|
+
/** @type {boolean} */
|
|
315
|
+
let interceptorInstalled = false;
|
|
316
|
+
|
|
317
|
+
// Keywords that promote a tagged message to INFO — MUST be narrow.
|
|
318
|
+
// Only truly human-critical events that require operator attention.
|
|
319
|
+
const INFO_KEYWORDS = [
|
|
320
|
+
"fatal",
|
|
321
|
+
"crash",
|
|
322
|
+
"circuit breaker",
|
|
323
|
+
"self-restart",
|
|
324
|
+
"shutting down",
|
|
325
|
+
"all tasks complete",
|
|
326
|
+
"stuck",
|
|
327
|
+
"preflight failed",
|
|
328
|
+
"manual resolution",
|
|
329
|
+
"backlog empty",
|
|
330
|
+
"permanently failed",
|
|
331
|
+
"retries exhausted",
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
// Keywords that push a tagged message down to TRACE (very noisy internals)
|
|
335
|
+
const TRACE_KEYWORDS = [
|
|
336
|
+
"skipping",
|
|
337
|
+
"dedup",
|
|
338
|
+
"rate limit",
|
|
339
|
+
"throttl",
|
|
340
|
+
"already in progress",
|
|
341
|
+
"no change",
|
|
342
|
+
"unchanged",
|
|
343
|
+
"cache miss",
|
|
344
|
+
"cache hit",
|
|
345
|
+
"nothing to",
|
|
346
|
+
"same as last",
|
|
347
|
+
"too soon",
|
|
348
|
+
"polling",
|
|
349
|
+
"heartbeat",
|
|
350
|
+
"ping",
|
|
351
|
+
"byte",
|
|
352
|
+
"chunk",
|
|
353
|
+
"offset",
|
|
354
|
+
"cursor",
|
|
355
|
+
"checking port",
|
|
356
|
+
"line count",
|
|
357
|
+
"no old completed",
|
|
358
|
+
"cooldown",
|
|
359
|
+
"score:",
|
|
360
|
+
"attempt ",
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
// Modules whose tagged messages default to DEBUG unless INFO_KEYWORDS match
|
|
364
|
+
const DEBUG_MODULES = new Set([
|
|
365
|
+
"monitor",
|
|
366
|
+
"fleet",
|
|
367
|
+
"telegram-bot",
|
|
368
|
+
"telegram",
|
|
369
|
+
"vk",
|
|
370
|
+
"vk-log",
|
|
371
|
+
"vk-dispatch",
|
|
372
|
+
"workspace-monitor",
|
|
373
|
+
"maintenance",
|
|
374
|
+
"autofix",
|
|
375
|
+
"config",
|
|
376
|
+
"conflict",
|
|
377
|
+
"merge-strategy",
|
|
378
|
+
"task-complexity",
|
|
379
|
+
"shared-knowledge",
|
|
380
|
+
"preflight",
|
|
381
|
+
"codex-config",
|
|
382
|
+
"task-archiver",
|
|
383
|
+
"update-check",
|
|
384
|
+
"restart-controller",
|
|
385
|
+
"sdk-conflict",
|
|
386
|
+
"fleet-coordinator",
|
|
387
|
+
"conflict-resolver",
|
|
388
|
+
"primary-agent",
|
|
389
|
+
"task-assessment",
|
|
390
|
+
"setup",
|
|
391
|
+
"analyze",
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
// Pattern: [tag] message or [tag] message (extra space)
|
|
395
|
+
const TAG_RE = /^\[([^\]]+)\]\s*/;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Classify a console.log message into a LogLevel.
|
|
399
|
+
* @param {string} msg - The first argument to console.log
|
|
400
|
+
* @returns {number} LogLevel
|
|
401
|
+
*/
|
|
402
|
+
function classifyMessage(msg) {
|
|
403
|
+
if (typeof msg !== "string") return LogLevel.INFO;
|
|
404
|
+
|
|
405
|
+
const tagMatch = msg.match(TAG_RE);
|
|
406
|
+
if (!tagMatch) {
|
|
407
|
+
// No [tag] prefix — human-facing output, always INFO
|
|
408
|
+
return LogLevel.INFO;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const tag = tagMatch[1].toLowerCase();
|
|
412
|
+
const body = msg.slice(tagMatch[0].length).toLowerCase();
|
|
413
|
+
|
|
414
|
+
// Check for TRACE keywords first (most restrictive)
|
|
415
|
+
for (const kw of TRACE_KEYWORDS) {
|
|
416
|
+
if (body.includes(kw)) return LogLevel.TRACE;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check for INFO keywords (important events worth showing)
|
|
420
|
+
for (const kw of INFO_KEYWORDS) {
|
|
421
|
+
if (body.includes(kw)) return LogLevel.INFO;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If the module is in our DEBUG set, default to DEBUG
|
|
425
|
+
if (DEBUG_MODULES.has(tag)) return LogLevel.DEBUG;
|
|
426
|
+
|
|
427
|
+
// Unknown tags → INFO (assume user-facing)
|
|
428
|
+
return LogLevel.INFO;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Install the global console interceptor.
|
|
433
|
+
* Call once at startup before any significant logging.
|
|
434
|
+
*
|
|
435
|
+
* This replaces console.log / console.warn / console.error with filtered
|
|
436
|
+
* versions that respect the configured console level.
|
|
437
|
+
*
|
|
438
|
+
* - console.error → always ERROR level (passes through)
|
|
439
|
+
* - console.warn → always WARN level
|
|
440
|
+
* - console.log → auto-classified by tag/content
|
|
441
|
+
*
|
|
442
|
+
* @param {Object} [opts]
|
|
443
|
+
* @param {string} [opts.logFile] - Path to the log file
|
|
444
|
+
*/
|
|
445
|
+
export function installConsoleInterceptor(opts = {}) {
|
|
446
|
+
if (interceptorInstalled) return;
|
|
447
|
+
interceptorInstalled = true;
|
|
448
|
+
|
|
449
|
+
if (opts.logFile) setLogFile(opts.logFile);
|
|
450
|
+
|
|
451
|
+
const _origLog = console.log.bind(console);
|
|
452
|
+
const _origWarn = console.warn.bind(console);
|
|
453
|
+
const _origError = console.error.bind(console);
|
|
454
|
+
|
|
455
|
+
// Replace console.log with filtered version
|
|
456
|
+
console.log = (...args) => {
|
|
457
|
+
const first = args[0];
|
|
458
|
+
const level = classifyMessage(first);
|
|
459
|
+
|
|
460
|
+
// Always write to file
|
|
461
|
+
if (logFilePath && level >= fileLevel) {
|
|
462
|
+
const msg = args
|
|
463
|
+
.map((a) => (typeof a === "string" ? a : String(a)))
|
|
464
|
+
.join(" ");
|
|
465
|
+
const tagMatch = typeof first === "string" ? first.match(TAG_RE) : null;
|
|
466
|
+
const mod = tagMatch ? tagMatch[1] : "stdout";
|
|
467
|
+
const levelName =
|
|
468
|
+
Object.keys(LogLevel).find((k) => LogLevel[k] === level) || "INFO";
|
|
469
|
+
writeToFile(levelName, mod, msg);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Console output
|
|
473
|
+
if (level >= consoleLevel) {
|
|
474
|
+
_origLog(...args);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// console.warn → always WARN level
|
|
479
|
+
console.warn = (...args) => {
|
|
480
|
+
_inInterceptor = true;
|
|
481
|
+
try {
|
|
482
|
+
const msg = args
|
|
483
|
+
.map((a) => (typeof a === "string" ? a : String(a)))
|
|
484
|
+
.join(" ");
|
|
485
|
+
const tagMatch = typeof msg === "string" ? msg.match(TAG_RE) : null;
|
|
486
|
+
const mod = tagMatch?.[1] || "stderr";
|
|
487
|
+
if (logFilePath && LogLevel.WARN >= fileLevel) {
|
|
488
|
+
writeToFile("WARN", mod, msg);
|
|
489
|
+
}
|
|
490
|
+
// Also write to dedicated error log
|
|
491
|
+
writeToErrorFile("WARN", mod, msg);
|
|
492
|
+
if (LogLevel.WARN >= consoleLevel) {
|
|
493
|
+
_origWarn(...args);
|
|
494
|
+
}
|
|
495
|
+
} finally {
|
|
496
|
+
_inInterceptor = false;
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// console.error → always ERROR level (always passes through)
|
|
501
|
+
console.error = (...args) => {
|
|
502
|
+
_inInterceptor = true;
|
|
503
|
+
try {
|
|
504
|
+
const msg = args
|
|
505
|
+
.map((a) => {
|
|
506
|
+
if (a instanceof Error) return a.stack || a.message;
|
|
507
|
+
return typeof a === "string" ? a : String(a);
|
|
508
|
+
})
|
|
509
|
+
.join(" ");
|
|
510
|
+
const tagMatch = typeof msg === "string" ? msg.match(TAG_RE) : null;
|
|
511
|
+
const mod = tagMatch?.[1] || "stderr";
|
|
512
|
+
if (logFilePath && LogLevel.ERROR >= fileLevel) {
|
|
513
|
+
writeToFile("ERROR", mod, msg);
|
|
514
|
+
}
|
|
515
|
+
// Also write to dedicated error log
|
|
516
|
+
writeToErrorFile("ERROR", mod, msg);
|
|
517
|
+
// Errors always pass through
|
|
518
|
+
_origError(...args);
|
|
519
|
+
} finally {
|
|
520
|
+
_inInterceptor = false;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// ── Intercept raw process.stderr.write ────────────────────────────────
|
|
525
|
+
// Some code (uncaughtException handlers, native Node errors, child_process
|
|
526
|
+
// stderr leaks) writes to process.stderr directly, bypassing console.
|
|
527
|
+
// Tee those writes to the error log file, but skip writes that originate
|
|
528
|
+
// from our own console.warn/error interceptors to avoid duplicates.
|
|
529
|
+
let _inInterceptor = false;
|
|
530
|
+
const _origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
531
|
+
let _stderrBroken = false;
|
|
532
|
+
const isBenignStderrError = (err) =>
|
|
533
|
+
!!(
|
|
534
|
+
err &&
|
|
535
|
+
(err.code === "EPIPE" ||
|
|
536
|
+
err.code === "EIO" ||
|
|
537
|
+
err.code === "ERR_STREAM_DESTROYED" ||
|
|
538
|
+
err.code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
539
|
+
/\bEIO\b/.test(err.message) ||
|
|
540
|
+
/\bEPIPE\b/.test(err.message))
|
|
541
|
+
);
|
|
542
|
+
const markStderrBroken = (err) => {
|
|
543
|
+
if (_stderrBroken) return true;
|
|
544
|
+
if (isBenignStderrError(err)) {
|
|
545
|
+
_stderrBroken = true;
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
return false;
|
|
549
|
+
};
|
|
550
|
+
process.stderr.on("error", (err) => {
|
|
551
|
+
if (markStderrBroken(err)) {
|
|
552
|
+
writeToErrorFile("STDERR", "process", `stderr error: ${err?.message || err}`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// If stderr errors for an unexpected reason, record it but avoid crashing.
|
|
556
|
+
writeToErrorFile("STDERR", "process", `stderr unexpected error: ${err?.message || err}`);
|
|
557
|
+
});
|
|
558
|
+
const safeStderrWrite = (chunk, ...rest) => {
|
|
559
|
+
if (
|
|
560
|
+
_stderrBroken ||
|
|
561
|
+
!process.stderr.writable ||
|
|
562
|
+
process.stderr.destroyed ||
|
|
563
|
+
process.stderr.writableEnded
|
|
564
|
+
) {
|
|
565
|
+
_stderrBroken = true;
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
return _origStderrWrite(chunk, ...rest);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
if (markStderrBroken(err)) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
process.stderr.write = (chunk, ...rest) => {
|
|
578
|
+
if (!_inInterceptor) {
|
|
579
|
+
const text = typeof chunk === "string" ? chunk : chunk?.toString?.("utf8") || "";
|
|
580
|
+
if (text.trim()) {
|
|
581
|
+
writeToErrorFile("STDERR", "process", text.replace(/\n$/, ""));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return safeStderrWrite(chunk, ...rest);
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// ── Harden stdout against broken pipes ────────────────────────────────
|
|
588
|
+
const _origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
589
|
+
let _stdoutBroken = false;
|
|
590
|
+
const isBenignStdoutError = (err) =>
|
|
591
|
+
!!(
|
|
592
|
+
err &&
|
|
593
|
+
(err.code === "EPIPE" ||
|
|
594
|
+
err.code === "EIO" ||
|
|
595
|
+
err.code === "ERR_STREAM_DESTROYED" ||
|
|
596
|
+
err.code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
597
|
+
/\bEIO\b/.test(err.message) ||
|
|
598
|
+
/\bEPIPE\b/.test(err.message))
|
|
599
|
+
);
|
|
600
|
+
const markStdoutBroken = (err) => {
|
|
601
|
+
if (_stdoutBroken) return true;
|
|
602
|
+
if (isBenignStdoutError(err)) {
|
|
603
|
+
_stdoutBroken = true;
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
607
|
+
};
|
|
608
|
+
process.stdout.on("error", (err) => {
|
|
609
|
+
if (markStdoutBroken(err)) {
|
|
610
|
+
writeToErrorFile("STDOUT", "process", `stdout error: ${err?.message || err}`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
writeToErrorFile("STDOUT", "process", `stdout unexpected error: ${err?.message || err}`);
|
|
614
|
+
});
|
|
615
|
+
const safeStdoutWrite = (chunk, ...rest) => {
|
|
616
|
+
if (
|
|
617
|
+
_stdoutBroken ||
|
|
618
|
+
!process.stdout.writable ||
|
|
619
|
+
process.stdout.destroyed ||
|
|
620
|
+
process.stdout.writableEnded
|
|
621
|
+
) {
|
|
622
|
+
_stdoutBroken = true;
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
return _origStdoutWrite(chunk, ...rest);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
if (markStdoutBroken(err)) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
throw err;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
process.stdout.write = (chunk, ...rest) => safeStdoutWrite(chunk, ...rest);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Restore original console methods (for testing).
|
|
639
|
+
*/
|
|
640
|
+
export function uninstallConsoleInterceptor() {
|
|
641
|
+
// Can't easily restore — this is a best-effort for tests
|
|
642
|
+
interceptorInstalled = false;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export default createLogger;
|