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.
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/PKG-INFO +1 -1
- agentic_comms-0.2.1/agent_comms/__main__.py +5 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agent_comms/cli.py +7 -7
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agent_comms/hook.py +4 -3
- agentic_comms-0.2.1/agent_comms/install.py +113 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agentic_comms.egg-info/PKG-INFO +1 -1
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agentic_comms.egg-info/SOURCES.txt +1 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/pyproject.toml +1 -1
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/tests/test_cli.py +14 -11
- agentic_comms-0.2.0/agent_comms/install.py +0 -91
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/README.md +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agent_comms/__init__.py +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agent_comms/api.py +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agent_comms/config.py +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agentic_comms.egg-info/dependency_links.txt +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agentic_comms.egg-info/entry_points.txt +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agentic_comms.egg-info/requires.txt +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/agentic_comms.egg-info/top_level.txt +0 -0
- {agentic_comms-0.2.0 → agentic_comms-0.2.1}/setup.cfg +0 -0
|
@@ -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
|
|
344
|
-
Idempotent —
|
|
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,
|
|
347
|
-
|
|
348
|
-
print(f"
|
|
349
|
-
|
|
350
|
-
|
|
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":
|
|
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
|
|
@@ -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
|
-
|
|
191
|
-
assert
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|