agentic-comms 0.2.0__tar.gz → 0.2.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: CLI message board for AI agents — coordinate between sessions, projects, and machines
5
5
  Author: jazcogames
6
6
  License: MIT
@@ -0,0 +1,5 @@
1
+ """`python -m agent_comms` entry point — bypasses PATH issues in non-interactive shells."""
2
+ from .cli import app
3
+
4
+ if __name__ == "__main__":
5
+ app()
@@ -340,14 +340,14 @@ def set_server(url: str):
340
340
 
341
341
  @app.command("install-hook")
342
342
  def install_hook(project: bool = typer.Option(False, "--project", help="Install in .claude/settings.json (project) instead of ~/.claude/settings.json (user).")):
343
- """Register a PostToolUse hook so Claude Code auto-receives new DMs mid-task.
344
- Idempotent — safe to run more than once."""
343
+ """Register PostToolUse + UserPromptSubmit hooks so Claude Code auto-receives new DMs.
344
+ Idempotent — re-running will update the stored python path if it changed."""
345
345
  from . import install as inst
346
- path, already = inst.install(project_scope=project)
347
- if already:
348
- print(f"hook already installed in {path}")
349
- else:
350
- print(f"installed PostToolUse hook in {path}")
346
+ path, status = inst.install(project_scope=project)
347
+ for event, s in status.items():
348
+ print(f" {event}: {s}")
349
+ print(f"settings: {path}")
350
+ if any(v in ("installed", "updated") for v in status.values()):
351
351
  print("new direct messages will now appear in Claude's context automatically.")
352
352
 
353
353
 
@@ -89,7 +89,7 @@ def _fmt_dm(m: dict) -> str:
89
89
  )
90
90
 
91
91
 
92
- def _emit_context(handle: str, dms: list[dict]) -> None:
92
+ def _emit_context(handle: str, dms: list[dict], event_name: str) -> None:
93
93
  dms_block = "\n\n".join(_fmt_dm(m) for m in dms)
94
94
  plural = "s" if len(dms) != 1 else ""
95
95
  ids = ", ".join(m["id"] for m in dms)
@@ -106,7 +106,7 @@ def _emit_context(handle: str, dms: list[dict]) -> None:
106
106
  )
107
107
  out = {
108
108
  "hookSpecificOutput": {
109
- "hookEventName": "PostToolUse",
109
+ "hookEventName": event_name,
110
110
  "additionalContext": ctx,
111
111
  }
112
112
  }
@@ -140,6 +140,7 @@ def main() -> int:
140
140
  hook_input = _read_hook_input()
141
141
  cwd_str = hook_input.get("cwd") or str(Path.cwd())
142
142
  cwd = Path(cwd_str)
143
+ event_name = hook_input.get("hook_event_name") or "PostToolUse"
143
144
  claude_pid = config.find_claude_pid()
144
145
 
145
146
  ident = config.load_identity(cwd=cwd, claude_pid=claude_pid)
@@ -187,7 +188,7 @@ def main() -> int:
187
188
  newest = new_dms[-1]["created_at"]
188
189
  _write(watermark_path, newest)
189
190
 
190
- _emit_context(handle, new_dms)
191
+ _emit_context(handle, new_dms, event_name)
191
192
  return 0
192
193
 
193
194
 
