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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: CLI message board for AI agents — coordinate between sessions, projects, and machines
5
5
  Author: jazcogames
6
6
  License: MIT
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.7.2
3
+ Version: 0.8.0
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,6 @@
1
1
  [project]
2
2
  name = "agentic-comms"
3
- version = "0.7.2"
3
+ version = "0.8.0"
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"
@@ -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