sentinelayer-cli 0.30.12 → 0.30.14
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/README.md +5 -0
- package/package.json +1 -1
- package/src/commands/mcp.js +34 -0
- package/src/commands/session.js +89 -19
- package/src/mcp/cli-command-tools.js +506 -0
- package/src/mcp/cli-registry.js +231 -0
- package/src/mcp/registry.js +260 -0
- package/src/mcp/session-stdio-server.js +436 -6
- package/src/review/persona-prompts.js +8 -0
- package/src/session/daemon-spawn.js +35 -15
- package/src/session/listener.js +44 -2
- package/src/session/rotating-log.js +181 -0
- package/src/session/sync.js +7 -1
package/src/session/listener.js
CHANGED
|
@@ -38,6 +38,10 @@ function normalizePositiveInteger(value, fallbackValue) {
|
|
|
38
38
|
return Math.floor(normalized);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function isListenerStopDirective(event = {}) {
|
|
42
|
+
return normalizeString(event?.event) === "listener_stop";
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
function isPlainObject(value) {
|
|
42
46
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
43
47
|
}
|
|
@@ -255,6 +259,8 @@ export async function listenSessionEvents({
|
|
|
255
259
|
_readCursor = readSyncCursor,
|
|
256
260
|
_writeCursor = writeSyncCursor,
|
|
257
261
|
_sleep = defaultSleep,
|
|
262
|
+
_setInterval = setInterval,
|
|
263
|
+
_clearInterval = clearInterval,
|
|
258
264
|
_nowMs = Date.now,
|
|
259
265
|
} = {}) {
|
|
260
266
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -394,6 +400,7 @@ export async function listenSessionEvents({
|
|
|
394
400
|
const visibleEvents = [];
|
|
395
401
|
let preStartEventCount = 0;
|
|
396
402
|
for (const event of events) {
|
|
403
|
+
if (!isPlainObject(event)) continue;
|
|
397
404
|
const timestampMs = eventTimestampMs(event);
|
|
398
405
|
if (!timestampMs || timestampMs < startedAtMs) {
|
|
399
406
|
preStartEventCount += 1;
|
|
@@ -402,6 +409,7 @@ export async function listenSessionEvents({
|
|
|
402
409
|
if (isRecentActivity(activityMs, observedAtMs, activeWindowMs)) {
|
|
403
410
|
lastHumanActivityMs = Math.max(lastHumanActivityMs, activityMs);
|
|
404
411
|
}
|
|
412
|
+
if (timestampMs && timestampMs < startedAtMs && isListenerStopDirective(event)) continue;
|
|
405
413
|
if (isSessionListenerLifecycleEvent(event)) continue;
|
|
406
414
|
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
407
415
|
visibleEvents.push(event);
|
|
@@ -441,9 +449,14 @@ export async function listenSessionEvents({
|
|
|
441
449
|
if (emittedKeys.has(key)) continue;
|
|
442
450
|
matched += 1;
|
|
443
451
|
if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
|
|
444
|
-
await onEvent(event);
|
|
445
452
|
emittedKeys.add(key);
|
|
446
|
-
|
|
453
|
+
try {
|
|
454
|
+
await onEvent(event);
|
|
455
|
+
emitted += 1;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
emittedKeys.delete(key);
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
447
460
|
}
|
|
448
461
|
|
|
449
462
|
if (nextCursor && nextCursor !== cursor) {
|
|
@@ -473,6 +486,32 @@ export async function listenSessionEvents({
|
|
|
473
486
|
return humanActive;
|
|
474
487
|
}
|
|
475
488
|
|
|
489
|
+
let streamHeartbeatTimer = null;
|
|
490
|
+
let streamHeartbeatInFlight = false;
|
|
491
|
+
const streamHeartbeatMs = Math.max(1_000, Math.min(idleSleepMs, 60_000));
|
|
492
|
+
|
|
493
|
+
function stopStreamHeartbeatTimer() {
|
|
494
|
+
if (!streamHeartbeatTimer) return;
|
|
495
|
+
_clearInterval(streamHeartbeatTimer);
|
|
496
|
+
streamHeartbeatTimer = null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function startStreamHeartbeatTimer() {
|
|
500
|
+
stopStreamHeartbeatTimer();
|
|
501
|
+
streamHeartbeatTimer = _setInterval(async () => {
|
|
502
|
+
if (signal?.aborted || streamHeartbeatInFlight) return;
|
|
503
|
+
streamHeartbeatInFlight = true;
|
|
504
|
+
try {
|
|
505
|
+
await notifyHeartbeat({ nextPollMs: null });
|
|
506
|
+
} finally {
|
|
507
|
+
streamHeartbeatInFlight = false;
|
|
508
|
+
}
|
|
509
|
+
}, streamHeartbeatMs);
|
|
510
|
+
if (streamHeartbeatTimer && typeof streamHeartbeatTimer.unref === "function") {
|
|
511
|
+
streamHeartbeatTimer.unref();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
476
515
|
await notifyLifecycle(
|
|
477
516
|
lifecycleSnapshot("started", {
|
|
478
517
|
startedAt: new Date(startedAtMs).toISOString(),
|
|
@@ -484,6 +523,7 @@ export async function listenSessionEvents({
|
|
|
484
523
|
if (normalizedTransport !== "poll") {
|
|
485
524
|
streamAttempted = true;
|
|
486
525
|
activeTransport = "stream";
|
|
526
|
+
startStreamHeartbeatTimer();
|
|
487
527
|
const streamResult = await _stream(normalizedSessionId, {
|
|
488
528
|
targetPath,
|
|
489
529
|
since: cursor,
|
|
@@ -502,6 +542,8 @@ export async function listenSessionEvents({
|
|
|
502
542
|
onHeartbeat: async () => {
|
|
503
543
|
await notifyHeartbeat({ nextPollMs: null });
|
|
504
544
|
},
|
|
545
|
+
}).finally(() => {
|
|
546
|
+
stopStreamHeartbeatTimer();
|
|
505
547
|
});
|
|
506
548
|
if (!streamResult?.ok) {
|
|
507
549
|
streamFallbackReason = normalizeString(streamResult?.reason) || lastReason || "stream_failed";
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import util from "node:util";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_ROTATING_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
7
|
+
export const DEFAULT_ROTATING_LOG_MAX_FILES = 5;
|
|
8
|
+
|
|
9
|
+
function normalizeString(value) {
|
|
10
|
+
return String(value || "").trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
14
|
+
const numeric = Number(value);
|
|
15
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
16
|
+
return fallbackValue;
|
|
17
|
+
}
|
|
18
|
+
return Math.max(1, Math.floor(numeric));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeRotatingLogOptions({
|
|
22
|
+
maxBytes = DEFAULT_ROTATING_LOG_MAX_BYTES,
|
|
23
|
+
maxFiles = DEFAULT_ROTATING_LOG_MAX_FILES,
|
|
24
|
+
} = {}) {
|
|
25
|
+
return {
|
|
26
|
+
maxBytes: normalizePositiveInteger(maxBytes, DEFAULT_ROTATING_LOG_MAX_BYTES),
|
|
27
|
+
maxFiles: normalizePositiveInteger(maxFiles, DEFAULT_ROTATING_LOG_MAX_FILES),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function rotateLogFileIfNeeded(logPath, options = {}) {
|
|
32
|
+
const normalizedLogPath = normalizeString(logPath);
|
|
33
|
+
if (!normalizedLogPath) {
|
|
34
|
+
return { rotated: false, reason: "missing_log_path" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { maxBytes, maxFiles } = normalizeRotatingLogOptions(options);
|
|
38
|
+
let currentSize = 0;
|
|
39
|
+
try {
|
|
40
|
+
currentSize = fs.statSync(normalizedLogPath).size;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error?.code === "ENOENT") {
|
|
43
|
+
return { rotated: false, reason: "missing_file", maxBytes, maxFiles };
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentSize < maxBytes) {
|
|
49
|
+
return { rotated: false, reason: "under_limit", size: currentSize, maxBytes, maxFiles };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(path.dirname(normalizedLogPath), { recursive: true });
|
|
53
|
+
const backupCount = Math.max(0, maxFiles - 1);
|
|
54
|
+
if (backupCount === 0) {
|
|
55
|
+
fs.rmSync(normalizedLogPath, { force: true });
|
|
56
|
+
return { rotated: true, reason: "removed_active", size: currentSize, maxBytes, maxFiles };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fs.rmSync(`${normalizedLogPath}.${backupCount}`, { force: true });
|
|
60
|
+
for (let index = backupCount - 1; index >= 1; index -= 1) {
|
|
61
|
+
const from = `${normalizedLogPath}.${index}`;
|
|
62
|
+
const to = `${normalizedLogPath}.${index + 1}`;
|
|
63
|
+
if (fs.existsSync(from)) {
|
|
64
|
+
fs.renameSync(from, to);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
fs.renameSync(normalizedLogPath, `${normalizedLogPath}.1`);
|
|
68
|
+
return { rotated: true, reason: "rotated", size: currentSize, maxBytes, maxFiles };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function appendRotatingLogLine(logPath, line = "", options = {}) {
|
|
72
|
+
const normalizedLogPath = normalizeString(logPath);
|
|
73
|
+
if (!normalizedLogPath) {
|
|
74
|
+
return { written: false, reason: "missing_log_path" };
|
|
75
|
+
}
|
|
76
|
+
fs.mkdirSync(path.dirname(normalizedLogPath), { recursive: true });
|
|
77
|
+
const rotation = rotateLogFileIfNeeded(normalizedLogPath, options);
|
|
78
|
+
fs.appendFileSync(normalizedLogPath, `${redactLogSecrets(line)}\n`, "utf-8");
|
|
79
|
+
return { written: true, rotation };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeLogChunk(chunk, encoding = "utf8") {
|
|
83
|
+
if (Buffer.isBuffer(chunk)) {
|
|
84
|
+
return chunk.toString(typeof encoding === "string" ? encoding : "utf8");
|
|
85
|
+
}
|
|
86
|
+
return String(chunk ?? "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatConsoleLogLine(parts = []) {
|
|
90
|
+
return parts.map((part) => (typeof part === "string" ? part : util.inspect(part))).join(" ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function redactLogSecrets(value = "") {
|
|
94
|
+
return String(value ?? "")
|
|
95
|
+
.replace(/\bbearer\s+[a-z0-9._~+/=-]+\b/gi, "bearer [REDACTED]")
|
|
96
|
+
.replace(
|
|
97
|
+
/\b([a-z0-9_.-]*(?:token|secret|password|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token)[a-z0-9_.-]*)\b\s*[:=]\s*["']?[^"'\s,;]+["']?/gi,
|
|
98
|
+
"$1=[REDACTED]",
|
|
99
|
+
)
|
|
100
|
+
.replace(/\beyJ[a-z0-9_-]{8,}\.[a-z0-9_-]{10,}\.[a-z0-9_-]{10,}\b/gi, "[REDACTED_JWT]");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function installRotatingConsoleLog({
|
|
104
|
+
logPath,
|
|
105
|
+
maxBytes = DEFAULT_ROTATING_LOG_MAX_BYTES,
|
|
106
|
+
maxFiles = DEFAULT_ROTATING_LOG_MAX_FILES,
|
|
107
|
+
tee = true,
|
|
108
|
+
now = () => new Date(),
|
|
109
|
+
onError = null,
|
|
110
|
+
captureConsoleError = true,
|
|
111
|
+
captureStderr = true,
|
|
112
|
+
} = {}) {
|
|
113
|
+
const normalizedLogPath = normalizeString(logPath);
|
|
114
|
+
if (!normalizedLogPath) {
|
|
115
|
+
return () => {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const options = normalizeRotatingLogOptions({ maxBytes, maxFiles });
|
|
119
|
+
const originalLog = console.log;
|
|
120
|
+
const originalError = console.error;
|
|
121
|
+
const originalStderrWrite = process.stderr.write;
|
|
122
|
+
let warned = false;
|
|
123
|
+
const writeLogLine = (line) => {
|
|
124
|
+
try {
|
|
125
|
+
appendRotatingLogLine(
|
|
126
|
+
normalizedLogPath,
|
|
127
|
+
`${now().toISOString()} ${line}`,
|
|
128
|
+
options,
|
|
129
|
+
);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!warned) {
|
|
132
|
+
warned = true;
|
|
133
|
+
if (typeof onError === "function") {
|
|
134
|
+
onError(error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
console.log = (...parts) => {
|
|
141
|
+
if (tee) {
|
|
142
|
+
originalLog(...parts);
|
|
143
|
+
}
|
|
144
|
+
writeLogLine(formatConsoleLogLine(parts));
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (captureStderr) {
|
|
148
|
+
process.stderr.write = function rotatingStderrWrite(chunk, encoding, callback) {
|
|
149
|
+
const text = normalizeLogChunk(chunk, encoding).replace(/\s+$/u, "");
|
|
150
|
+
if (text) {
|
|
151
|
+
writeLogLine(`[stderr] ${text}`);
|
|
152
|
+
}
|
|
153
|
+
if (!tee) {
|
|
154
|
+
if (typeof encoding === "function") {
|
|
155
|
+
encoding();
|
|
156
|
+
} else if (typeof callback === "function") {
|
|
157
|
+
callback();
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return originalStderrWrite.apply(process.stderr, arguments);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (captureConsoleError) {
|
|
166
|
+
console.error = (...parts) => {
|
|
167
|
+
if (tee) {
|
|
168
|
+
originalError(...parts);
|
|
169
|
+
}
|
|
170
|
+
if (!captureStderr || !tee) {
|
|
171
|
+
writeLogLine(`[stderr] ${formatConsoleLogLine(parts)}`);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
console.log = originalLog;
|
|
178
|
+
console.error = originalError;
|
|
179
|
+
process.stderr.write = originalStderrWrite;
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/session/sync.js
CHANGED
|
@@ -461,7 +461,13 @@ function abortRequestController(controller, reason) {
|
|
|
461
461
|
function cancelResponseBody(response) {
|
|
462
462
|
try {
|
|
463
463
|
if (response?.body && typeof response.body.cancel === "function") {
|
|
464
|
-
|
|
464
|
+
if (response.body.locked) return;
|
|
465
|
+
const cancellation = response.body.cancel();
|
|
466
|
+
if (cancellation && typeof cancellation.catch === "function") {
|
|
467
|
+
void cancellation.catch(() => {
|
|
468
|
+
// Best-effort socket/body cleanup only.
|
|
469
|
+
});
|
|
470
|
+
}
|
|
465
471
|
}
|
|
466
472
|
} catch {
|
|
467
473
|
// Best-effort socket/body cleanup only.
|