sentinelayer-cli 0.36.2 → 0.36.4
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/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -69,6 +69,8 @@ import {
|
|
|
69
69
|
getListenerProcessStatus,
|
|
70
70
|
removeListenerPidRecord,
|
|
71
71
|
requestListenerProcessStop,
|
|
72
|
+
summarizeLocalListenerProcesses,
|
|
73
|
+
stopMatchingListenerProcesses,
|
|
72
74
|
writeListenerPidRecord,
|
|
73
75
|
} from "../session/listener-process.js";
|
|
74
76
|
import {
|
|
@@ -4465,7 +4467,47 @@ export function registerSessionCommand(program) {
|
|
|
4465
4467
|
limit,
|
|
4466
4468
|
maxPages,
|
|
4467
4469
|
});
|
|
4468
|
-
|
|
4470
|
+
let listeners = Array.isArray(result.listeners) ? result.listeners : [];
|
|
4471
|
+
let localProcessScan = {
|
|
4472
|
+
ok: false,
|
|
4473
|
+
reason: listeners.length > 0 ? "not_scanned" : "no_listener_rows",
|
|
4474
|
+
processCount: 0,
|
|
4475
|
+
duplicateProcessCount: 0,
|
|
4476
|
+
agents: [],
|
|
4477
|
+
};
|
|
4478
|
+
if (listeners.length > 0) {
|
|
4479
|
+
try {
|
|
4480
|
+
const summary = await summarizeLocalListenerProcesses(
|
|
4481
|
+
normalizedSessionId,
|
|
4482
|
+
listeners.map((row) => row.agentId),
|
|
4483
|
+
);
|
|
4484
|
+
const byAgent = new Map(summary.agents.map((entry) => [entry.agentId, entry]));
|
|
4485
|
+
listeners = listeners.map((row) => {
|
|
4486
|
+
const local = byAgent.get(row.agentId) || {};
|
|
4487
|
+
const pids = Array.isArray(local.pids) ? local.pids : [];
|
|
4488
|
+
return {
|
|
4489
|
+
...row,
|
|
4490
|
+
localProcessCount: Number(local.processCount || 0),
|
|
4491
|
+
localDuplicateProcessCount: Number(local.duplicateProcessCount || 0),
|
|
4492
|
+
localProcessPids: pids,
|
|
4493
|
+
};
|
|
4494
|
+
});
|
|
4495
|
+
localProcessScan = {
|
|
4496
|
+
ok: true,
|
|
4497
|
+
processCount: Number(summary.processCount || 0),
|
|
4498
|
+
duplicateProcessCount: Number(summary.duplicateProcessCount || 0),
|
|
4499
|
+
agents: summary.agents,
|
|
4500
|
+
};
|
|
4501
|
+
} catch (error) {
|
|
4502
|
+
localProcessScan = {
|
|
4503
|
+
ok: false,
|
|
4504
|
+
reason: normalizeString(error?.message) || "process_scan_failed",
|
|
4505
|
+
processCount: 0,
|
|
4506
|
+
duplicateProcessCount: 0,
|
|
4507
|
+
agents: [],
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4469
4511
|
const live = listeners.filter((row) => row.status === "active" || row.status === "idle").length;
|
|
4470
4512
|
const payload = {
|
|
4471
4513
|
command: "session listeners",
|
|
@@ -4479,6 +4521,7 @@ export function registerSessionCommand(program) {
|
|
|
4479
4521
|
listenerEventCount: Number(result.listenerEventCount || 0),
|
|
4480
4522
|
partial: Boolean(result.partial),
|
|
4481
4523
|
beforeSequence: result.beforeSequence || null,
|
|
4524
|
+
localProcessScan,
|
|
4482
4525
|
listeners,
|
|
4483
4526
|
};
|
|
4484
4527
|
if (shouldEmitJson(options, command)) {
|
|
@@ -4500,6 +4543,16 @@ export function registerSessionCommand(program) {
|
|
|
4500
4543
|
else if (row.status === "idle") console.log(pc.cyan(` ${line}`));
|
|
4501
4544
|
else console.log(pc.gray(` ${line}`));
|
|
4502
4545
|
}
|
|
4546
|
+
const duplicateLocalRows = listeners.filter((row) => Number(row.localDuplicateProcessCount || 0) > 0);
|
|
4547
|
+
if (duplicateLocalRows.length > 0) {
|
|
4548
|
+
const summary = duplicateLocalRows
|
|
4549
|
+
.map((row) => `${row.agentId}:${row.localProcessCount}`)
|
|
4550
|
+
.join(", ");
|
|
4551
|
+
console.log(pc.yellow(`Local process warning: multiple matching listener processes detected (${summary}).`));
|
|
4552
|
+
console.log(pc.gray(
|
|
4553
|
+
`Use "sl session listen --session ${normalizedSessionId} --agent <your-agent> --force --from-now" to replace your own duplicate listener; coordinate before touching another agent.`,
|
|
4554
|
+
));
|
|
4555
|
+
}
|
|
4503
4556
|
return payload;
|
|
4504
4557
|
});
|
|
4505
4558
|
|
|
@@ -4752,10 +4805,26 @@ export function registerSessionCommand(program) {
|
|
|
4752
4805
|
);
|
|
4753
4806
|
}
|
|
4754
4807
|
if (existingListener.running && options.force) {
|
|
4755
|
-
const
|
|
4756
|
-
|
|
4808
|
+
const stopMatchesResult = await stopMatchingListenerProcesses(normalizedSessionId, agentId);
|
|
4809
|
+
const stoppedPids = new Set(
|
|
4810
|
+
stopMatchesResult.results
|
|
4811
|
+
.filter((result) => result.stopped)
|
|
4812
|
+
.map((result) => Number(result.pid)),
|
|
4813
|
+
);
|
|
4814
|
+
let explicitStopResult = null;
|
|
4815
|
+
if (existingListener.pid && !stoppedPids.has(Number(existingListener.pid))) {
|
|
4816
|
+
explicitStopResult = await requestListenerProcessStop(existingListener.pid);
|
|
4817
|
+
}
|
|
4818
|
+
const failedStops = [
|
|
4819
|
+
...stopMatchesResult.failed,
|
|
4820
|
+
...(explicitStopResult && !explicitStopResult.stopped
|
|
4821
|
+
? [{ pid: existingListener.pid, ...explicitStopResult }]
|
|
4822
|
+
: []),
|
|
4823
|
+
];
|
|
4824
|
+
if (failedStops.length > 0) {
|
|
4825
|
+
const failed = failedStops[0];
|
|
4757
4826
|
throw new Error(
|
|
4758
|
-
`Unable to stop existing listener pid ${existingListener.pid} for session ${normalizedSessionId} as ${agentId} (${
|
|
4827
|
+
`Unable to stop existing listener pid ${failed.pid || existingListener.pid} for session ${normalizedSessionId} as ${agentId} (${failed.reason}). Stop it manually or use --allow-duplicate if parallel listeners are intentional.`,
|
|
4759
4828
|
);
|
|
4760
4829
|
}
|
|
4761
4830
|
}
|
|
@@ -30,21 +30,159 @@ function normalizeCommandLine(value) {
|
|
|
30
30
|
return normalizeString(value).toLowerCase();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function escapeRegExp(value) {
|
|
34
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function commandLineFlagValueMatches(commandLine, flagName, expectedValue) {
|
|
38
|
+
const expected = normalizeString(expectedValue).toLowerCase();
|
|
39
|
+
if (!expected) return { found: false, matches: true };
|
|
40
|
+
const pattern = new RegExp(
|
|
41
|
+
`(?:^|\\s)--${escapeRegExp(flagName)}(?:=|\\s+)(?:"([^"]+)"|'([^']+)'|([^\\s]+))`,
|
|
42
|
+
"gi",
|
|
43
|
+
);
|
|
44
|
+
let found = false;
|
|
45
|
+
for (const match of commandLine.matchAll(pattern)) {
|
|
46
|
+
found = true;
|
|
47
|
+
const value = normalizeString(match[1] ?? match[2] ?? match[3]).toLowerCase();
|
|
48
|
+
if (value === expected) {
|
|
49
|
+
return { found: true, matches: true };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { found, matches: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
function listenerCommandLineMatches(commandLine, { sessionId = "", agentId = "" } = {}) {
|
|
34
|
-
const
|
|
56
|
+
const rawCommandLine = normalizeString(commandLine);
|
|
57
|
+
const normalized = normalizeCommandLine(rawCommandLine);
|
|
35
58
|
if (!normalized) return false;
|
|
36
59
|
const normalizedSessionId = normalizeString(sessionId).toLowerCase();
|
|
37
60
|
const normalizedAgentId = normalizeString(agentId).toLowerCase();
|
|
38
61
|
const hasCliEntry =
|
|
39
62
|
normalized.includes("sentinelayer-cli") ||
|
|
40
63
|
/[\\/](?:sl|sentinelayer-cli)\.js\b/.test(normalized);
|
|
64
|
+
const sessionFlagMatch = commandLineFlagValueMatches(rawCommandLine, "session", normalizedSessionId);
|
|
65
|
+
const agentFlagMatch = commandLineFlagValueMatches(rawCommandLine, "agent", normalizedAgentId);
|
|
41
66
|
return (
|
|
42
67
|
hasCliEntry &&
|
|
43
68
|
normalized.includes("session") &&
|
|
44
69
|
normalized.includes("listen") &&
|
|
45
|
-
(!normalizedSessionId ||
|
|
46
|
-
|
|
70
|
+
(!normalizedSessionId ||
|
|
71
|
+
(sessionFlagMatch.found
|
|
72
|
+
? sessionFlagMatch.matches
|
|
73
|
+
: normalized.includes(normalizedSessionId))) &&
|
|
74
|
+
(!normalizedAgentId ||
|
|
75
|
+
(agentFlagMatch.found
|
|
76
|
+
? agentFlagMatch.matches
|
|
77
|
+
: normalized.includes(normalizedAgentId)))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeProcessRows(rows = []) {
|
|
82
|
+
if (!Array.isArray(rows)) return [];
|
|
83
|
+
return rows
|
|
84
|
+
.map((row) => ({
|
|
85
|
+
pid: Number(row?.pid ?? row?.ProcessId ?? row?.processId),
|
|
86
|
+
commandLine: normalizeString(row?.commandLine ?? row?.CommandLine ?? row?.command ?? row?.args),
|
|
87
|
+
}))
|
|
88
|
+
.filter((row) => Number.isInteger(row.pid) && row.pid > 0 && row.commandLine);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function listWindowsProcesses() {
|
|
92
|
+
const command = [
|
|
93
|
+
"$ErrorActionPreference = 'Stop';",
|
|
94
|
+
"$rows = Get-CimInstance Win32_Process |",
|
|
95
|
+
"Where-Object { $_.CommandLine -and ($_.CommandLine -match 'sentinelayer-cli|[\\\\/](sl|sentinelayer-cli)\\.js') } |",
|
|
96
|
+
"Select-Object ProcessId,CommandLine;",
|
|
97
|
+
"if ($rows) { $rows | ConvertTo-Json -Compress }",
|
|
98
|
+
].join(" ");
|
|
99
|
+
const { stdout } = await execFileAsync(
|
|
100
|
+
"powershell.exe",
|
|
101
|
+
["-NoProfile", "-NonInteractive", "-Command", command],
|
|
102
|
+
{ windowsHide: true, timeout: 3000, maxBuffer: 10 * 1024 * 1024 },
|
|
47
103
|
);
|
|
104
|
+
const text = normalizeString(stdout);
|
|
105
|
+
if (!text) return [];
|
|
106
|
+
const parsed = JSON.parse(text);
|
|
107
|
+
return normalizeProcessRows(Array.isArray(parsed) ? parsed : [parsed]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function listPosixProcesses() {
|
|
111
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,command="], {
|
|
112
|
+
timeout: 3000,
|
|
113
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
114
|
+
});
|
|
115
|
+
return String(stdout || "")
|
|
116
|
+
.split(/\r?\n/)
|
|
117
|
+
.map((line) => {
|
|
118
|
+
const match = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
119
|
+
return match ? { pid: Number(match[1]), commandLine: normalizeString(match[2]) } : null;
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function listProcessTable() {
|
|
125
|
+
try {
|
|
126
|
+
return process.platform === "win32"
|
|
127
|
+
? await listWindowsProcesses()
|
|
128
|
+
: await listPosixProcesses();
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function listMatchingListenerProcesses(
|
|
135
|
+
sessionId,
|
|
136
|
+
agentId,
|
|
137
|
+
{
|
|
138
|
+
excludePid = process.pid,
|
|
139
|
+
_listProcesses = listProcessTable,
|
|
140
|
+
} = {},
|
|
141
|
+
) {
|
|
142
|
+
const rows = normalizeProcessRows(await _listProcesses());
|
|
143
|
+
const excluded = Number(excludePid);
|
|
144
|
+
return rows.filter((row) =>
|
|
145
|
+
row.pid !== excluded &&
|
|
146
|
+
listenerCommandLineMatches(row.commandLine, { sessionId, agentId })
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function summarizeLocalListenerProcesses(
|
|
151
|
+
sessionId,
|
|
152
|
+
agentIds = [],
|
|
153
|
+
{
|
|
154
|
+
excludePid = process.pid,
|
|
155
|
+
_listProcesses = listProcessTable,
|
|
156
|
+
} = {},
|
|
157
|
+
) {
|
|
158
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
159
|
+
const uniqueAgentIds = [...new Set((Array.isArray(agentIds) ? agentIds : [])
|
|
160
|
+
.map((agentId) => normalizeString(agentId))
|
|
161
|
+
.filter(Boolean))];
|
|
162
|
+
const rows = normalizeProcessRows(await _listProcesses());
|
|
163
|
+
const excluded = Number(excludePid);
|
|
164
|
+
const candidates = rows.filter((row) => row.pid !== excluded);
|
|
165
|
+
const agents = uniqueAgentIds.map((agentId) => {
|
|
166
|
+
const matches = candidates
|
|
167
|
+
.filter((row) => listenerCommandLineMatches(row.commandLine, {
|
|
168
|
+
sessionId: normalizedSessionId,
|
|
169
|
+
agentId,
|
|
170
|
+
}))
|
|
171
|
+
.sort((a, b) => a.pid - b.pid);
|
|
172
|
+
const pids = matches.map((match) => match.pid);
|
|
173
|
+
return {
|
|
174
|
+
agentId,
|
|
175
|
+
processCount: pids.length,
|
|
176
|
+
duplicateProcessCount: Math.max(0, pids.length - 1),
|
|
177
|
+
pids,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
sessionId: normalizedSessionId,
|
|
182
|
+
processCount: agents.reduce((sum, agent) => sum + agent.processCount, 0),
|
|
183
|
+
duplicateProcessCount: agents.reduce((sum, agent) => sum + agent.duplicateProcessCount, 0),
|
|
184
|
+
agents,
|
|
185
|
+
};
|
|
48
186
|
}
|
|
49
187
|
|
|
50
188
|
async function readWindowsCommandLine(pid) {
|
|
@@ -276,6 +414,7 @@ export async function getListenerProcessStatus(
|
|
|
276
414
|
targetPath = process.cwd(),
|
|
277
415
|
homeDir = os.homedir(),
|
|
278
416
|
_readProcessCommandLine = readProcessCommandLine,
|
|
417
|
+
_listProcesses = listProcessTable,
|
|
279
418
|
} = {},
|
|
280
419
|
) {
|
|
281
420
|
const records = [
|
|
@@ -302,6 +441,22 @@ export async function getListenerProcessStatus(
|
|
|
302
441
|
}
|
|
303
442
|
staleStatuses.push(status);
|
|
304
443
|
}
|
|
444
|
+
const matchingProcesses = await listMatchingListenerProcesses(sessionId, agentId, {
|
|
445
|
+
_listProcesses,
|
|
446
|
+
});
|
|
447
|
+
if (matchingProcesses.length > 0) {
|
|
448
|
+
return {
|
|
449
|
+
running: true,
|
|
450
|
+
pid: matchingProcesses[0].pid,
|
|
451
|
+
stale: false,
|
|
452
|
+
record: null,
|
|
453
|
+
recordScope: "process_scan",
|
|
454
|
+
untracked: true,
|
|
455
|
+
listenerKey: normalizeListenerProcessKey(agentId),
|
|
456
|
+
commandLine: matchingProcesses[0].commandLine,
|
|
457
|
+
matchingProcesses,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
305
460
|
if (staleStatuses.length === 0) {
|
|
306
461
|
return {
|
|
307
462
|
running: false,
|
|
@@ -314,6 +469,42 @@ export async function getListenerProcessStatus(
|
|
|
314
469
|
return staleStatuses[0];
|
|
315
470
|
}
|
|
316
471
|
|
|
472
|
+
export async function stopMatchingListenerProcesses(
|
|
473
|
+
sessionId,
|
|
474
|
+
agentId,
|
|
475
|
+
{
|
|
476
|
+
excludePid = process.pid,
|
|
477
|
+
timeoutMs = 3000,
|
|
478
|
+
pollIntervalMs = 100,
|
|
479
|
+
_listProcesses = listProcessTable,
|
|
480
|
+
} = {},
|
|
481
|
+
) {
|
|
482
|
+
const matches = await listMatchingListenerProcesses(sessionId, agentId, {
|
|
483
|
+
excludePid,
|
|
484
|
+
_listProcesses,
|
|
485
|
+
});
|
|
486
|
+
const results = [];
|
|
487
|
+
for (const match of matches) {
|
|
488
|
+
const result = await requestListenerProcessStop(match.pid, {
|
|
489
|
+
timeoutMs,
|
|
490
|
+
pollIntervalMs,
|
|
491
|
+
});
|
|
492
|
+
results.push({
|
|
493
|
+
pid: match.pid,
|
|
494
|
+
commandLine: match.commandLine,
|
|
495
|
+
...result,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const failed = results.filter((result) => !result.stopped);
|
|
499
|
+
return {
|
|
500
|
+
matches,
|
|
501
|
+
results,
|
|
502
|
+
stoppedCount: results.filter((result) => result.stopped).length,
|
|
503
|
+
failedCount: failed.length,
|
|
504
|
+
failed,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
317
508
|
export async function requestListenerProcessStop(
|
|
318
509
|
pid,
|
|
319
510
|
{ signal = "SIGTERM", timeoutMs = 3000, pollIntervalMs = 100 } = {},
|
package/src/session/listeners.js
CHANGED
|
@@ -246,5 +246,13 @@ export function formatListenerLine(row) {
|
|
|
246
246
|
: row.status === "stale"
|
|
247
247
|
? "◌ stale"
|
|
248
248
|
: "× stopped";
|
|
249
|
-
|
|
249
|
+
const localProcessCount = Number(row.localProcessCount || 0);
|
|
250
|
+
const localPids = Array.isArray(row.localProcessPids)
|
|
251
|
+
? row.localProcessPids.filter((pid) => Number.isInteger(Number(pid)) && Number(pid) > 0)
|
|
252
|
+
: [];
|
|
253
|
+
const local =
|
|
254
|
+
localProcessCount > 1 && localPids.length > 0
|
|
255
|
+
? ` local_pids=${localPids.join(",")}`
|
|
256
|
+
: "";
|
|
257
|
+
return `${statusLabel.padEnd(10)} ${row.agentId.padEnd(24)} cadence=${cadence.padEnd(6)} last_seen=${seen}${local}`;
|
|
250
258
|
}
|