pi-insomnia 0.1.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-10-09
10
+
11
+ ### Fixed
12
+
13
+ - Restrict the footer spinner to TUI mode while preserving sleep-inhibitor lifecycle in other modes.
14
+
15
+ ### Changed
16
+
17
+ - Update the shared Pi development and contract-test baseline to 1.1.0; require Node.js >=22.19.0 to match the host runtime. Pi remains a host-supplied peer dependency.
18
+ - Share install telemetry mechanics through `@mocito/install-telemetry` while preserving Pi-specific settings and state paths.
19
+
20
+ ### Fixed
21
+
22
+ - Let `enableInstallTelemetry: false` override an enabled `PI_TELEMETRY` environment flag.
23
+
24
+ ## [0.1.3] - 2026-07-17
25
+
26
+ ### Fixed
27
+
28
+ - Keep macOS sleep inhibition active across automatic retries, compaction retries, and queued follow-up work until Pi emits `agent_settled`.
29
+
9
30
  ## [0.1.2] - 2026-07-01
10
31
 
11
32
  ### Changed
@@ -14,7 +35,11 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
14
35
 
15
36
  ## [0.1.1] - 2026-06-09
16
37
 
17
- ## [0.1.0] - TBD
38
+ ### Changed
39
+
40
+ - Replace the static footer icon with an animated Braille spinner while sleep inhibition is active.
41
+
42
+ ## [0.1.0] - 2026-06-08
18
43
 
19
44
  ### Added
20
45
 
package/README.md CHANGED
@@ -1,18 +1,22 @@
1
1
  # pi-insomnia
2
2
 
