@crewhaus/tool-desktop 0.7.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.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `PowerAssertion` — keeping a machine awake for a long step, and giving it
3
+ * back afterwards.
4
+ *
5
+ * THE ASSERTION IS THE LIFETIME OF A CHILD PROCESS, NOT A CALL. The tool has
6
+ * to return while the holder keeps running, which makes three things true
7
+ * that an ordinary tool never has to think about:
8
+ *
9
+ * 1. **A DEADLINE IS MANDATORY, AND THE KERNEL ENFORCES IT.** An unbounded
10
+ * inhibitor is a laptop that never sleeps again because a harness crashed
11
+ * three days ago. So the bound is not a reaper in this process — a reaper
12
+ * dies with the harness that crashed — it is an argument to the holder
13
+ * itself. `caffeinate -t <seconds>` exits on its own; `systemd-inhibit …
14
+ * sleep <seconds>` releases when `sleep` returns; the PowerShell holder
15
+ * parks for a fixed `Start-Sleep`. Kill the whole harness and every one of
16
+ * them still ends on time. The result reports `expiresAt` because a
17
+ * caller that cannot see when its own inhibitor lapses will hold another.
18
+ * 2. **A PID IS NOT AN IDENTITY.** Pids are reused, and `release` reading a
19
+ * stale state file would otherwise send SIGTERM to whatever now owns that
20
+ * number. So the state file records the backend program, and `release` and
21
+ * `status` VERIFY the live process's command line before signalling
22
+ * anything. A mismatch is reported as `stale` and the file is cleared —
23
+ * nothing is killed.
24
+ * 3. **WINDOWS CANNOT DO THIS IN A ONE-LINER.** `SetThreadExecutionState` is
25
+ * THREAD-scoped and its effect dies with the thread, so a PowerShell
26
+ * one-liner that sets the flag and exits does precisely nothing — the
27
+ * naive port's failure is silent and total. The holder must stay alive
28
+ * with the flag set, which is what `Start-Sleep` inside the script is for.
29
+ *
30
+ * Nothing here escalates privilege: `caffeinate`, `systemd-inhibit` and
31
+ * `SetThreadExecutionState` are all available to an ordinary user, and
32
+ * `run.ts`'s `assertArgv` refuses an escalation program structurally.
33
+ */
34
+ import type { HostPlatform } from "../host";
35
+ import type { RunRequest } from "../run";
36
+ import { type Unavailable } from "./outcome";
37
+ /** What the assertion covers. `display` also implies `system`. */
38
+ export type AssertionScope = "system" | "display";
39
+ /** The ceiling, and the reason there is one. Eight hours is a working day. */
40
+ export declare const MAX_HOLD_MINUTES = 480;
41
+ export declare const DEFAULT_HOLD_MINUTES = 30;
42
+ /** Where the held assertion is remembered, relative to the workspace root. */
43
+ export declare const STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
44
+ export declare const WINDOWS_INHIBIT: import("./escape").PowerShellScript;
45
+ export declare const WINDOWS_PROCESS_PROBE: import("./escape").PowerShellScript;
46
+ /**
47
+ * The reason string a caller may attach to a Linux inhibitor.
48
+ *
49
+ * Printable ASCII only, and no newline. It becomes one argv ELEMENT of the
50
+ * form `--why=<text>`, so getopt reads the whole tail as the option's value
51
+ * whatever it contains — but `systemd-inhibit --list` prints it to an
52
+ * operator later, and a control character there corrupts a terminal rather
53
+ * than the command.
54
+ */
55
+ export declare const SAFE_REASON: RegExp;
56
+ /**
57
+ * Is this parsed JSON a state record every field of which can be used?
58
+ *
59
+ * ADVERSARIAL REVIEW FOUND A CRASH HERE. The first version checked `pid` and
60
+ * `marker` and nothing else, so a record carrying those two and a missing or
61
+ * non-numeric `startedAt`/`expiresAt` — an interrupted write, a file from an
62
+ * older layout, an operator who edited it — was accepted, and then
63
+ * `new Date(undefined).toISOString()` threw `RangeError: Invalid time value`
64
+ * straight out of `execute`. Both `status` and `release` did it, which is to
65
+ * say the tool crashed on exactly the path a caller reaches when the recorded
66
+ * assertion is the thing that has gone wrong. `hold` had the quieter half of
67
+ * the same bug: `now() < state.expiresAt` against a non-number is `false`, so
68
+ * it silently overwrote a record it should have refused to replace.
69
+ *
70
+ * Every field is checked, because every field is used: the dates are
71
+ * formatted, the pid is signalled, the marker is matched and the scope and
72
+ * backend are echoed.
73
+ */
74
+ export declare function isAssertionState(value: unknown): value is AssertionState;
75
+ /** What is remembered between calls. Written atomically; see ./fsseam.ts. */
76
+ export type AssertionState = {
77
+ readonly pid: number;
78
+ readonly platform: HostPlatform;
79
+ readonly backend: string;
80
+ /** The program the live process must still be running to be OURS. */
81
+ readonly marker: string;
82
+ readonly scope: AssertionScope;
83
+ readonly startedAt: number;
84
+ readonly expiresAt: number;
85
+ readonly reason: string | null;
86
+ };
87
+ export type HoldPlan = {
88
+ readonly ok: true;
89
+ readonly backend: string;
90
+ /** The program name `release` matches against a live command line. */
91
+ readonly marker: string;
92
+ readonly request: Omit<RunRequest, "timeoutMs">;
93
+ } | {
94
+ readonly ok: false;
95
+ readonly unavailable: Unavailable;
96
+ };
97
+ /** Build the holder's argv for `seconds` of inhibition. */
98
+ export declare function planHold(platform: HostPlatform, scope: AssertionScope, seconds: number, reason: string | null): HoldPlan;
99
+ /**
100
+ * Ask the host what `pid` is actually running.
101
+ *
102
+ * Not `process.kill(pid, 0)`: that answers "some process has this number",
103
+ * which after a pid rollover is exactly the wrong question. The command line
104
+ * is what distinguishes our holder from whatever inherited its number.
105
+ */
106
+ export declare function planProcessProbe(platform: HostPlatform, pid: number): Omit<RunRequest, "timeoutMs"> | undefined;
107
+ /** The signal that ends a verified holder. */
108
+ export declare function planRelease(platform: HostPlatform, pid: number): Omit<RunRequest, "timeoutMs"> | undefined;
109
+ /**
110
+ * Is the recorded assertion still ours, and still running?
111
+ *
112
+ * Four answers, and the two failure cases are deliberately different: a probe
113
+ * that could not run at all is NOT the same as a probe that ran and found
114
+ * somebody else's process. The first must not cause a kill, and must not
115
+ * cause the state file to be discarded either — the holder may be alive and
116
+ * unreachable only to `ps`.
117
+ */
118
+ export type Liveness = "alive" | "gone" | "reused" | "unknown";
119
+ /**
120
+ * Is this command line OUR holder, rather than something that merely mentions
121
+ * it?
122
+ *
123
+ * ADVERSARIAL REVIEW: the first version was `line.includes(marker)`, which
124
+ * answers "yes" for `vim /home/max/notes/caffeinate.md` and for
125
+ * `node ./caffeinate-runner.js`. After a pid rollover that is `release`
126
+ * sending SIGTERM to the operator's editor — precisely the failure the
127
+ * module header promises this check prevents ("A PID IS NOT AN IDENTITY"). A
128
+ * substring test over a whole command line is not an identity test.
129
+ *
130
+ * So the PROGRAM is compared: the first token, unquoted, basename, `.exe`
131
+ * stripped. Windows is the one case where that is not enough and cannot be —
132
+ * the holder there genuinely IS `powershell.exe`, and what makes it ours is
133
+ * the generated type name inside the script it was handed — so the substring
134
+ * test is kept for that case alone, narrowed to a powershell host.
135
+ */
136
+ export declare function commandMatchesMarker(commandLine: string, marker: string): boolean;
137
+ export declare function classifyLiveness(marker: string, result: {
138
+ readonly code: number;
139
+ readonly stdout: string;
140
+ readonly stderr: string;
141
+ readonly timedOut: boolean;
142
+ readonly missing: boolean;
143
+ readonly refused?: boolean;
144
+ }): Liveness;
145
+ /** Minutes to a bounded second count, or a refusal naming the ceiling. */
146
+ export declare function boundedSeconds(minutes: number): {
147
+ seconds: number;
148
+ } | {
149
+ refusal: string;
150
+ };
@@ -0,0 +1,224 @@
1
+ import { powershellArgv, registerPowerShellScript } from "./escape";
2
+ import { unsupportedPlatform } from "./outcome";
3
+ /** The ceiling, and the reason there is one. Eight hours is a working day. */
4
+ export const MAX_HOLD_MINUTES = 480;
5
+ export const DEFAULT_HOLD_MINUTES = 30;
6
+ /** Where the held assertion is remembered, relative to the workspace root. */
7
+ export const STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
8
+ export const WINDOWS_INHIBIT = registerPowerShellScript({
9
+ name: "power-inhibit",
10
+ // DOCUMENTED, NOT RECORDED. The flags are ES_CONTINUOUS (0x80000000) |
11
+ // ES_SYSTEM_REQUIRED (0x00000001), plus ES_DISPLAY_REQUIRED (0x00000002)
12
+ // when the caller asked for the display — passed as a NUMBER in the
13
+ // environment, never interpolated into this source. `Start-Sleep` is what
14
+ // keeps the thread alive; without it the state is discarded the instant the
15
+ // process exits and the whole call is a no-op.
16
+ script: [
17
+ "$src = 'using System;using System.Runtime.InteropServices;public class CHPwr{[DllImport(\"kernel32.dll\")]public static extern uint SetThreadExecutionState(uint f);}'",
18
+ "Add-Type -TypeDefinition $src -ErrorAction Stop",
19
+ "[void][CHPwr]::SetThreadExecutionState([uint32]$env:CREWHAUS_INHIBIT_FLAGS)",
20
+ "Start-Sleep -Seconds ([int]$env:CREWHAUS_INHIBIT_SECONDS)",
21
+ ].join("; "),
22
+ reads: ["CREWHAUS_INHIBIT_FLAGS", "CREWHAUS_INHIBIT_SECONDS"],
23
+ });
24
+ export const WINDOWS_PROCESS_PROBE = registerPowerShellScript({
25
+ name: "process-probe",
26
+ script: '$p = Get-CimInstance Win32_Process -Filter "ProcessId=$env:CREWHAUS_PID" -ErrorAction SilentlyContinue; if ($p) { [Console]::Out.Write($p.CommandLine) }',
27
+ reads: ["CREWHAUS_PID"],
28
+ });
29
+ /**
30
+ * The reason string a caller may attach to a Linux inhibitor.
31
+ *
32
+ * Printable ASCII only, and no newline. It becomes one argv ELEMENT of the
33
+ * form `--why=<text>`, so getopt reads the whole tail as the option's value
34
+ * whatever it contains — but `systemd-inhibit --list` prints it to an
35
+ * operator later, and a control character there corrupts a terminal rather
36
+ * than the command.
37
+ */
38
+ export const SAFE_REASON = /^[ -~]{1,120}$/;
39
+ /**
40
+ * Is this parsed JSON a state record every field of which can be used?
41
+ *
42
+ * ADVERSARIAL REVIEW FOUND A CRASH HERE. The first version checked `pid` and
43
+ * `marker` and nothing else, so a record carrying those two and a missing or
44
+ * non-numeric `startedAt`/`expiresAt` — an interrupted write, a file from an
45
+ * older layout, an operator who edited it — was accepted, and then
46
+ * `new Date(undefined).toISOString()` threw `RangeError: Invalid time value`
47
+ * straight out of `execute`. Both `status` and `release` did it, which is to
48
+ * say the tool crashed on exactly the path a caller reaches when the recorded
49
+ * assertion is the thing that has gone wrong. `hold` had the quieter half of
50
+ * the same bug: `now() < state.expiresAt` against a non-number is `false`, so
51
+ * it silently overwrote a record it should have refused to replace.
52
+ *
53
+ * Every field is checked, because every field is used: the dates are
54
+ * formatted, the pid is signalled, the marker is matched and the scope and
55
+ * backend are echoed.
56
+ */
57
+ export function isAssertionState(value) {
58
+ if (typeof value !== "object" || value === null)
59
+ return false;
60
+ const v = value;
61
+ const finite = (x) => typeof x === "number" && Number.isFinite(x);
62
+ return (finite(v["pid"]) &&
63
+ Number.isInteger(v["pid"]) &&
64
+ v["pid"] > 0 &&
65
+ typeof v["marker"] === "string" &&
66
+ v["marker"] !== "" &&
67
+ typeof v["backend"] === "string" &&
68
+ typeof v["platform"] === "string" &&
69
+ (v["scope"] === "system" || v["scope"] === "display") &&
70
+ finite(v["startedAt"]) &&
71
+ finite(v["expiresAt"]) &&
72
+ (v["reason"] === null || typeof v["reason"] === "string"));
73
+ }
74
+ /** Build the holder's argv for `seconds` of inhibition. */
75
+ export function planHold(platform, scope, seconds, reason) {
76
+ switch (platform) {
77
+ case "darwin": {
78
+ // `-i` idle sleep, `-m` disk idle, `-s` only while on AC — omitted on
79
+ // purpose, a laptop on battery still needs the step to finish. `-d`
80
+ // additionally keeps the display awake. `-t` is the deadline, enforced
81
+ // by caffeinate rather than by this process.
82
+ const argv = ["caffeinate", "-i", "-m"];
83
+ if (scope === "display")
84
+ argv.push("-d");
85
+ argv.push("-t", String(seconds));
86
+ return { ok: true, backend: "caffeinate", marker: "caffeinate", request: { argv } };
87
+ }
88
+ case "linux": {
89
+ const what = scope === "display" ? "idle:sleep:handle-lid-switch" : "idle:sleep";
90
+ const argv = [
91
+ "systemd-inhibit",
92
+ `--what=${what}`,
93
+ "--who=crewhaus",
94
+ `--why=${reason ?? "crewhaus PowerAssertion"}`,
95
+ "--mode=block",
96
+ // The inhibitor lives exactly as long as this command. `sleep` is a
97
+ // program, not a shell — `assertArgv` would refuse an interpreter.
98
+ "sleep",
99
+ String(seconds),
100
+ ];
101
+ return {
102
+ ok: true,
103
+ backend: "systemd-inhibit",
104
+ marker: "systemd-inhibit",
105
+ request: { argv },
106
+ };
107
+ }
108
+ case "win32": {
109
+ const flags = scope === "display" ? 0x8000_0003 : 0x8000_0001;
110
+ const built = powershellArgv(WINDOWS_INHIBIT, {
111
+ CREWHAUS_INHIBIT_FLAGS: String(flags),
112
+ CREWHAUS_INHIBIT_SECONDS: String(seconds),
113
+ });
114
+ return {
115
+ ok: true,
116
+ backend: "powershell SetThreadExecutionState",
117
+ marker: "CHPwr",
118
+ request: { argv: built.argv, env: built.env },
119
+ };
120
+ }
121
+ default:
122
+ return { ok: false, unavailable: unsupportedPlatform(platform, "power-assertion") };
123
+ }
124
+ }
125
+ /**
126
+ * Ask the host what `pid` is actually running.
127
+ *
128
+ * Not `process.kill(pid, 0)`: that answers "some process has this number",
129
+ * which after a pid rollover is exactly the wrong question. The command line
130
+ * is what distinguishes our holder from whatever inherited its number.
131
+ */
132
+ export function planProcessProbe(platform, pid) {
133
+ switch (platform) {
134
+ case "darwin":
135
+ case "linux":
136
+ // `-o command=` prints the command line with no header, and nothing
137
+ // else. The pid is a number this package produced, not caller input.
138
+ return { argv: ["ps", "-o", "command=", "-p", String(pid)] };
139
+ case "win32":
140
+ return { argv: powershellArgv(WINDOWS_PROCESS_PROBE, { CREWHAUS_PID: String(pid) }).argv };
141
+ default:
142
+ return undefined;
143
+ }
144
+ }
145
+ /** The signal that ends a verified holder. */
146
+ export function planRelease(platform, pid) {
147
+ switch (platform) {
148
+ case "darwin":
149
+ case "linux":
150
+ // SIGTERM, not SIGKILL: `systemd-inhibit` must run its own teardown to
151
+ // drop the logind lock, and a SIGKILLed inhibitor leaves the lock held
152
+ // until logind notices the process is gone.
153
+ return { argv: ["kill", "-TERM", String(pid)] };
154
+ case "win32":
155
+ // `/T` takes the child `Start-Sleep` with it; `/F` because a PowerShell
156
+ // host parked in Start-Sleep does not process a polite close.
157
+ return { argv: ["taskkill", "/PID", String(pid), "/T", "/F"] };
158
+ default:
159
+ return undefined;
160
+ }
161
+ }
162
+ /**
163
+ * Is this command line OUR holder, rather than something that merely mentions
164
+ * it?
165
+ *
166
+ * ADVERSARIAL REVIEW: the first version was `line.includes(marker)`, which
167
+ * answers "yes" for `vim /home/max/notes/caffeinate.md` and for
168
+ * `node ./caffeinate-runner.js`. After a pid rollover that is `release`
169
+ * sending SIGTERM to the operator's editor — precisely the failure the
170
+ * module header promises this check prevents ("A PID IS NOT AN IDENTITY"). A
171
+ * substring test over a whole command line is not an identity test.
172
+ *
173
+ * So the PROGRAM is compared: the first token, unquoted, basename, `.exe`
174
+ * stripped. Windows is the one case where that is not enough and cannot be —
175
+ * the holder there genuinely IS `powershell.exe`, and what makes it ours is
176
+ * the generated type name inside the script it was handed — so the substring
177
+ * test is kept for that case alone, narrowed to a powershell host.
178
+ */
179
+ export function commandMatchesMarker(commandLine, marker) {
180
+ const first = commandLine.trim().split(/\s+/)[0] ?? "";
181
+ const unquoted = first.replace(/^"+|"+$/g, "");
182
+ const base = (unquoted.split(/[\\/]/).pop() ?? unquoted).replace(/\.exe$/i, "");
183
+ if (base === marker)
184
+ return true;
185
+ return /^powershell$/i.test(base) && commandLine.includes(marker);
186
+ }
187
+ export function classifyLiveness(marker, result) {
188
+ if (result.refused === true || result.missing || result.timedOut)
189
+ return "unknown";
190
+ // `ps -p <pid>` exits 1 with no output when nothing has that pid. That is a
191
+ // definite answer, not a failure — but ONLY when `ps` itself said nothing.
192
+ //
193
+ // ADVERSARIAL REVIEW: the first version read every non-zero exit with empty
194
+ // stdout as "gone", and `ps` has other ways to exit non-zero with an empty
195
+ // stdout, all of them loud on stderr. Recorded on macOS 26.6.2:
196
+ // $ ps -o command= -p 999999 ; echo $?
197
+ // ps: process id too large
198
+ // 1
199
+ // Reading that as "gone" makes `release` delete the state file and report
200
+ // `expired` for a holder that may still be running — the exact "could not
201
+ // determine is not no" inversion rule 6 names, on the one path where the
202
+ // consequence is an inhibitor nobody can release before its deadline.
203
+ if (result.code !== 0) {
204
+ if (result.stderr.trim() !== "")
205
+ return "unknown";
206
+ return result.stdout.trim() === "" ? "gone" : "unknown";
207
+ }
208
+ const line = result.stdout.trim();
209
+ if (line === "")
210
+ return "gone";
211
+ return commandMatchesMarker(line, marker) ? "alive" : "reused";
212
+ }
213
+ /** Minutes to a bounded second count, or a refusal naming the ceiling. */
214
+ export function boundedSeconds(minutes) {
215
+ if (!Number.isFinite(minutes) || minutes <= 0) {
216
+ return { refusal: "maxMinutes must be a positive number of minutes" };
217
+ }
218
+ if (minutes > MAX_HOLD_MINUTES) {
219
+ return {
220
+ refusal: `maxMinutes must be at most ${MAX_HOLD_MINUTES} (8 hours) — an assertion held longer than a working day is one nobody is waiting for any more`,
221
+ };
222
+ }
223
+ return { seconds: Math.max(1, Math.round(minutes * 60)) };
224
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `UserPresence` — is there a human at this machine, and should a workflow
3
+ * interrupt them.
4
+ *
5
+ * THIS IS THE TOOL RULE 6 WAS WRITTEN FOR. Every field here has a tempting
6
+ * fallback that inverts the tool's purpose:
7
+ *
8
+ * - "no idle signal, assume active" tells a workflow the operator is at the
9
+ * keyboard when the truth is that nothing could be asked.
10
+ * - "the probe failed, so idle" tells it the opposite, and a workflow that
11
+ * acts on `idle: true` does things unattended.
12
+ * - "screen-lock has no CLI, so not locked" claims the screen is unlocked
13
+ * on every machine in the world.
14
+ *
15
+ * So every field is `number | null` or `boolean | null`, `null` always
16
+ * carries an entry in `unknown` naming the probe and the reason, and the
17
+ * derived `idle` boolean is `null` whenever `idleSeconds` is.
18
+ *
19
+ * ── WHAT EACH PLATFORM CAN ACTUALLY ANSWER ────────────────────────────────
20
+ *
21
+ * macOS Idle comes out of an `ioreg` text dump, in NANOSECONDS. The unit
22
+ * is the trap: reading it as milliseconds makes every host look
23
+ * permanently active, and nothing about the number's shape gives
24
+ * that away. The incantation matters too — the survey sketch's
25
+ * `ioreg -c IOHIDSystem -d 1` prints NOTHING on macOS 26.6.2
26
+ * (verified); `-r` is required, because `-d 1` without it bounds
27
+ * the depth from the root rather than from the matched object:
28
+ *
29
+ * $ ioreg -c IOHIDSystem -d 1 | grep HIDIdleTime # empty
30
+ * $ ioreg -r -c IOHIDSystem -d 1 | grep HIDIdleTime
31
+ * "HIDIdleTime" = 3254341351000
32
+ *
33
+ * Lock state has no supported CLI, and the framework call
34
+ * (CGSessionCopyCurrentDictionary) is a C API. But the same
35
+ * registry carries it, recorded on the same machine with the screen
36
+ * locked:
37
+ *
38
+ * $ ioreg -n Root -d1
39
+ * "IOConsoleLocked" = Yes
40
+ * "IOConsoleUsers" = ({...,"CGSSessionScreenIsLocked"=Yes,...})
41
+ *
42
+ * Both keys are read, `IOConsoleLocked` first. A dump with neither
43
+ * is reported as unknown rather than as unlocked.
44
+ * Linux `xprintidle` is the only idle source and is usually NOT
45
+ * installed; under Wayland there is no idle query at all. Lock
46
+ * state comes from logind's `LockedHint`, which is a HINT — it is
47
+ * set by a screen locker that bothers to call `SetLockedHint`, and
48
+ * a locker that does not leaves it `no` on a locked screen. It is
49
+ * reported with that caveat attached rather than as a fact.
50
+ * Windows `GetLastInputInfo` through a registered PowerShell script, and
51
+ * the presence of `LogonUI` as the lock signal — that process runs
52
+ * only while the lock or login screen is up.
53
+ *
54
+ * And over all of it: an SSH session. Idle time on a machine you are talking
55
+ * to over a wire says nothing about whether a person is sitting at it, so a
56
+ * remote shell reports `sessionKind: "ssh"` and does not pretend otherwise.
57
+ */
58
+ import { type HostPlatform, type SessionEnv } from "../host";
59
+ import type { RunRequest } from "../run";
60
+ export type SessionKind = "console" | "ssh" | "headless" | "unknown";
61
+ export declare const WINDOWS_PRESENCE: import("./escape").PowerShellScript;
62
+ /** One probe this tool might run. Each is independently allowed to fail. */
63
+ export type PresenceProbe = {
64
+ readonly field: "idleSeconds" | "screenLocked";
65
+ readonly request: Omit<RunRequest, "timeoutMs">;
66
+ };
67
+ /**
68
+ * Which probes to run, plus the fields this platform cannot probe at all.
69
+ *
70
+ * A platform with no probe for a field is not an error — it is a field that
71
+ * comes back `null` with its reason, which is the whole contract here.
72
+ */
73
+ export declare function planPresence(platform: HostPlatform, env: SessionEnv): {
74
+ readonly probes: readonly PresenceProbe[];
75
+ readonly notes: ReadonlyArray<[string, string]>;
76
+ };
77
+ /**
78
+ * Parse `HIDIdleTime` out of an ioreg dump.
79
+ *
80
+ * NANOSECONDS. Recorded: `"HIDIdleTime" = 3254341351000` is 3254 seconds —
81
+ * about 54 minutes, which is what the machine had been idle for. Reading it
82
+ * as milliseconds would have reported 3254 SECONDS as 3.2 seconds and made a
83
+ * locked, untouched machine look freshly used.
84
+ *
85
+ * The value passes 2^53 on a host idle for more than about 104 days, so it is
86
+ * divided as a BigInt. A float round there is only seconds of error, but it
87
+ * is error with no reason to exist.
88
+ */
89
+ export declare function parseIoregIdleSeconds(stdout: string): number | null;
90
+ /**
91
+ * Parse macOS lock state out of an `ioreg -n Root -d1` dump.
92
+ *
93
+ * `null` when neither key is present — that is a macOS version this parser
94
+ * has not seen, and guessing "unlocked" for it would claim every such machine
95
+ * has a human looking at it.
96
+ */
97
+ export declare function parseIoregLocked(stdout: string): boolean | null;
98
+ /** `xprintidle` prints milliseconds and nothing else. */
99
+ export declare function parseXprintidleSeconds(stdout: string): number | null;
100
+ /** Parse `loginctl show-session -p …` key=value output. */
101
+ export declare function parseLoginctl(stdout: string): {
102
+ readonly locked: boolean | null;
103
+ readonly type: string | null;
104
+ readonly active: boolean | null;
105
+ };
106
+ /** Parse the two lines the Windows script prints. */
107
+ export declare function parseWindowsPresence(stdout: string): {
108
+ readonly idleSeconds: number | null;
109
+ readonly locked: boolean | null;
110
+ };
111
+ /**
112
+ * What kind of session this is.
113
+ *
114
+ * `ssh` wins over everything: a forwarded `DISPLAY` on an ssh connection is
115
+ * still a wire, and the idle time it would report belongs to the machine at
116
+ * the far end rather than to the person typing.
117
+ */
118
+ export declare function sessionKind(platform: HostPlatform, env: SessionEnv): SessionKind;