bark-agent-hook 0.1.1__py3-none-any.whl

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 @@
1
+ """bark_agent_hook package."""
@@ -0,0 +1,3 @@
1
+ from bark_agent_hook.cli import cmd
2
+
3
+ cmd()
bark_agent_hook/app.py ADDED
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.metadata
4
+ from typing import Any
5
+
6
+ import typer
7
+
8
+ helptext = """
9
+ Send Bark notifications from agent lifecycle hooks.
10
+
11
+ Install plugins:
12
+ bark-agent-hook install
13
+ bark-agent-hook install --agent codex
14
+ bark-agent-hook install --agent claude --agent openclaw
15
+
16
+ Uninstall plugins:
17
+ bark-agent-hook uninstall
18
+ bark-agent-hook uninstall --agent codex
19
+
20
+ Hook commands used by the plugins:
21
+ bark-agent-hook hook --runtime codex --event completion
22
+ bark-agent-hook hook --runtime claude --event approval_needed --summary-mode extract
23
+ bark-agent-hook hook --runtime openclaw --event completion --summary-mode extract
24
+
25
+ Direct plugin install commands:
26
+ codex plugin marketplace add qsoyq/bark-agent-hook
27
+ codex plugin add bark-agent-hook-codex@bark-agent-hook
28
+ claude plugin marketplace add qsoyq/bark-agent-hook
29
+ claude plugin install bark-agent-hook@bark-agent-hook --scope user
30
+ openclaw plugins install --link ./plugins/bark-agent-hook-openclaw
31
+ openclaw plugins enable bark-agent-hook-openclaw
32
+
33
+ Configuration:
34
+ BARK_DEVICE_KEY is required. Missing or empty means skip and exit 0.
35
+ BARK_GROUP is optional and overrides the computed Bark group.
36
+ BARK_SERVER defaults to https://api.day.app.
37
+ AGENT_BARK_NOTIFY_HOOK_URL optionally sets a Bark click URL template.
38
+ AGENT_BARK_NOTIFY_TITLE_TEMPLATE optionally sets a notification title template.
39
+ AGENT_BARK_NOTIFY_GROUP_MODE selects the computed group when BARK_GROUP is unset: agent, project, or project-branch.
40
+ AGENT_BARK_NOTIFY_AUDIT_LOG=1 enables local JSONL audit logging.
41
+ AGENT_BARK_NOTIFY_AUDIT_LOG_FILE overrides the audit log path.
42
+
43
+ Template variables:
44
+ AGENT_BARK_NOTIFY_TITLE_TEMPLATE supports:
45
+ {agent}, {event}, {project}, {branch}, {session}, {runtime}, {cwd_basename},
46
+ {LODY_ELECTRON_BOOTSTRAP}, {LODY_ELECTRON_SESSION_USER_ID}, {LODY_SESSION_ID},
47
+ {LODY_WORKSPACE_SESSION_ID}.
48
+ Example: AGENT_BARK_NOTIFY_TITLE_TEMPLATE='[{agent}][{event}][{LODY_SESSION_ID}]'
49
+ AGENT_BARK_NOTIFY_HOOK_URL supports:
50
+ {runtime}, {agent}, {event}, {project}, {branch}, {session}, {session_id},
51
+ {session_key}, {conversation_id}, {message_id}, {run_id}, {agent_id},
52
+ {workspace_dir}, {cwd_basename}, {LODY_ELECTRON_BOOTSTRAP},
53
+ {LODY_ELECTRON_SESSION_USER_ID}, {LODY_SESSION_ID}, {LODY_WORKSPACE_SESSION_ID}.
54
+ Example: AGENT_BARK_NOTIFY_HOOK_URL='https://lody.ai/users/{LODY_ELECTRON_SESSION_USER_ID}/sessions/{LODY_SESSION_ID}'
55
+ Hook URL variable values are percent-encoded; title variables are not URL-encoded.
56
+ Lody passthrough variables are read from LodySettings and limited to the four LODY_* keys above.
57
+
58
+ Audit log field sources:
59
+ Generated by the hook command:
60
+ 1. time: UTC timestamp when the audit record is created.
61
+ 2. status: final hook handling result, such as sent, skipped_duplicate, or skipped_missing_device_key.
62
+ CLI options:
63
+ 1. runtime: agent runtime selected by --runtime or auto detection.
64
+ 2. event: notification event selected by --event or inferred from the payload.
65
+ 3. summary_mode: notification body mode selected by --summary-mode.
66
+ Hook payload-derived values:
67
+ 1. hook_event_name: hook event name read from hook_event_name, event, event_name, or type.
68
+ 2. project: project name derived from payload metadata or the current working directory.
69
+ 3. session_id_hash: hashed session identifier; raw session IDs are not logged.
70
+ Built notification-derived values:
71
+ 1. dedupe_key_hash: hashed notification dedupe key.
72
+ 2. title: final notification title.
73
+ 3. body_len: length of the final notification body; notification bodies are not logged.
74
+ Lody environment passthrough:
75
+ 1. lody: non-empty whitelisted Lody environment values managed by LodySettings.
76
+ Keys: LODY_ELECTRON_BOOTSTRAP, LODY_ELECTRON_SESSION_USER_ID,
77
+ LODY_SESSION_ID, and LODY_WORKSPACE_SESSION_ID.
78
+ """
79
+
80
+
81
+ def version_callback(ctx: typer.Context, value: bool) -> None:
82
+ if not value:
83
+ return
84
+ name = ctx.find_root().info_name or "bark-agent-hook"
85
+ version = importlib.metadata.version("bark-agent-hook")
86
+ typer.echo(f"{name} cli version: {version}")
87
+ raise typer.Exit(0)
88
+
89
+
90
+ def default_invoke_without_command(
91
+ _: bool = typer.Option(False, "--version", "-v", "-V", callback=version_callback),
92
+ ) -> None:
93
+ return None
94
+
95
+
96
+ def make_typer(help: str, **kwargs: Any) -> typer.Typer:
97
+ app = typer.Typer(help=help, **kwargs)
98
+ app.callback(invoke_without_command=True)(default_invoke_without_command)
99
+ return app
100
+
101
+
102
+ cmd = make_typer(helptext)
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from bark_agent_hook.constants import (
9
+ AUDIT_LOG_ENV,
10
+ AUDIT_LOG_FILE_ENV,
11
+ BEARER_RE,
12
+ DEFAULT_AUDIT_LOG_PATH,
13
+ SENSITIVE_ASSIGNMENT_RE,
14
+ )
15
+ from bark_agent_hook.models import Notification, SummaryMode
16
+ from bark_agent_hook.runtime import project_name
17
+ from bark_agent_hook.settings import LodySettings
18
+ from bark_agent_hook.summary import _redact_url, _truncate_summary
19
+ from bark_agent_hook.utils import _env_value, _hash_value, _hook_event_name
20
+
21
+
22
+ def _session_id(payload: dict[str, Any]) -> str | None:
23
+ value = payload.get("session_id") or payload.get("sessionId") or payload.get("sessionKey") or payload.get("conversation_id") or payload.get("transcript_path")
24
+ return str(value) if value is not None else None
25
+
26
+
27
+ def _audit_enabled(env: dict[str, str]) -> bool:
28
+ return _env_value(env, AUDIT_LOG_ENV).lower() in {"1", "true", "yes", "on"}
29
+
30
+
31
+ def _audit_log_path(env: dict[str, str]) -> Path:
32
+ configured = _env_value(env, AUDIT_LOG_FILE_ENV)
33
+ if configured:
34
+ return Path(configured).expanduser()
35
+ return DEFAULT_AUDIT_LOG_PATH.expanduser()
36
+
37
+
38
+ def _safe_error_message(error: BaseException) -> str:
39
+ message = " ".join(str(error).split())
40
+ message = BEARER_RE.sub("Bearer [REDACTED]", message)
41
+ message = SENSITIVE_ASSIGNMENT_RE.sub(lambda m: f"{m.group(1)}=[REDACTED]", message)
42
+ message = _redact_url(message)
43
+ return _truncate_summary(message, 200)
44
+
45
+
46
+ def _write_audit_record(env: dict[str, str], record: dict[str, Any]) -> None:
47
+ if not _audit_enabled(env):
48
+ return
49
+ try:
50
+ path = _audit_log_path(env)
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ with path.open("a", encoding="utf-8") as f:
53
+ f.write(json.dumps(record, ensure_ascii=False, sort_keys=True))
54
+ f.write("\n")
55
+ except OSError:
56
+ return
57
+
58
+
59
+ def _new_audit_record(
60
+ *,
61
+ runtime: str,
62
+ event: str | None,
63
+ payload: dict[str, Any],
64
+ summary_mode: SummaryMode,
65
+ lody_settings: LodySettings,
66
+ cwd: Path | None = None,
67
+ ) -> dict[str, Any]:
68
+ record: dict[str, Any] = {
69
+ "time": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
70
+ "runtime": runtime,
71
+ "event": event,
72
+ "hook_event_name": _hook_event_name(payload),
73
+ "status": None,
74
+ "project": project_name(payload, cwd),
75
+ "session_id_hash": _hash_value(_session_id(payload)),
76
+ "dedupe_key_hash": None,
77
+ "summary_mode": summary_mode,
78
+ "title": None,
79
+ "body_len": None,
80
+ }
81
+ lody_values = lody_settings.audit_values()
82
+ if lody_values:
83
+ record["lody"] = lody_values
84
+ return record
85
+
86
+
87
+ def _finish_audit_record(
88
+ env: dict[str, str],
89
+ record: dict[str, Any],
90
+ *,
91
+ status: str,
92
+ notification: Notification | None = None,
93
+ error: BaseException | None = None,
94
+ ) -> None:
95
+ record["status"] = status
96
+ if notification is not None:
97
+ record["dedupe_key_hash"] = _hash_value(notification.dedupe_key)
98
+ record["title"] = notification.title
99
+ record["body_len"] = len(notification.body)
100
+ if error is not None:
101
+ record["error_class"] = error.__class__.__name__
102
+ record["error_message"] = _safe_error_message(error)
103
+ _write_audit_record(env, record)
bark_agent_hook/cli.py ADDED
@@ -0,0 +1,3 @@
1
+ from bark_agent_hook.hook import cmd
2
+
3
+ __all__ = ["cmd"]
@@ -0,0 +1,138 @@
1
+ import json
2
+ import os
3
+
4
+ import httpx
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from bark_agent_hook.app import cmd
9
+ from bark_agent_hook.audit import _finish_audit_record, _new_audit_record
10
+ from bark_agent_hook.constants import DEFAULT_SUMMARY_MAX_CHARS
11
+ from bark_agent_hook.installer import _install_for_available_agents, _uninstall_for_available_agents
12
+ from bark_agent_hook.models import AgentOption, Event, GroupModeOption, Runtime, SummaryMode
13
+ from bark_agent_hook.notification import (
14
+ already_sent,
15
+ build_notification,
16
+ resolve_group_mode,
17
+ send_bark,
18
+ skip_notification_reason,
19
+ )
20
+ from bark_agent_hook.output import (
21
+ _found_cli_count,
22
+ _print_install_results,
23
+ _print_uninstall_results,
24
+ _succeeded,
25
+ )
26
+ from bark_agent_hook.runtime import _read_stdin, detect_event, detect_runtime, parse_hook_payload
27
+ from bark_agent_hook.settings import LodySettings
28
+ from bark_agent_hook.summary import extract_summary
29
+
30
+
31
+ @cmd.command()
32
+ def install(
33
+ agents: list[AgentOption] | None = typer.Option(None, "--agent", help="Agent plugin to install. Repeat for multiple agents. Defaults to all supported agents."),
34
+ ) -> None:
35
+ """Install bark-agent-hook plugins for locally available agents.
36
+
37
+ This command checks for codex, claude, and openclaw CLIs in PATH unless
38
+ one or more --agent options are passed. Missing CLIs are skipped.
39
+
40
+ Installed plugins:
41
+ Codex: bark-agent-hook-codex@bark-agent-hook
42
+ Claude Code: bark-agent-hook@bark-agent-hook --scope user
43
+ OpenClaw: local linked plugin from plugins/bark-agent-hook-openclaw
44
+ """
45
+ results = _install_for_available_agents(agents)
46
+ _print_install_results(results, Console(highlight=False, width=120))
47
+ if _found_cli_count(results) > 0 and _succeeded(results) == 0:
48
+ raise typer.Exit(1)
49
+
50
+
51
+ @cmd.command()
52
+ def uninstall(
53
+ agents: list[AgentOption] | None = typer.Option(None, "--agent", help="Agent plugin to uninstall. Repeat for multiple agents. Defaults to all supported agents."),
54
+ ) -> None:
55
+ """Uninstall bark-agent-hook plugins for locally available agents."""
56
+ results = _uninstall_for_available_agents(agents)
57
+ _print_uninstall_results(results, Console(highlight=False, width=120))
58
+ if _found_cli_count(results) > 0 and _succeeded(results) == 0:
59
+ raise typer.Exit(1)
60
+
61
+
62
+ @cmd.command()
63
+ def hook(
64
+ runtime: Runtime = typer.Option("auto", "--runtime", help="Hook runtime: codex, claude, openclaw, or auto."),
65
+ event: Event = typer.Option("auto", "--event", help="Notification event override."),
66
+ message: str | None = typer.Option(None, "--message", help="Override short notification body."),
67
+ group_mode: GroupModeOption | None = typer.Option(None, "--group-mode", help="Bark group mode: agent, project, or project-branch."),
68
+ summary_mode: SummaryMode = typer.Option("fixed", "--summary-mode", help="Notification summary mode: fixed or extract."),
69
+ summary_max_chars: int = typer.Option(DEFAULT_SUMMARY_MAX_CHARS, "--summary-max-chars", min=1, help="Maximum extractive summary length."),
70
+ dry_run: bool = typer.Option(False, "--dry-run", help="Print notification summary without sending Bark request."),
71
+ no_dedupe: bool = typer.Option(False, "--no-dedupe", help="Disable duplicate suppression."),
72
+ ) -> None:
73
+ """Read hook JSON from stdin and send a best-effort Bark notification."""
74
+ env = dict(os.environ)
75
+ payload = parse_hook_payload(_read_stdin())
76
+ lody_settings = LodySettings()
77
+ resolved_runtime = detect_runtime(runtime, env, payload, lody_settings)
78
+ resolved_event = detect_event(event, payload)
79
+ resolved_group_mode = resolve_group_mode(group_mode, env)
80
+ audit_record = _new_audit_record(runtime=resolved_runtime, event=resolved_event, payload=payload, summary_mode=summary_mode, lody_settings=lody_settings)
81
+ try:
82
+ if resolved_event is None:
83
+ _finish_audit_record(env, audit_record, status="skipped_unsupported_event")
84
+ if dry_run:
85
+ typer.echo("skip: unsupported hook event")
86
+ return
87
+
88
+ resolved_message = message
89
+ if resolved_message is None and summary_mode == "extract":
90
+ resolved_message = extract_summary(resolved_runtime, resolved_event, payload, summary_max_chars)
91
+
92
+ skip_reason = skip_notification_reason(resolved_runtime, resolved_event, payload, resolved_message)
93
+ if skip_reason is not None:
94
+ _finish_audit_record(env, audit_record, status=skip_reason)
95
+ if dry_run:
96
+ typer.echo("skip: OpenClaw event has no deliverable reply")
97
+ return
98
+
99
+ notification = build_notification(
100
+ runtime=resolved_runtime,
101
+ event=resolved_event,
102
+ message=resolved_message,
103
+ env=env,
104
+ payload=payload,
105
+ lody_settings=lody_settings,
106
+ group_mode=resolved_group_mode,
107
+ )
108
+ if notification is None:
109
+ _finish_audit_record(env, audit_record, status="skipped_missing_device_key")
110
+ if dry_run:
111
+ typer.echo("skip: BARK_DEVICE_KEY is missing")
112
+ return
113
+
114
+ if not no_dedupe and already_sent(notification.dedupe_key, env):
115
+ _finish_audit_record(env, audit_record, status="skipped_duplicate", notification=notification)
116
+ if dry_run:
117
+ typer.echo("skip: duplicate notification")
118
+ return
119
+
120
+ if dry_run:
121
+ _finish_audit_record(env, audit_record, status="sent", notification=notification)
122
+ output = {"title": notification.title, "body": notification.body, "icon": notification.icon_url, "group": notification.group, "url": notification.bark_url}
123
+ if notification.click_url:
124
+ output["click_url"] = notification.click_url
125
+ typer.echo(json.dumps(output, ensure_ascii=False))
126
+ return
127
+
128
+ try:
129
+ send_bark(notification)
130
+ except httpx.HTTPError as e:
131
+ _finish_audit_record(env, audit_record, status="bark_http_error", notification=notification, error=e)
132
+ typer.echo(f"Bark notification failed: {e}", err=True)
133
+ return
134
+ _finish_audit_record(env, audit_record, status="sent", notification=notification)
135
+ except Exception as e:
136
+ _finish_audit_record(env, audit_record, status="hook_exception", error=e)
137
+ typer.echo(f"Bark hook failed: {e}", err=True)
138
+ return
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from bark_agent_hook.models import GroupMode
7
+
8
+ CODEX_ICON_URL = "https://raw.githubusercontent.com/lobehub/lobe-icons/refs/heads/master/packages/static-png/light/codex-color.png"
9
+
10
+
11
+ CLAUDE_CODE_ICON_URL = "https://raw.githubusercontent.com/lobehub/lobe-icons/refs/heads/master/packages/static-png/light/claudecode-color.png"
12
+
13
+
14
+ OPENCLAW_ICON_URL = "https://openclaw.ai/apple-touch-icon.png"
15
+
16
+
17
+ LODY_ICON_URL = "https://lody.ai/favicon.ico"
18
+
19
+
20
+ DEFAULT_MESSAGES: dict[str, str] = {
21
+ "completion": "任务已完成",
22
+ "approval_needed": "需要你审批当前操作",
23
+ "failed": "本轮因错误停止",
24
+ }
25
+
26
+
27
+ EVENT_LABELS: dict[str, str] = {
28
+ "completion": "Done",
29
+ "approval_needed": "Approval",
30
+ "failed": "Failed",
31
+ }
32
+
33
+
34
+ MAX_MESSAGE_LENGTH = 80
35
+
36
+
37
+ DEFAULT_SUMMARY_MAX_CHARS = 120
38
+
39
+
40
+ MAX_TRANSCRIPT_BYTES = 1024 * 1024
41
+
42
+
43
+ DEDUP_TTL_SECONDS = 60 * 60
44
+
45
+
46
+ HOOK_URL_TEMPLATE_ENV = "AGENT_BARK_NOTIFY_HOOK_URL"
47
+
48
+
49
+ TITLE_TEMPLATE_ENV = "AGENT_BARK_NOTIFY_TITLE_TEMPLATE"
50
+
51
+
52
+ DEFAULT_TITLE_TEMPLATE = "[{agent}][{event}][{project}][{branch}][{session}]"
53
+
54
+
55
+ GROUP_MODE_ENV = "AGENT_BARK_NOTIFY_GROUP_MODE"
56
+
57
+
58
+ GROUP_MODE_CHOICES: tuple[GroupMode, ...] = ("agent", "project", "project-branch")
59
+
60
+
61
+ AUDIT_LOG_ENV = "AGENT_BARK_NOTIFY_AUDIT_LOG"
62
+
63
+
64
+ AUDIT_LOG_FILE_ENV = "AGENT_BARK_NOTIFY_AUDIT_LOG_FILE"
65
+
66
+
67
+ DEFAULT_AUDIT_LOG_PATH = Path("~/.bark-agent-hook/bark-agent-hook.log")
68
+
69
+
70
+ SENSITIVE_KEY_RE = re.compile(r"(?i)\b(authorization|cookie|set-cookie|x-api-key|api[_-]?key|token|secret|password|passwd|bearer)\b")
71
+
72
+
73
+ SENSITIVE_ASSIGNMENT_RE = re.compile(r"(?i)\b([a-z0-9_.-]*(?:token|secret|password|passwd|cookie|authorization|api[_-]?key)[a-z0-9_.-]*)\s*[:=]\s*('[^']*'|\"[^\"]*\"|[^\s,;]+)")
74
+
75
+
76
+ BEARER_RE = re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/=-]+")
77
+
78
+
79
+ FENCED_CODE_RE = re.compile(r"```.*?```", re.DOTALL)
80
+
81
+
82
+ SHELL_PREFIX_RE = re.compile(r"^\s*(?:bash|zsh|sh|fish|python|python3|node|npm|pnpm|yarn|curl|ssh|scp|rsync)\b", re.IGNORECASE)
83
+
84
+
85
+ OPENCLAW_CONVERSATION_ACCESS_PATCH = '{"plugins":{"entries":{"bark-agent-hook-openclaw":{"hooks":{"allowConversationAccess":true}}}}}'
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+
6
+ from bark_agent_hook.app import cmd
7
+ from bark_agent_hook.commands import hook, install, uninstall
8
+ from bark_agent_hook.constants import (
9
+ CLAUDE_CODE_ICON_URL,
10
+ CODEX_ICON_URL,
11
+ LODY_ICON_URL,
12
+ OPENCLAW_ICON_URL,
13
+ )
14
+ from bark_agent_hook.installer import _openclaw_plugin_dir
15
+
16
+ __all__ = (
17
+ "CLAUDE_CODE_ICON_URL",
18
+ "CODEX_ICON_URL",
19
+ "LODY_ICON_URL",
20
+ "OPENCLAW_ICON_URL",
21
+ "_openclaw_plugin_dir",
22
+ "cmd",
23
+ "hook",
24
+ "install",
25
+ "shutil",
26
+ "subprocess",
27
+ "uninstall",
28
+ )
29
+
30
+ if __name__ == "__main__":
31
+ cmd()
@@ -0,0 +1,3 @@
1
+ from bark_agent_hook.hook import install, uninstall
2
+
3
+ __all__ = ["install", "uninstall"]