sentinelayer-cli 0.36.2 → 0.36.3

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.36.2",
3
+ "version": "0.36.3",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -69,6 +69,7 @@ import {
69
69
  getListenerProcessStatus,
70
70
  removeListenerPidRecord,
71
71
  requestListenerProcessStop,
72
+ stopMatchingListenerProcesses,
72
73
  writeListenerPidRecord,
73
74
  } from "../session/listener-process.js";
74
75
  import {
@@ -4752,10 +4753,26 @@ export function registerSessionCommand(program) {
4752
4753
  );
4753
4754
  }
4754
4755
  if (existingListener.running && options.force) {
4755
- const stopResult = await requestListenerProcessStop(existingListener.pid);
4756
- if (!stopResult.stopped) {
4756
+ const stopMatchesResult = await stopMatchingListenerProcesses(normalizedSessionId, agentId);
4757
+ const stoppedPids = new Set(
4758
+ stopMatchesResult.results
4759
+ .filter((result) => result.stopped)
4760
+ .map((result) => Number(result.pid)),
4761
+ );
4762
+ let explicitStopResult = null;
4763
+ if (existingListener.pid && !stoppedPids.has(Number(existingListener.pid))) {
4764
+ explicitStopResult = await requestListenerProcessStop(existingListener.pid);
4765
+ }
4766
+ const failedStops = [
4767
+ ...stopMatchesResult.failed,
4768
+ ...(explicitStopResult && !explicitStopResult.stopped
4769
+ ? [{ pid: existingListener.pid, ...explicitStopResult }]
4770
+ : []),
4771
+ ];
4772
+ if (failedStops.length > 0) {
4773
+ const failed = failedStops[0];
4757
4774
  throw new Error(
4758
- `Unable to stop existing listener pid ${existingListener.pid} for session ${normalizedSessionId} as ${agentId} (${stopResult.reason}). Stop it manually or use --allow-duplicate if parallel listeners are intentional.`,
4775
+ `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
4776
  );
4760
4777
  }
4761
4778
  }
@@ -30,20 +30,120 @@ 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 normalized = normalizeCommandLine(commandLine);
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 || normalized.includes(normalizedSessionId)) &&
46
- (!normalizedAgentId || normalized.includes(normalizedAgentId))
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 },
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 })
47
147
  );
48
148
  }
49
149
 
@@ -276,6 +376,7 @@ export async function getListenerProcessStatus(
276
376
  targetPath = process.cwd(),
277
377
  homeDir = os.homedir(),
278
378
  _readProcessCommandLine = readProcessCommandLine,
379
+ _listProcesses = listProcessTable,
279
380
  } = {},
280
381
  ) {
281
382
  const records = [
@@ -302,6 +403,22 @@ export async function getListenerProcessStatus(
302
403
  }
303
404
  staleStatuses.push(status);
304
405
  }
406
+ const matchingProcesses = await listMatchingListenerProcesses(sessionId, agentId, {
407
+ _listProcesses,
408
+ });
409
+ if (matchingProcesses.length > 0) {
410
+ return {
411
+ running: true,
412
+ pid: matchingProcesses[0].pid,
413
+ stale: false,
414
+ record: null,
415
+ recordScope: "process_scan",
416
+ untracked: true,
417
+ listenerKey: normalizeListenerProcessKey(agentId),
418
+ commandLine: matchingProcesses[0].commandLine,
419
+ matchingProcesses,
420
+ };
421
+ }
305
422
  if (staleStatuses.length === 0) {
306
423
  return {
307
424
  running: false,
@@ -314,6 +431,42 @@ export async function getListenerProcessStatus(
314
431
  return staleStatuses[0];
315
432
  }
316
433
 
434
+ export async function stopMatchingListenerProcesses(
435
+ sessionId,
436
+ agentId,
437
+ {
438
+ excludePid = process.pid,
439
+ timeoutMs = 3000,
440
+ pollIntervalMs = 100,
441
+ _listProcesses = listProcessTable,
442
+ } = {},
443
+ ) {
444
+ const matches = await listMatchingListenerProcesses(sessionId, agentId, {
445
+ excludePid,
446
+ _listProcesses,
447
+ });
448
+ const results = [];
449
+ for (const match of matches) {
450
+ const result = await requestListenerProcessStop(match.pid, {
451
+ timeoutMs,
452
+ pollIntervalMs,
453
+ });
454
+ results.push({
455
+ pid: match.pid,
456
+ commandLine: match.commandLine,
457
+ ...result,
458
+ });
459
+ }
460
+ const failed = results.filter((result) => !result.stopped);
461
+ return {
462
+ matches,
463
+ results,
464
+ stoppedCount: results.filter((result) => result.stopped).length,
465
+ failedCount: failed.length,
466
+ failed,
467
+ };
468
+ }
469
+
317
470
  export async function requestListenerProcessStop(
318
471
  pid,
319
472
  { signal = "SIGTERM", timeoutMs = 3000, pollIntervalMs = 100 } = {},