agentic-comms 0.7.2__tar.gz → 0.8.0__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.7.2 → agentic_comms-0.8.0}/PKG-INFO +1 -1
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agent_comms/cli.py +84 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agent_comms/config.py +19 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agentic_comms.egg-info/PKG-INFO +1 -1
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/pyproject.toml +1 -1
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/tests/test_cli.py +44 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/README.md +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agent_comms/__init__.py +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agent_comms/__main__.py +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agent_comms/api.py +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agent_comms/hook.py +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agent_comms/install.py +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agentic_comms.egg-info/SOURCES.txt +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agentic_comms.egg-info/dependency_links.txt +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agentic_comms.egg-info/entry_points.txt +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agentic_comms.egg-info/requires.txt +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/agentic_comms.egg-info/top_level.txt +0 -0
- {agentic_comms-0.7.2 → agentic_comms-0.8.0}/setup.cfg +0 -0
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
import json
|
|
17
17
|
import os
|
|
18
18
|
import sys
|
|
19
|
+
from pathlib import Path
|
|
19
20
|
from typing import Optional
|
|
20
21
|
|
|
21
22
|
import typer
|
|
@@ -393,6 +394,89 @@ def set_server(url: str):
|
|
|
393
394
|
print(f"saved {url} to {config.SERVER_FILE}")
|
|
394
395
|
|
|
395
396
|
|
|
397
|
+
@app.command()
|
|
398
|
+
def statusline():
|
|
399
|
+
"""Claude Code statusLine entry point. Reads the statusLine JSON payload from stdin,
|
|
400
|
+
resolves the identity by session_id, fetches the live mission from the server, and
|
|
401
|
+
prints a colored badge: [Name] mission. Always exits 0; failures fall back to a plain
|
|
402
|
+
handle string so the status bar never goes blank."""
|
|
403
|
+
import time as _time
|
|
404
|
+
from pathlib import Path as _P
|
|
405
|
+
try:
|
|
406
|
+
payload = json.load(sys.stdin) if not sys.stdin.isatty() else {}
|
|
407
|
+
except Exception:
|
|
408
|
+
payload = {}
|
|
409
|
+
session_id = payload.get("session_id")
|
|
410
|
+
cwd_str = (payload.get("workspace") or {}).get("current_dir") or payload.get("cwd")
|
|
411
|
+
cwd = _P(cwd_str) if cwd_str else None
|
|
412
|
+
|
|
413
|
+
ident = config.find_identity_by_session_id(session_id) or config.load_identity(cwd=cwd)
|
|
414
|
+
if not ident:
|
|
415
|
+
sys.exit(0)
|
|
416
|
+
|
|
417
|
+
# 3-second cache of the live identity record (mission_title may have changed server-side)
|
|
418
|
+
from agent_comms.hook import _cache_dir, _hash
|
|
419
|
+
cache_path = _cache_dir() / f"statusline-{_hash(ident.handle)}.json"
|
|
420
|
+
record = None
|
|
421
|
+
try:
|
|
422
|
+
if cache_path.exists() and (_time.time() - cache_path.stat().st_mtime) < 3:
|
|
423
|
+
record = json.loads(cache_path.read_text())
|
|
424
|
+
except Exception:
|
|
425
|
+
record = None
|
|
426
|
+
if record is None:
|
|
427
|
+
try:
|
|
428
|
+
c = Client()
|
|
429
|
+
c._h.timeout = 2.0
|
|
430
|
+
record = c.get_identity(ident.handle)
|
|
431
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
432
|
+
cache_path.write_text(json.dumps(record))
|
|
433
|
+
except Exception:
|
|
434
|
+
record = {"display_name": ident.display_name, "color_hex": ident.color_hex,
|
|
435
|
+
"mission_title": ident.mission_title} if hasattr(ident, "display_name") else {}
|
|
436
|
+
|
|
437
|
+
name = (record or {}).get("display_name") or ident.handle.split("-")[0]
|
|
438
|
+
color = (record or {}).get("color_hex") or "#ffffff"
|
|
439
|
+
mission = (record or {}).get("mission_title") or ""
|
|
440
|
+
h = color.lstrip("#")
|
|
441
|
+
try:
|
|
442
|
+
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
443
|
+
except Exception:
|
|
444
|
+
r = g = b = 255
|
|
445
|
+
badge = f"\033[1;38;2;{r};{g};{b}m[{name}]\033[0m"
|
|
446
|
+
if mission:
|
|
447
|
+
if len(mission) > 80:
|
|
448
|
+
mission = mission[:77] + "..."
|
|
449
|
+
print(f"{badge} {mission}")
|
|
450
|
+
else:
|
|
451
|
+
print(badge)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@app.command("install-statusline")
|
|
455
|
+
def install_statusline(
|
|
456
|
+
project: bool = typer.Option(False, "--project", help="Install in .claude/settings.json (project) instead of ~/.claude/settings.json (user."),
|
|
457
|
+
):
|
|
458
|
+
"""Register a Claude Code statusLine that shows this session's agent badge + live mission.
|
|
459
|
+
Idempotent — re-running updates the stored python path."""
|
|
460
|
+
settings_path = (Path(".claude") / "settings.json") if project else \
|
|
461
|
+
(Path(os.environ.get("CLAUDE_CONFIG_DIR", str(Path.home() / ".claude"))) / "settings.json")
|
|
462
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
463
|
+
try:
|
|
464
|
+
data = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
465
|
+
except Exception:
|
|
466
|
+
data = {}
|
|
467
|
+
import shlex as _shlex
|
|
468
|
+
command = f"{_shlex.quote(sys.executable)} -m agent_comms statusline"
|
|
469
|
+
existing = data.get("statusLine") or {}
|
|
470
|
+
data["statusLine"] = {
|
|
471
|
+
"type": "command",
|
|
472
|
+
"command": command,
|
|
473
|
+
"padding": existing.get("padding", 1),
|
|
474
|
+
}
|
|
475
|
+
settings_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
476
|
+
print(f"installed statusLine in {settings_path}")
|
|
477
|
+
print("Reopen Claude Code (or wait for next assistant message) to see the badge.")
|
|
478
|
+
|
|
479
|
+
|
|
396
480
|
@app.command("install-hook")
|
|
397
481
|
def install_hook(
|
|
398
482
|
project: bool = typer.Option(False, "--project", help="Install in .claude/settings.json (project) instead of ~/.claude/settings.json (user)."),
|
|
@@ -147,6 +147,25 @@ def load_identity(cwd: Path | None = None, claude_pid: int | None = None) -> Loc
|
|
|
147
147
|
return None
|
|
148
148
|
|
|
149
149
|
|
|
150
|
+
def find_identity_by_session_id(session_id: str | None) -> "LocalIdentity | None":
|
|
151
|
+
"""Scan all stored identity files for one whose stored session_id matches.
|
|
152
|
+
Used by the statusline subcommand to resolve which agent the current Claude
|
|
153
|
+
session belongs to, regardless of cwd."""
|
|
154
|
+
if not session_id:
|
|
155
|
+
return None
|
|
156
|
+
if not SESSIONS_DIR.exists():
|
|
157
|
+
return None
|
|
158
|
+
for p in SESSIONS_DIR.glob("*.json"):
|
|
159
|
+
try:
|
|
160
|
+
data = json.loads(p.read_text())
|
|
161
|
+
except Exception:
|
|
162
|
+
continue
|
|
163
|
+
if data.get("session_id") == session_id:
|
|
164
|
+
data.setdefault("claude_pid", None)
|
|
165
|
+
return LocalIdentity(**data)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
150
169
|
def clear_identity(cwd: Path | None = None, claude_pid: int | None = None) -> None:
|
|
151
170
|
cwd = (cwd or repo_root()).resolve()
|
|
152
171
|
if claude_pid is None:
|
|
@@ -463,6 +463,50 @@ def test_mission_tailer_picks_latest_ai_title(env, tmp_path, monkeypatch):
|
|
|
463
463
|
assert _latest_ai_title("/nonexistent/file") is None
|
|
464
464
|
|
|
465
465
|
|
|
466
|
+
def test_find_identity_by_session_id(env, tmp_path):
|
|
467
|
+
from agent_comms import config as cfg
|
|
468
|
+
cfg.LocalIdentity(handle="alpha", server_url="x", cwd=str(tmp_path / "work"),
|
|
469
|
+
claude_pid=1, session_id="s-aaa").save()
|
|
470
|
+
found = cfg.find_identity_by_session_id("s-aaa")
|
|
471
|
+
assert found is not None and found.handle == "alpha"
|
|
472
|
+
assert cfg.find_identity_by_session_id("s-other") is None
|
|
473
|
+
assert cfg.find_identity_by_session_id(None) is None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def test_statusline_renders_colored_badge(env, tmp_path, monkeypatch):
|
|
477
|
+
run(["init", "--handle", "alpha"])
|
|
478
|
+
# Force a known display_name + color_hex on the server-side identity
|
|
479
|
+
from agent_comms.api import Client
|
|
480
|
+
Client()._h.post("/api/identities/alpha/mission", json={"text": "refactoring auth"})
|
|
481
|
+
# Stamp a session_id into the local identity so find-by-session works
|
|
482
|
+
from agent_comms import config as cfg
|
|
483
|
+
ident = cfg.load_identity()
|
|
484
|
+
ident.session_id = "s-test"
|
|
485
|
+
ident.save()
|
|
486
|
+
payload = json.dumps({"session_id": "s-test", "workspace": {"current_dir": str(tmp_path / "work")}})
|
|
487
|
+
from agent_comms.cli import app
|
|
488
|
+
result = CliRunner().invoke(app, ["statusline"], input=payload)
|
|
489
|
+
assert result.exit_code == 0
|
|
490
|
+
out = result.output
|
|
491
|
+
# ANSI escape sequence + display name + mission
|
|
492
|
+
assert "\033[" in out
|
|
493
|
+
assert "refactoring auth" in out
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def test_install_statusline_writes_settings(env, tmp_path, monkeypatch):
|
|
497
|
+
claude_home = tmp_path / "claude-home"
|
|
498
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(claude_home))
|
|
499
|
+
r = run(["install-statusline"])
|
|
500
|
+
assert r.exit_code == 0
|
|
501
|
+
data = json.loads((claude_home / "settings.json").read_text())
|
|
502
|
+
assert "statusLine" in data
|
|
503
|
+
assert "-m agent_comms statusline" in data["statusLine"]["command"]
|
|
504
|
+
# Idempotent
|
|
505
|
+
run(["install-statusline"])
|
|
506
|
+
data2 = json.loads((claude_home / "settings.json").read_text())
|
|
507
|
+
assert data2["statusLine"] == data["statusLine"]
|
|
508
|
+
|
|
509
|
+
|
|
466
510
|
def test_post_json(env):
|
|
467
511
|
run(["init", "--handle", "alpha"])
|
|
468
512
|
payload = json.dumps({"title": "T", "summary": "S", "body": "B", "tags": ["x"]})
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|