diff --git a/.claude/commands/memo.md b/.claude/templates/memo.md similarity index 100% rename from .claude/commands/memo.md rename to .claude/templates/memo.md diff --git a/setup.sh b/setup.sh index 707ba95c..2d428b5a 100755 --- a/setup.sh +++ b/setup.sh @@ -649,6 +649,18 @@ else echo "Skipping hooks (no .claude/hooks/ directory found)" fi +# --- Install Claude Code commands (provider level) --- +# memo.md belongs at provider level — works in all projects. +# prep.md stays at repo root only — it's AIPass-specific. +COMMANDS_SRC="$SCRIPT_DIR/.claude/templates" +COMMANDS_DST="$HOME/.claude/commands" +if [ -f "$COMMANDS_SRC/memo.md" ]; then + mkdir -p "$COMMANDS_DST" + cp -n "$COMMANDS_SRC/memo.md" "$COMMANDS_DST/memo.md" 2>/dev/null && \ + echo " memo.md -> $COMMANDS_DST/ (installed)" || \ + echo " memo.md -> $COMMANDS_DST/ (already exists, skipped)" +fi + # --- Install Codex CLI hooks --- if command -v codex &>/dev/null; then if [ -f "$SCRIPT_DIR/.codex/hooks.json" ]; then diff --git a/src/aipass/aipass/.claude/commands/memo.md b/src/aipass/aipass/.claude/commands/memo.md deleted file mode 100644 index 948ac39b..00000000 --- a/src/aipass/aipass/.claude/commands/memo.md +++ /dev/null @@ -1,23 +0,0 @@ -# Memory Update - -Purpose: Update branch memory files after completing work this session. - -## Execution - -1. Read `.trinity/passport.json` first — re-absorb your identity, role, and principles before writing memories -2. Review what was done this session (context, recent changes, key decisions) -3. Update each file below as needed -4. Confirm completion — list files updated - -## Memory Roles - -Each memory file plays a distinct role. Update based on what actually changed this session. - -- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. Don't touch it just to touch it. -- **`.trinity/local.json`** — YOUR MEMORY. Session history and key_learnings. Add a session entry for significant work. Add key_learnings for facts you'd need next time. Trim oldest sessions if over 20. -- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points, flow states. Skip entirely if nothing new about the user this session. -- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate and drop quick notes in the Notepad section. - -## If Relevant - -- **README.md** — Does it reflect current state? Update if stale. diff --git a/src/aipass/aipass/.claude/commands/prep.md b/src/aipass/aipass/.claude/commands/prep.md deleted file mode 100644 index 4a56e0a3..00000000 --- a/src/aipass/aipass/.claude/commands/prep.md +++ /dev/null @@ -1,57 +0,0 @@ -# Session Wrap-Up - -Purpose: Button up everything at the end of a session — or before a /compact. Memories, plans, git — all tidy. Works for both closing out a chat and preparing for compaction. - -**Workflow:** `/prep` → review output → close chat or `/compact` - -## Execution - -1. Read `.trinity/passport.json` first — re-absorb your identity before writing anything -2. Do ALL of the following, then confirm what was updated - -## 1. Memories - -Each memory file plays a distinct role. Update based on what actually changed this session. - -- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. -- **`.trinity/local.json`** — YOUR MEMORY. Add/update session entry with a summary of work done. Add key_learnings for anything learned. Trim oldest sessions if over 20. -- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points. Skip if nothing new about the user this session. -- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate. - -## 2. Active Plans - -- Check any DPLANs or FPLANs referenced in this session -- Update their execution logs, status, decision logs with current state -- If a plan was completed, note it (but don't close — the user does that) - -## 3. Git State - -- Run `git status` — report uncommitted changes -- If there's a logical commit waiting, suggest it (don't commit without asking) -- Note the current branch and any open PRs - -## 4. Inbox - -- Run `drone @ai_mail inbox 2>/dev/null` — report any unread emails -- Close any that were already processed but not formally closed - -## 5. Loose Ends - -- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions -- If anything can't survive compaction (e.g., agent IDs needed for resume), write it to STATUS.local.md Notepad - -## Confirm - -List everything updated. Format: -``` -Prep complete: -- local.json: [what was added] -- observations.json: [updated / skipped] -- STATUS.local.md: [updated / skipped] -- Plans: [which ones updated] -- Git: [branch, uncommitted count, suggestion] -- Inbox: [count, action taken] -- Loose ends: [any flagged] - -Ready to close out or /compact. -``` diff --git a/src/aipass/aipass/.claude/settings.json b/src/aipass/aipass/.claude/settings.json index defb5cde..58f965ff 100644 --- a/src/aipass/aipass/.claude/settings.json +++ b/src/aipass/aipass/.claude/settings.json @@ -1,4 +1,42 @@ { + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/branch_prompt_loader.py" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/email_notification.py" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/identity_injector.py" + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/pre_compact.py" + } + ] + } + ] + }, "env": { "AIPASS_HOME": "/home/patrick/Projects/AIPass" } diff --git a/src/aipass/aipass/apps/handlers/init/bootstrap.py b/src/aipass/aipass/apps/handlers/init/bootstrap.py index 8a2e1c99..3181b41d 100644 --- a/src/aipass/aipass/apps/handlers/init/bootstrap.py +++ b/src/aipass/aipass/apps/handlers/init/bootstrap.py @@ -319,6 +319,7 @@ def init_project(target: Path, project_name: str | None = None) -> dict: created.append(str(settings_path)) # 9b. .claude/commands/prep.md — /prep session wrap-up slash command + # Only prep.md here — memo.md belongs at provider level (~/.claude/commands/) commands_dir = claude_dir / "commands" commands_dir.mkdir(exist_ok=True) prep_path = commands_dir / "prep.md" @@ -326,12 +327,6 @@ def init_project(target: Path, project_name: str | None = None) -> dict: prep_path.write_text(sc.prep_md(), encoding="utf-8") created.append(str(prep_path)) - # 9c. .claude/commands/memo.md — /memo memory update slash command - memo_path = commands_dir / "memo.md" - if not memo_path.exists(): - memo_path.write_text(sc.memo_md(), encoding="utf-8") - created.append(str(memo_path)) - # 9d. Ship enforcement + injector hooks from AIPass install if aipass_home: shipped = _ship_hooks(aipass_home, target) @@ -465,6 +460,7 @@ def update_project(target: Path) -> dict: already_current.append(str(gemini_md_path)) # .claude/commands/prep.md — managed slash command, refresh to latest + # Only prep.md — memo.md belongs at provider level (~/.claude/commands/) commands_dir = claude_dir / "commands" commands_dir.mkdir(exist_ok=True) prep_path = commands_dir / "prep.md" @@ -475,15 +471,6 @@ def update_project(target: Path) -> dict: else: already_current.append(str(prep_path)) - # .claude/commands/memo.md — managed slash command, refresh to latest - memo_path = commands_dir / "memo.md" - generated = sc.memo_md() - if not memo_path.exists() or memo_path.read_text(encoding="utf-8") != generated: - memo_path.write_text(generated, encoding="utf-8") - updated.append(str(memo_path)) - else: - already_current.append(str(memo_path)) - # Re-sync enforcement + injector hooks from AIPass install hook_home = aipass_home or _detect_aipass_home() if hook_home: diff --git a/src/aipass/aipass/status/.claude/commands/memo.md b/src/aipass/aipass/status/.claude/commands/memo.md deleted file mode 100644 index 948ac39b..00000000 --- a/src/aipass/aipass/status/.claude/commands/memo.md +++ /dev/null @@ -1,23 +0,0 @@ -# Memory Update - -Purpose: Update branch memory files after completing work this session. - -## Execution - -1. Read `.trinity/passport.json` first — re-absorb your identity, role, and principles before writing memories -2. Review what was done this session (context, recent changes, key decisions) -3. Update each file below as needed -4. Confirm completion — list files updated - -## Memory Roles - -Each memory file plays a distinct role. Update based on what actually changed this session. - -- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. Don't touch it just to touch it. -- **`.trinity/local.json`** — YOUR MEMORY. Session history and key_learnings. Add a session entry for significant work. Add key_learnings for facts you'd need next time. Trim oldest sessions if over 20. -- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points, flow states. Skip entirely if nothing new about the user this session. -- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate and drop quick notes in the Notepad section. - -## If Relevant - -- **README.md** — Does it reflect current state? Update if stale. diff --git a/src/aipass/aipass/status/.claude/commands/prep.md b/src/aipass/aipass/status/.claude/commands/prep.md deleted file mode 100644 index 4a56e0a3..00000000 --- a/src/aipass/aipass/status/.claude/commands/prep.md +++ /dev/null @@ -1,57 +0,0 @@ -# Session Wrap-Up - -Purpose: Button up everything at the end of a session — or before a /compact. Memories, plans, git — all tidy. Works for both closing out a chat and preparing for compaction. - -**Workflow:** `/prep` → review output → close chat or `/compact` - -## Execution - -1. Read `.trinity/passport.json` first — re-absorb your identity before writing anything -2. Do ALL of the following, then confirm what was updated - -## 1. Memories - -Each memory file plays a distinct role. Update based on what actually changed this session. - -- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. -- **`.trinity/local.json`** — YOUR MEMORY. Add/update session entry with a summary of work done. Add key_learnings for anything learned. Trim oldest sessions if over 20. -- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points. Skip if nothing new about the user this session. -- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate. - -## 2. Active Plans - -- Check any DPLANs or FPLANs referenced in this session -- Update their execution logs, status, decision logs with current state -- If a plan was completed, note it (but don't close — the user does that) - -## 3. Git State - -- Run `git status` — report uncommitted changes -- If there's a logical commit waiting, suggest it (don't commit without asking) -- Note the current branch and any open PRs - -## 4. Inbox - -- Run `drone @ai_mail inbox 2>/dev/null` — report any unread emails -- Close any that were already processed but not formally closed - -## 5. Loose Ends - -- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions -- If anything can't survive compaction (e.g., agent IDs needed for resume), write it to STATUS.local.md Notepad - -## Confirm - -List everything updated. Format: -``` -Prep complete: -- local.json: [what was added] -- observations.json: [updated / skipped] -- STATUS.local.md: [updated / skipped] -- Plans: [which ones updated] -- Git: [branch, uncommitted count, suggestion] -- Inbox: [count, action taken] -- Loose ends: [any flagged] - -Ready to close out or /compact. -``` diff --git a/src/aipass/aipass/status/.claude/settings.json b/src/aipass/aipass/status/.claude/settings.json index defb5cde..58f965ff 100644 --- a/src/aipass/aipass/status/.claude/settings.json +++ b/src/aipass/aipass/status/.claude/settings.json @@ -1,4 +1,42 @@ { + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/branch_prompt_loader.py" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/email_notification.py" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/identity_injector.py" + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/pre_compact.py" + } + ] + } + ] + }, "env": { "AIPASS_HOME": "/home/patrick/Projects/AIPass" } diff --git a/src/aipass/aipass/tests/test_bootstrap.py b/src/aipass/aipass/tests/test_bootstrap.py index 6d518526..f1238bed 100644 --- a/src/aipass/aipass/tests/test_bootstrap.py +++ b/src/aipass/aipass/tests/test_bootstrap.py @@ -104,7 +104,6 @@ def test_init_project_creates_all_expected_files(tmp_path): target / ".gitignore", target / ".claude" / "settings.json", target / ".claude" / "commands" / "prep.md", - target / ".claude" / "commands" / "memo.md", target / ".ai_mail.local" / "inbox.json", ] for f in expected_files: @@ -119,8 +118,8 @@ def test_init_project_creates_all_expected_files(tmp_path): # No local prompt at project level (belongs in agent dirs only) assert not (target / ".aipass" / "aipass_local_prompt.md").exists() - # 11 items + 2 commands + 7 shipped hooks (when AIPASS_HOME detected) = 20 - assert len(result["created_files"]) == 20 + # 11 items + 1 command (prep.md) + 7 shipped hooks (when AIPASS_HOME detected) = 19 + assert len(result["created_files"]) == 19 def test_init_project_return_dict_structure(tmp_path): @@ -354,7 +353,7 @@ def test_init_project_auto_creates_target_dir(tmp_path): assert target.is_dir() assert result["project_name"] == "NESTED" - assert len(result["created_files"]) == 20 + assert len(result["created_files"]) == 19 def test_init_project_defaults_name_from_directory(tmp_path): @@ -410,8 +409,8 @@ def test_init_project_skips_existing_optional_files(tmp_path): result = init_project(target, project_name="eta") - # Registry + prep.md + memo.md + 7 shipped hooks = 10 (everything else pre-existed) - assert len(result["created_files"]) == 10 + # Registry + prep.md + 7 shipped hooks = 9 (everything else pre-existed) + assert len(result["created_files"]) == 9 # Verify pre-existing files were NOT overwritten md_content = (target / "CLAUDE.md").read_text(encoding="utf-8") @@ -504,7 +503,7 @@ def test_update_project_already_current_after_init(tmp_path): result = update_project(target) assert len(result["updated_files"]) == 0 - assert len(result["already_current"]) == 7 + assert len(result["already_current"]) == 6 def test_update_project_idempotent(tmp_path): @@ -583,8 +582,8 @@ def test_update_project_creates_missing_managed_dirs(tmp_path): assert (target / ".aipass" / "aipass_global_prompt.md").exists() assert (target / ".claude" / "settings.json").exists() - # Managed files in deleted dirs re-written (global_prompt, settings, prep, memo + 7 hooks) - assert len(result["updated_files"]) == 11 + # Managed files in deleted dirs re-written (global_prompt, settings, prep + 7 hooks) + assert len(result["updated_files"]) == 10 assert len(result["already_current"]) == 3 @@ -741,61 +740,15 @@ def test_update_project_adds_aipass_home_if_missing(tmp_path): # --------------------------------------------------------------------------- -def test_init_project_creates_memo_md(tmp_path): - """init_project creates .claude/commands/memo.md slash command.""" +def test_init_project_no_memo_md(tmp_path): + """init_project does NOT create memo.md — it belongs at provider level.""" target = tmp_path / "proj" target.mkdir() init_project(target, project_name="memo") memo_path = target / ".claude" / "commands" / "memo.md" - assert memo_path.exists() - content = memo_path.read_text(encoding="utf-8") - assert "# Memory Update" in content - assert ".trinity/passport.json" in content - assert ".trinity/local.json" in content - assert "STATUS.local.md" in content - - -def test_init_project_memo_not_overwritten_on_rerun(tmp_path): - """Re-running init skips existing memo.md.""" - target = tmp_path / "proj" - target.mkdir() - init_project(target, project_name="memo") - - memo_path = target / ".claude" / "commands" / "memo.md" - memo_path.write_text("# Custom memo\n", encoding="utf-8") - - init_project(target, project_name="memo") - - assert memo_path.read_text(encoding="utf-8") == "# Custom memo\n" - - -def test_update_project_refreshes_memo_md(tmp_path): - """update_project refreshes memo.md when content differs.""" - target = tmp_path / "proj" - target.mkdir() - init_project(target, project_name="umemo") - - memo_path = target / ".claude" / "commands" / "memo.md" - memo_path.write_text("# Stale content\n", encoding="utf-8") - - result = update_project(target) - - assert str(memo_path) in result["updated_files"] - assert "# Memory Update" in memo_path.read_text(encoding="utf-8") - - -def test_update_project_memo_already_current(tmp_path): - """update_project reports memo.md as already_current when unchanged.""" - target = tmp_path / "proj" - target.mkdir() - init_project(target, project_name="memocur") - - result = update_project(target) - - memo_path = target / ".claude" / "commands" / "memo.md" - assert str(memo_path) in result["already_current"] + assert not memo_path.exists() def test_init_project_ships_hooks(tmp_path): diff --git a/src/aipass/devpulse/.claude/commands/memo.md b/src/aipass/devpulse/.claude/commands/memo.md deleted file mode 100644 index 948ac39b..00000000 --- a/src/aipass/devpulse/.claude/commands/memo.md +++ /dev/null @@ -1,23 +0,0 @@ -# Memory Update - -Purpose: Update branch memory files after completing work this session. - -## Execution - -1. Read `.trinity/passport.json` first — re-absorb your identity, role, and principles before writing memories -2. Review what was done this session (context, recent changes, key decisions) -3. Update each file below as needed -4. Confirm completion — list files updated - -## Memory Roles - -Each memory file plays a distinct role. Update based on what actually changed this session. - -- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. Don't touch it just to touch it. -- **`.trinity/local.json`** — YOUR MEMORY. Session history and key_learnings. Add a session entry for significant work. Add key_learnings for facts you'd need next time. Trim oldest sessions if over 20. -- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points, flow states. Skip entirely if nothing new about the user this session. -- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate and drop quick notes in the Notepad section. - -## If Relevant - -- **README.md** — Does it reflect current state? Update if stale. diff --git a/src/aipass/devpulse/.claude/commands/prep.md b/src/aipass/devpulse/.claude/commands/prep.md deleted file mode 100644 index 4a56e0a3..00000000 --- a/src/aipass/devpulse/.claude/commands/prep.md +++ /dev/null @@ -1,57 +0,0 @@ -# Session Wrap-Up - -Purpose: Button up everything at the end of a session — or before a /compact. Memories, plans, git — all tidy. Works for both closing out a chat and preparing for compaction. - -**Workflow:** `/prep` → review output → close chat or `/compact` - -## Execution - -1. Read `.trinity/passport.json` first — re-absorb your identity before writing anything -2. Do ALL of the following, then confirm what was updated - -## 1. Memories - -Each memory file plays a distinct role. Update based on what actually changed this session. - -- **`.trinity/passport.json`** — IDENTITY. Who you are: role, capabilities, principles. Only update if identity genuinely evolved this session. -- **`.trinity/local.json`** — YOUR MEMORY. Add/update session entry with a summary of work done. Add key_learnings for anything learned. Trim oldest sessions if over 20. -- **`.trinity/observations.json`** — YOUR MEMORY OF THE USER. Collaboration insights, preferences, friction points. Skip if nothing new about the user this session. -- **`STATUS.local.md`** — PUBLIC STATUS BEACON. Current work, known issues, todos, notepad. Auto-synced to central STATUS.md on PR events — this is how other branches see you. Keep Current Work accurate. - -## 2. Active Plans - -- Check any DPLANs or FPLANs referenced in this session -- Update their execution logs, status, decision logs with current state -- If a plan was completed, note it (but don't close — the user does that) - -## 3. Git State - -- Run `git status` — report uncommitted changes -- If there's a logical commit waiting, suggest it (don't commit without asking) -- Note the current branch and any open PRs - -## 4. Inbox - -- Run `drone @ai_mail inbox 2>/dev/null` — report any unread emails -- Close any that were already processed but not formally closed - -## 5. Loose Ends - -- Flag anything in-flight: running background agents, dispatched branches waiting for replies, pending decisions -- If anything can't survive compaction (e.g., agent IDs needed for resume), write it to STATUS.local.md Notepad - -## Confirm - -List everything updated. Format: -``` -Prep complete: -- local.json: [what was added] -- observations.json: [updated / skipped] -- STATUS.local.md: [updated / skipped] -- Plans: [which ones updated] -- Git: [branch, uncommitted count, suggestion] -- Inbox: [count, action taken] -- Loose ends: [any flagged] - -Ready to close out or /compact. -``` diff --git a/src/aipass/devpulse/.claude/settings.json b/src/aipass/devpulse/.claude/settings.json index defb5cde..58f965ff 100644 --- a/src/aipass/devpulse/.claude/settings.json +++ b/src/aipass/devpulse/.claude/settings.json @@ -1,4 +1,42 @@ { + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/branch_prompt_loader.py" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/email_notification.py" + } + ] + }, + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/identity_injector.py" + } + ] + } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/pre_compact.py" + } + ] + } + ] + }, "env": { "AIPASS_HOME": "/home/patrick/Projects/AIPass" } diff --git a/src/aipass/trigger/apps/modules/medic.py b/src/aipass/trigger/apps/modules/medic.py index 4d972913..37e1967b 100644 --- a/src/aipass/trigger/apps/modules/medic.py +++ b/src/aipass/trigger/apps/modules/medic.py @@ -37,6 +37,56 @@ from aipass.trigger.apps.handlers.medic_state import ( ) SERVICE_NAME = "trigger-log-watcher.service" +_SERVICE_UNIT_PATH = Path.home() / ".config" / "systemd" / "user" / SERVICE_NAME +_TEMPLATE_PATH = Path(__file__).resolve().parent.parent.parent / "templates" / f"{SERVICE_NAME}.template" + + +def _get_aipass_home() -> Path: + """Resolve AIPASS_HOME from env var or git repo root.""" + import os + + env = os.environ.get("AIPASS_HOME") + if env: + return Path(env) + try: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + return Path(result.stdout.strip()) + except Exception as exc: + logger.warning("[MEDIC] git repo root detection failed: %s", exc) + return Path(__file__).resolve().parent.parent.parent.parent.parent + + +def _ensure_service_installed() -> bool: + """Install systemd unit from template if missing. Returns True if ready.""" + if _SERVICE_UNIT_PATH.exists(): + return True + + if not _TEMPLATE_PATH.exists(): + logger.warning("[MEDIC] Service template not found: %s", _TEMPLATE_PATH) + return False + + aipass_home = _get_aipass_home() + template = _TEMPLATE_PATH.read_text() + rendered = template.replace("{{AIPASS_HOME}}", str(aipass_home)) + + _SERVICE_UNIT_PATH.parent.mkdir(parents=True, exist_ok=True) + _SERVICE_UNIT_PATH.write_text(rendered) + logger.info("[MEDIC] Installed systemd unit to %s", _SERVICE_UNIT_PATH) + + _systemctl("daemon-reload") + subprocess.run( + ["systemctl", "--user", "enable", SERVICE_NAME], + capture_output=True, + text=True, + timeout=10, + ) + return True def print_introspection(): @@ -246,6 +296,7 @@ def _handle_on(console) -> None: logger.info("[MEDIC] Medic ENABLED - error dispatch active") if not _is_service_active(): + _ensure_service_installed() started = _systemctl("start") if started: logger.info("[MEDIC] Log watcher service started") diff --git a/src/aipass/trigger/templates/trigger-log-watcher.service.template b/src/aipass/trigger/templates/trigger-log-watcher.service.template new file mode 100644 index 00000000..62e9bbb6 --- /dev/null +++ b/src/aipass/trigger/templates/trigger-log-watcher.service.template @@ -0,0 +1,13 @@ +[Unit] +Description=AIPass Trigger Log Watcher +After=default.target + +[Service] +Type=simple +WorkingDirectory={{AIPASS_HOME}} +ExecStart={{AIPASS_HOME}}/.venv/bin/python3 -m aipass.trigger.apps.log_watcher_service +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=default.target