3
- A source-distributed [Pi](https://pi.dev) package that prevents macOS idle sleep while the Pi agent is working.
3
+ Keep long-running [Pi](https://pi.dev) tasks alive when you step away from your Mac.
4
+
5
+ `pi-insomnia` automatically prevents idle system sleep while Pi is working, then releases the sleep assertion as soon as the agent fully settles. No commands or manual activation needed.
4
6
 
5
7
  > [!WARNING]
6
8
  > Pi packages can execute arbitrary code through extensions. Review package source before installing any third-party Pi package.
7
9
 
8
10
  ## Features
9
11
 
10
- - Automatically starts a macOS sleep assertion when a Pi agent run starts.
11
- - Automatically releases the assertion when the agent run ends.
12
- - Cleans up on Pi session shutdown, reloads, and quits.
13
- - Uses the macOS built-in `/usr/bin/caffeinate` command; no third-party runtime dependency is required.
14
- - Shows a small `☕ sleep inhibited` footer status while active in UI modes.
15
- - No slash commands, tools, or manual activation needed.
12
+ - **Automatic protection** — inhibit idle sleep only while Pi has active work.
13
+ - **Full-run awareness** — stay awake through retries, compaction, and queued follow-up work, then release when Pi settles.
14
+ - **Zero setup** — use macOS built-in `caffeinate`; no commands, configuration, or third-party runtime dependency.
15
+ - **Visible state** — show a small braille spinner with `sleep inhibited` in the TUI footer while active.
16
+ - **Safe cleanup** — release assertions on session shutdown, reload, and quit.
17
+
18
+ The spinner uses palette-neutral text and does not run in print, JSON or RPC
19
+ mode. Sleep inhibition itself follows the same active-work lifecycle in all modes.
16
20
 
17
21
  ## Platform support
18
22
 
@@ -24,7 +28,7 @@ On macOS, the extension launches:
24
28
  /usr/bin/caffeinate -i -w <pi-pid>
25
29
  ```
26
30
 
27
- `-i` prevents idle system sleep while the process is running. `-w <pi-pid>` also ties the assertion to the Pi process so the helper exits if Pi exits unexpectedly. The extension still explicitly terminates the helper when the agent becomes idle.
31
+ `-i` prevents idle system sleep while the process is running. `-w <pi-pid>` also ties the assertion to the Pi process so the helper exits if Pi exits unexpectedly. The extension still explicitly terminates the helper when Pi becomes fully idle after any automatic retries, compaction retries, or queued follow-up work.
28
32
 
29
33
  ## Installation
30
34
 
package/SECURITY.md CHANGED
@@ -25,4 +25,4 @@ Do not commit API keys, tokens, credentials, local settings, or machine-specific
25
25
 
26
26
  On macOS, the extension starts the system-provided `/usr/bin/caffeinate` helper while the Pi agent is working. It does not request elevated privileges, pass user prompts to the helper, or run shell commands. The helper is started directly with fixed arguments and is terminated when the agent becomes idle or the Pi session shuts down.
27
27
 
28
- On startup, the extension sends a best-effort install/update telemetry ping to `mocito.dev` once per package version unless Pi telemetry is disabled, offline mode is enabled, or Pi runs in CI. The ping includes only the package name, version, and parsed platform/runtime/architecture from its User-Agent; it does not include prompts, file paths, config values, environment variables, or API keys.
28
+ On startup, `@mocito/install-telemetry` sends a best-effort install/update telemetry ping to the configured telemetry endpoint once per package version unless Pi telemetry is disabled, offline mode is enabled, or Pi runs in CI. The ping includes only the package name, version, and parsed platform/runtime/architecture from its User-Agent; it does not include prompts, file paths, config values, environment variables, or API keys.
@@ -12,12 +12,12 @@ let spinnerTimer: ReturnType<typeof setInterval> | undefined;
12
12
  let spinnerFrame = 0;
13
13
 
14
14
  function setStatus(ctx: ExtensionContext, text: string | undefined): void {
15
- if (!ctx.hasUI) return;
15
+ if (ctx.mode !== "tui") return;
16
16
  ctx.ui.setStatus(STATUS_KEY, text);
17
17
  }
18
18
 
19
19
  function startSpinner(ctx: ExtensionContext): void {
20
- if (!ctx.hasUI || spinnerTimer) return;
20
+ if (ctx.mode !== "tui" || spinnerTimer) return;
21
21
  spinnerFrame = 0;
22
22
  setStatus(ctx, `${SPINNER_FRAMES[0]} sleep inhibited`);
23
23
  spinnerTimer = setInterval(() => {
@@ -34,16 +34,17 @@ function stopSpinner(ctx: ExtensionContext): void {
34
34
  setStatus(ctx, undefined);
35
35
  }
36
36
 
37
- export default function piInsomnia(pi: ExtensionAPI) {
37
+ export default function piInsomnia(
38
+ pi: ExtensionAPI,
39
+ inhibitor = new MacSleepInhibitor(),
40
+ ) {
38
41
  reportInstallTelemetry();
39
42
 
40
- const inhibitor = new MacSleepInhibitor();
41
-
42
43
  pi.on("agent_start", async (_event, ctx) => {
43
44
  if (inhibitor.acquire()) startSpinner(ctx);
44
45
  });
45
46
 
46
- pi.on("agent_end", async (_event, ctx) => {
47
+ pi.on("agent_settled", async (_event, ctx) => {
47
48
  inhibitor.release();
48
49
  if (!inhibitor.isInhibiting) stopSpinner(ctx);
49
50
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-insomnia",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Prevent macOS idle sleep while Pi agents are working.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,20 +45,25 @@
45
45
  "scripts": {
46
46
  "check": "tsc --noEmit",
47
47
  "typecheck": "tsc --noEmit",
48
+ "test": "node --import tsx --test tests/*.test.mjs",
48
49
  "pack:dry-run": "npm pack --dry-run"
49
50
  },
50
51
  "peerDependencies": {
51
52
  "@earendil-works/pi-coding-agent": "*"
52
53
  },
53
54
  "devDependencies": {
54
- "@earendil-works/pi-coding-agent": "^0.80.0",
55
- "@types/node": "^25.9.3",
56
- "typescript": "^6.0.3"
55
+ "@earendil-works/pi-coding-agent": "1.1.0",
56
+ "@types/node": "^26.6.3",
57
+ "tsx": "^4.23.15",
58
+ "typescript": "^7.0.2"
57
59
  },
58
60
  "publishConfig": {
59
61
  "access": "public"
60
62
  },
61
63
  "engines": {
62
- "node": ">=20.6.0"
64
+ "node": ">=22.19.0"
65
+ },
66
+ "dependencies": {
67
+ "@mocito/install-telemetry": "0.1.1"
63
68
  }
64
69
  }
@@ -1,12 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
2
  import { join } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
4
+ import { reportInstallTelemetry as report } from "@mocito/install-telemetry";
5
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
6
 
7
7
  const PACKAGE_NAME = "pi-insomnia";
8
- const INSTALL_TELEMETRY_URL = "https://mocito.dev/api/report-install";
9
- const INSTALL_TELEMETRY_TIMEOUT_MS = 5000;
8
+ const INSTALL_TELEMETRY_ENDPOINT = "https://mocito.dev/api/report-install";
10
9
  const CI_ENVIRONMENT_VARIABLES = [
11
10
  "APPVEYOR",
12
11
  "BITBUCKET_BUILD_NUMBER",
@@ -24,10 +23,6 @@ const CI_ENVIRONMENT_VARIABLES = [
24
23
  "VERCEL",
25
24
  ];
26
25
 
27
- interface InstallTelemetryState {
28
- lastReportedVersion?: string;
29
- }
30
-
31
26
  interface PiSettingsDocument {
32
27
  enableInstallTelemetry?: unknown;
33
28
  }
@@ -51,18 +46,15 @@ function isPresentEnvFlag(value: string | undefined): boolean {
51
46
  return normalized !== "0" && normalized !== "false" && normalized !== "no";
52
47
  }
53
48
 
54
- function isCiEnvironment(): boolean {
55
- if (isTruthyEnvFlag(process.env.CI)) return true;
56
- return CI_ENVIRONMENT_VARIABLES.some((name) => isPresentEnvFlag(process.env[name]));
57
- }
58
-
59
- function isInstallTelemetryEnabled(): boolean {
60
- if (isCiEnvironment()) return false;
61
- if (isTruthyEnvFlag(process.env.PI_OFFLINE)) return false;
62
- if (process.env.PI_TELEMETRY !== undefined) return isTruthyEnvFlag(process.env.PI_TELEMETRY);
49
+ export function isInstallTelemetryEnabled(env: NodeJS.ProcessEnv = process.env, settingsPath = join(getAgentDir(), "settings.json")): boolean {
50
+ if (isTruthyEnvFlag(env.CI)) return false;
51
+ if (CI_ENVIRONMENT_VARIABLES.some((name) => isPresentEnvFlag(env[name]))) return false;
52
+ if (isTruthyEnvFlag(env.PI_OFFLINE)) return false;
63
53
 
64
- const settings = readJsonFile(join(getAgentDir(), "settings.json")) as PiSettingsDocument;
65
- return settings.enableInstallTelemetry !== false;
54
+ const settings = readJsonFile(settingsPath) as PiSettingsDocument;
55
+ if (settings.enableInstallTelemetry === false) return false;
56
+ if (env.PI_TELEMETRY !== undefined) return isTruthyEnvFlag(env.PI_TELEMETRY);
57
+ return true;
66
58
  }
67
59
 
68
60
  function getPackageVersion(): string {
@@ -70,35 +62,16 @@ function getPackageVersion(): string {
70
62
  return typeof packageJson.version === "string" && packageJson.version.length > 0 ? packageJson.version : "0.0.0";
71
63
  }
72
64
 
73
- function getInstallTelemetryUserAgent(version: string): string {
74
- const runtimeVersions = process.versions as NodeJS.ProcessVersions & { bun?: string };
75
- const runtime = runtimeVersions.bun ? `bun/${runtimeVersions.bun}` : `node/${process.version}`;
76
- return `${PACKAGE_NAME}/${version} (${process.platform}; ${runtime}; ${process.arch})`;
77
- }
78
-
79
- async function reportInstallTelemetryAsync(): Promise<void> {
65
+ export function reportInstallTelemetry(): void {
80
66
  try {
81
- if (!isInstallTelemetryEnabled()) return;
82
-
83
- const version = getPackageVersion();
84
- const extensionsDir = join(getAgentDir(), "extensions");
85
- const statePath = join(extensionsDir, "pi-insomnia-install.json");
86
- const state = readJsonFile(statePath) as InstallTelemetryState;
87
- if (state.lastReportedVersion === version) return;
88
-
89
- await mkdir(extensionsDir, { recursive: true });
90
- await writeFile(statePath, `${JSON.stringify({ lastReportedVersion: version }, null, 2)}\n`, "utf8");
91
-
92
- const params = new URLSearchParams({ tool: PACKAGE_NAME, version });
93
- await fetch(`${INSTALL_TELEMETRY_URL}?${params.toString()}`, {
94
- headers: { "User-Agent": getInstallTelemetryUserAgent(version) },
95
- signal: AbortSignal.timeout(INSTALL_TELEMETRY_TIMEOUT_MS),
96
- });
67
+ void report({
68
+ endpoint: INSTALL_TELEMETRY_ENDPOINT,
69
+ tool: PACKAGE_NAME,
70
+ version: getPackageVersion(),
71
+ statePath: join(getAgentDir(), "extensions", "pi-insomnia-install.json"),
72
+ enabled: isInstallTelemetryEnabled(),
73
+ }).catch(() => undefined);
97
74
  } catch {
98
- // Best-effort telemetry: ignore settings, filesystem, and network failures.
75
+ // Best-effort telemetry: ignore local policy and filesystem failures.
99
76
  }
100
77
  }
101
-
102
- export function reportInstallTelemetry(): void {
103
- void reportInstallTelemetryAsync();
104
- }
@@ -13,7 +13,7 @@ export class MacSleepInhibitor {
13
13
  private readonly pid: number;
14
14
  private readonly caffeinatePath: string;
15
15
  private child: ChildProcess | undefined;
16
- private activeRequests = 0;
16
+ private active = false;
17
17
 
18
18
  constructor(options: MacSleepInhibitorOptions = {}) {
19
19
  this.platform = options.platform ?? process.platform;
@@ -26,7 +26,7 @@ export class MacSleepInhibitor {
26
26
  }
27
27
 
28
28
  get isActive(): boolean {
29
- return this.activeRequests > 0;
29
+ return this.active;
30
30
  }
31
31
 
32
32
  get isInhibiting(): boolean {
@@ -36,7 +36,7 @@ export class MacSleepInhibitor {
36
36
  acquire(): boolean {
37
37
  if (!this.isSupported) return false;
38
38
 
39
- this.activeRequests += 1;
39
+ this.active = true;
40
40
  this.start();
41
41
  return this.isInhibiting;
42
42
  }
@@ -44,12 +44,12 @@ export class MacSleepInhibitor {
44
44
  release(): void {
45
45
  if (!this.isSupported) return;
46
46
 
47
- if (this.activeRequests > 0) this.activeRequests -= 1;
48
- if (this.activeRequests === 0) this.stop();
47
+ this.active = false;
48
+ this.stop();
49
49
  }
50
50
 
51
51
  forceStop(): void {
52
- this.activeRequests = 0;
52
+ this.active = false;
53
53
  this.stop();
54
54
  }
55
55