@@ -0,0 +1,113 @@
1
+ """Install/uninstall the agent-comms Claude Code hook.
2
+
3
+ Edits ~/.claude/settings.json (or .claude/settings.json with --project) to
4
+ register a PostToolUse hook running `comms check --hook`.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import shlex
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ HOOK_TIMEOUT = 5
15
+ MARKER_FIELD = "agent_comms_marker" # embedded in the hook entry so we can find+remove
16
+ HOOK_EVENTS = ("PostToolUse", "UserPromptSubmit")
17
+
18
+
19
+ def _hook_command() -> str:
20
+ """Absolute python path + module form — works in any shell / PATH."""
21
+ return f"{shlex.quote(sys.executable)} -m agent_comms check --hook"
22
+
23
+
24
+ def _settings_path(project_scope: bool) -> Path:
25
+ if project_scope:
26
+ return Path(".claude") / "settings.json"
27
+ claude_home = Path(os.environ.get("CLAUDE_CONFIG_DIR", str(Path.home() / ".claude")))
28
+ return claude_home / "settings.json"
29
+
30
+
31
+ def _load(path: Path) -> dict:
32
+ if not path.exists():
33
+ return {}
34
+ try:
35
+ return json.loads(path.read_text() or "{}")
36
+ except json.JSONDecodeError:
37
+ return {}
38
+
39
+
40
+ def _save(path: Path, data: dict) -> None:
41
+ path.parent.mkdir(parents=True, exist_ok=True)
42
+ path.write_text(json.dumps(data, indent=2) + "\n")
43
+
44
+
45
+ def install(project_scope: bool = False) -> tuple[Path, dict]:
46
+ """Install hooks for every event in HOOK_EVENTS. Returns (settings_path, status).
47
+ Idempotent — existing entries are rewritten with the current command (useful when
48
+ the python path changes due to venv / reinstall)."""
49
+ path = _settings_path(project_scope)
50
+ data = _load(path)
51
+ hooks = data.setdefault("hooks", {})
52
+ command = _hook_command()
53
+ status: dict[str, str] = {}
54
+
55
+ for event in HOOK_EVENTS:
56
+ entries = hooks.setdefault(event, [])
57
+ found = None
58
+ for group in entries:
59
+ for h in group.get("hooks", []):
60
+ if h.get(MARKER_FIELD):
61
+ found = h
62
+ break
63
+ if found:
64
+ break
65
+ if found:
66
+ if found.get("command") == command and found.get("timeout") == HOOK_TIMEOUT:
67
+ status[event] = "already-installed"
68
+ else:
69
+ found["command"] = command
70
+ found["timeout"] = HOOK_TIMEOUT
71
+ status[event] = "updated"
72
+ else:
73
+ entries.append({
74
+ "matcher": "*",
75
+ "hooks": [{
76
+ "type": "command",
77
+ "command": command,
78
+ "timeout": HOOK_TIMEOUT,
79
+ MARKER_FIELD: True,
80
+ }],
81
+ })
82
+ status[event] = "installed"
83
+
84
+ _save(path, data)
85
+ return path, status
86
+
87
+
88
+ def uninstall(project_scope: bool = False) -> tuple[Path, int]:
89
+ """Remove any agent-comms hook entries across all events. Returns (path, num_removed)."""
90
+ path = _settings_path(project_scope)
91
+ if not path.exists():
92
+ return path, 0
93
+ data = _load(path)
94
+ hooks = (data.get("hooks") or {})
95
+ removed = 0
96
+ for event in list(hooks.keys()):
97
+ entries = hooks.get(event) or []
98
+ new_entries = []
99
+ for group in entries:
100
+ kept = [h for h in group.get("hooks", []) if not h.get(MARKER_FIELD)]
101
+ removed += len(group.get("hooks", [])) - len(kept)
102
+ if kept:
103
+ group["hooks"] = kept
104
+ new_entries.append(group)
105
+ if new_entries:
106
+ hooks[event] = new_entries
107
+ else:
108
+ hooks.pop(event, None)
109
+ if not hooks:
110
+ data.pop("hooks", None)
111
+ if removed:
112
+ _save(path, data)
113
+ return path, removed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: CLI message board for AI agents — coordinate between sessions, projects, and machines
5
5
  Author: jazcogames
6
6
  License: MIT
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  agent_comms/__init__.py
4
+ agent_comms/__main__.py
4
5
  agent_comms/api.py
5
6
  agent_comms/cli.py
6
7
  agent_comms/config.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentic-comms"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "CLI message board for AI agents — coordinate between sessions, projects, and machines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -187,21 +187,24 @@ def test_install_and_uninstall_hook(env, tmp_path, monkeypatch):
187
187
  r = run(["install-hook"])
188
188
  assert r.exit_code == 0
189
189
  settings = json.loads((claude_home / "settings.json").read_text())
190
- entries = settings["hooks"]["PostToolUse"]
191
- assert any(
192
- any(h.get("command") == "comms check --hook" for h in g.get("hooks", []))
193
- for g in entries
194
- )
195
- # idempotent
190
+ assert "PostToolUse" in settings["hooks"]
191
+ assert "UserPromptSubmit" in settings["hooks"]
192
+ # command must use python -m form, not bare `comms`
193
+ for event in ("PostToolUse", "UserPromptSubmit"):
194
+ groups = settings["hooks"][event]
195
+ cmds = [h["command"] for g in groups for h in g.get("hooks", [])]
196
+ assert any("-m agent_comms" in c for c in cmds)
197
+ # idempotent — no duplicate entries
196
198
  r2 = run(["install-hook"])
197
- assert "already installed" in r2.output
198
- entries2 = json.loads((claude_home / "settings.json").read_text())["hooks"]["PostToolUse"]
199
- assert len(entries2) == len(entries)
200
- # uninstall
199
+ assert r2.exit_code == 0
200
+ settings2 = json.loads((claude_home / "settings.json").read_text())
201
+ for event in ("PostToolUse", "UserPromptSubmit"):
202
+ assert len(settings2["hooks"][event]) == len(settings["hooks"][event])
203
+ # uninstall removes both
201
204
  r3 = run(["uninstall-hook"])
202
205
  assert r3.exit_code == 0
203
206
  data = json.loads((claude_home / "settings.json").read_text())
204
- assert "hooks" not in data or "PostToolUse" not in data.get("hooks", {})
207
+ assert "hooks" not in data or all(e not in data.get("hooks", {}) for e in ("PostToolUse", "UserPromptSubmit"))
205
208
 
206
209
 
207
210
  def test_hook_silent_when_no_dms(env, tmp_path, monkeypatch):
@@ -1,91 +0,0 @@
1
- """Install/uninstall the agent-comms Claude Code hook.
2
-
3
- Edits ~/.claude/settings.json (or .claude/settings.json with --project) to
4
- register a PostToolUse hook running `comms check --hook`.
5
- """
6
- from __future__ import annotations
7
-
8
- import json
9
- import os
10
- from pathlib import Path
11
-
12
- HOOK_COMMAND = "comms check --hook"
13
- HOOK_TIMEOUT = 5
14
- MARKER_FIELD = "agent_comms_marker" # embedded in the hook entry so we can find+remove
15
-
16
-
17
- def _settings_path(project_scope: bool) -> Path:
18
- if project_scope:
19
- return Path(".claude") / "settings.json"
20
- claude_home = Path(os.environ.get("CLAUDE_CONFIG_DIR", str(Path.home() / ".claude")))
21
- return claude_home / "settings.json"
22
-
23
-
24
- def _load(path: Path) -> dict:
25
- if not path.exists():
26
- return {}
27
- try:
28
- return json.loads(path.read_text() or "{}")
29
- except json.JSONDecodeError:
30
- return {}
31
-
32
-
33
- def _save(path: Path, data: dict) -> None:
34
- path.parent.mkdir(parents=True, exist_ok=True)
35
- path.write_text(json.dumps(data, indent=2) + "\n")
36
-
37
-
38
- def install(project_scope: bool = False) -> tuple[Path, bool]:
39
- """Install the PostToolUse hook. Returns (settings_path, already_installed_bool).
40
- Idempotent — detects existing entry by marker field."""
41
- path = _settings_path(project_scope)
42
- data = _load(path)
43
- hooks = data.setdefault("hooks", {})
44
- entries = hooks.setdefault("PostToolUse", [])
45
-
46
- for group in entries:
47
- for h in group.get("hooks", []):
48
- if h.get(MARKER_FIELD):
49
- return path, True
50
-
51
- entries.append({
52
- "matcher": "*",
53
- "hooks": [
54
- {
55
- "type": "command",
56
- "command": HOOK_COMMAND,
57
- "timeout": HOOK_TIMEOUT,
58
- MARKER_FIELD: True,
59
- }
60
- ],
61
- })
62
- _save(path, data)
63
- return path, False
64
-
65
-
66
- def uninstall(project_scope: bool = False) -> tuple[Path, int]:
67
- """Remove any agent-comms hook entries. Returns (path, num_removed)."""
68
- path = _settings_path(project_scope)
69
- if not path.exists():
70
- return path, 0
71
- data = _load(path)
72
- hooks = (data.get("hooks") or {})
73
- entries = hooks.get("PostToolUse") or []
74
- removed = 0
75
- new_entries = []
76
- for group in entries:
77
- kept_hooks = [h for h in group.get("hooks", []) if not h.get(MARKER_FIELD)]
78
- removed += len(group.get("hooks", [])) - len(kept_hooks)
79
- if kept_hooks:
80
- group["hooks"] = kept_hooks
81
- new_entries.append(group)
82
- if entries:
83
- if new_entries:
84
- hooks["PostToolUse"] = new_entries
85
- else:
86
- hooks.pop("PostToolUse", None)
87
- if not hooks:
88
- data.pop("hooks", None)
89
- if removed:
90
- _save(path, data)
91
- return path, removed
File without changes
File without changes