Skip to content

Fix/linux support#1126

Closed
dmcgoldrd wants to merge 7 commits into
danielmiessler:mainfrom
dmcgoldrd:fix/linux-support
Closed

Fix/linux support#1126
dmcgoldrd wants to merge 7 commits into
danielmiessler:mainfrom
dmcgoldrd:fix/linux-support

Conversation

@dmcgoldrd

Copy link
Copy Markdown

What this is

A bug fix PR — closes the gap that prevented Releases/v5.0.0/.claude/ from installing end-to-end on Linux. macOS path is unchanged.

Every edit is scoped to Releases/v5.0.0/.claude/ — no impact on v4.x or anywhere else in the tree.

What it changes (7 commits)

  • Add runtime package.json at .claude/ root so bun install resolves Pulse runtime deps on a fresh extract.
  • Resolve bash absolutely in PULSE/lib.ts spawnScript() so subprocesses don't depend on PATH ordering.
  • Add PULSE/pai-pulse.service — systemd user-unit template, companion to the existing com.pai.pulse.plist. Same __HOME__ / __BUN_PATH__ placeholder pattern.
  • Make PULSE/manage.sh OS-awarestart/stop/install/uninstall route to launchd on macOS, systemctl --user on Linux. Install kills any stale bun.*pulse.ts, renders the unit, runs daemon-reload + enable --now, and verifies :31337 binds within 10s — failing loud if not.
  • Casing fix Pulse/PULSE/ across 9 TypeScript files. macOS didn't care, Linux did.
  • Remove .cursor/rules/*.mdc IDE artifacts from the public release.
  • Make installer wizard messaging + engine/validate.ts OS-aware — neutral "system service" wording instead of "launchd"; post-install validator branches on platform() and checks the correct artifact (launchd plist on macOS, systemd unit on Linux) so Linux installs no longer false-fail with Pulse launchd agent: Not installed.

Tested on a fresh-ish system

Arch Linux x86_64, kernel 6.19.x, bun 1.3.6 via mise, fresh ~/.claude/ with prior install backed up to ~/.claude.backup-<timestamp>/.

1. bash ~/.claude/PAI/PULSE/manage.sh install — the OS-aware install path the wizard calls:

$ bash ~/.claude/PAI/PULSE/manage.sh install
Created symlink '~/.config/systemd/user/default.target.wants/pai-pulse.service' →
                '~/.config/systemd/user/pai-pulse.service'.
Hint: 'sudo loginctl enable-linger <user>' to keep Pulse running after logout.
PAI Pulse installed and verified on port 31337 (bun: ~/.local/share/mise/installs/bun/1.3.5/bin/bun)
$ echo $?
0

2. Post-install probes:

$ systemctl --user is-enabled pai-pulse.service
enabled
$ systemctl --user is-active pai-pulse.service
active
$ ss -tlnp | grep :31337
LISTEN 0 512 127.0.0.1:31337 *:* users:(("bun",pid=<pid>,fd=257))
$ curl -s -X POST http://localhost:31337/notify -H "Content-Type: application/json" \
       -d '{"message":"smoke","voice_enabled":false}'
{"status":"success","message":"Notification sent"}

Any stale pre-install pulse process from the backup tree was correctly killed by the install's pkill -9 -f "bun.*pulse.ts" step.

3. Validate.ts spot-check (the platform() branch from the last commit):

{
  platform: "linux",
  isLinux: true,
  serviceUnit: "~/.config/systemd/user/pai-pulse.service",
  installed: true,
  label: "Pulse systemd unit"
}

Confirms the post-install validator now passes on Linux instead of false-failing on a missing launchd plist.

4. Typecheck: bun tsc --noEmit -p PAI-Install/tsconfig.json — no errors in engine/actions.ts or engine/validate.ts.

Before / after example

Linux user runs curl -sSL https://ourpai.ai/install.sh | bash, picks "Yes" at the Pulse install prompt, then runs the validator:

Probe Before this PR After this PR
~/.config/systemd/user/pai-pulse.service rendered n/a (manage.sh had no Linux path)
systemctl --user is-active inactive (no unit) active
Listener on :31337 nothing bun (PID matches state/pulse.pid)
Validator output Pulse launchd agent: Not installed (false-negative on Linux) Pulse systemd unit: Installed at ~/.config/systemd/user/pai-pulse.service
curl POST /notify connection refused HTTP 200

macOS impact

None. All OS branches default to the macOS path; manage.sh and validate.ts route via case "$OS" / platform() === "linux" so existing macOS behavior is byte-identical.

Known follow-ups (intentionally not in this PR)

  • engine/actions.ts:1859 descriptive emit message still says "Installing it as a launchd agent makes it auto-start..." — clean cosmetic follow-up to neutralize.
  • The Pulse menu bar app step (engine/actions.ts:1874+) is structurally macOS-only. No cross-platform menu bar work in this PR.

dmcgoldrd added 7 commits May 1, 2026 12:37
Pulse and several PAI tools import grammy, jose, minisearch, smol-toml,
yaml, openai, and @anthropic-ai/claude-agent-sdk at runtime. The v5.0.0
release ships without a top-level package.json, so a fresh extraction
fails immediately on:

  $ bun run pulse.ts
  error: Cannot find package 'smol-toml' from '/home/danielm/.claude/PAI/PULSE/pulse.ts'

The release .gitignore intentionally ignores /package.json to block
accidental Claude-Code debris. Adds an explicit negation for the
deliberate runtime manifest.

After this change a fresh install runs:
  cd ~/.claude && bun install
and Pulse starts cleanly. Affects every platform — not Linux-specific.
Three .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc files
shipped as symlinks to ../../CLAUDE.md. They're Cursor IDE artifacts
(.mdc with description/globs/alwaysApply frontmatter) — Claude Code
reads CLAUDE.md, not .cursor/rules/*.mdc. Nothing in PAI references
these files (only docs in skills/Migrate/SKILL.md mention Cursor as
a migration source).

These also crashed Bun's fs watcher on Linux because the symlinks
extracted as absolute self-references after tarball + rsync, producing
ELOOP and exiting Pulse. Deleting them removes both the dead-weight
artifact and the failure mode in one cut.

Removed:
  PAI/PULSE/Observability/.cursor/
  skills/Art/Tools/.cursor/
  skills/Prompting/Templates/Tools/.cursor/
Bun.spawn(["bash", "-c", ...]) relies on PATH for command resolution.
On Linux, when Pulse runs under a minimal-env service manager (or even
launched headless from some shells) PATH can be too sparse for
posix_spawn to find bash, producing:

  assistant-tasks failed: ENOENT no such file or directory, posix_spawn 'bash'

Resolve bash via Bun.which("bash") at module load and fall back to
/bin/bash — present on macOS natively and on every mainstream Linux
distro. macOS behavior unchanged because /bin/bash is the same path it
already used implicitly.
Linux companion to com.pai.pulse.plist. Mirrors the plist's behavior:
  - same WorkingDirectory and ExecStart pattern
  - same __HOME__ / __BUN_PATH__ placeholders for manage.sh substitution
  - same stdout/stderr log paths
  - Restart=always with RestartSec=30 (mirrors KeepAlive + ThrottleInterval=30)

Standalone — manage.sh wiring lands in the next commit so this can be
verified in isolation. Manual install instructions are at the top of
the file for users who want to enable Pulse on Linux today.
Pre-existing manage.sh hard-coded launchctl + ~/Library/LaunchAgents
which makes Pulse undeployable on Linux: the script silently no-ops
launchctl, the plist becomes inert, and the daemon never starts on
boot/login.

Refactor:
  - extract per-OS lifecycle into service_start / service_stop /
    service_install / service_uninstall helpers
  - Darwin path is byte-for-byte equivalent to the prior behavior
  - Linux path renders pai-pulse.service from the v5 template,
    drops it under ~/.config/systemd/user, runs daemon-reload,
    and enable --now so Pulse starts immediately
  - install verification (curl :31337 loop) preserved; on Linux
    failure, the error hint also surfaces 'journalctl --user -u
    pai-pulse' so users find the right log
  - prints a hint to enable-linger so the service survives logout
  - rejects unsupported uname with a clear error
The release ships the directory as PAI/PULSE/ (uppercase) but 11 path
joins across 9 .ts files use "Pulse" (TitleCase). On macOS this is
masked by the case-insensitive default filesystem (HFS+/APFS). On Linux
ext4 (case-sensitive), the join produces a path that does not exist:

  GET / → HTTP 404   # observability dashboard 404s entirely
  STATE_DIR / state.json reads fail
  cron jobs that cd into PULSE_DIR fall back to wrong cwd

Aligns every path join with the on-disk directory name (PULSE). macOS
behavior is unchanged because the case-insensitive FS treats both
spellings as the same inode.

Files updated:
  PAI/PULSE/lib.ts                                (1 ref)
  PAI/PULSE/pulse-old.ts                          (1 ref)
  PAI/PULSE/pulse-unified.ts                      (1 ref)
  PAI/PULSE/run-job.ts                            (1 ref)
  PAI/PULSE/setup.ts                              (1 ref)
  PAI/PULSE/Observability/observability.ts        (3 refs)
  PAI/PULSE/Performance/cost-aggregator.ts        (1 ref)
  PAI/PULSE/checks/github-work.ts                 (1 ref)
  PAI/PULSE/checks/notification-governor.ts       (1 ref)
  PAI/PULSE/checks/poller-meta-monitor.ts         (2 refs)
  PAI/PULSE/modules/imessage.ts                   (2 refs)
  PAI/PULSE/modules/user-index.ts                 (1 ref)
Three small Linux-support warts the prior commits missed:

- engine/actions.ts: installPulse() comment block now describes both
  launchd (macOS) and systemd (Linux) paths instead of launchd-only.
- engine/actions.ts: wizard prompt label changed from "Install Pulse
  as a system launchd service?" to "Install Pulse as a system service?"
  so Linux users see neutral terminology.
- engine/validate.ts: the post-install "auto-start on login" check
  now branches on platform() — checks ~/Library/LaunchAgents/
  com.pai.pulse.plist on macOS and ~/.config/systemd/user/
  pai-pulse.service on Linux. Previously hardcoded the macOS path,
  producing a false-negative "Pulse launchd agent: Not installed"
  on healthy Linux installs.

Verified on Arch Linux: validate now reports
"Pulse systemd unit: Installed at ~/.config/systemd/user/pai-pulse.service".
@danielmiessler

Copy link
Copy Markdown
Owner

Hey @dmcgoldrd, thanks for raising this, and sorry it sat for a while.

We're changing how LifeOS ships. Instead of cloning a full ~/.claude directory and running it as a complete system, LifeOS is becoming a skill you install through an agentic installer. The installer hands integration to your own AI, which reads your actual machine (your OS, your paths, your harness) and wires the hooks and system prompt in where they belong.

That's aimed right at what you hit here. The old "one directory, one layout, hope it matches your setup" approach is exactly what broke for so many people, and the new model should handle it far better because your AI does the integration per machine instead of us guessing.

So we're closing this in prep for that release. If it still bites you once the skill-based version is out, reopen or file a fresh one and we'll jump on it. Appreciate you taking the time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants