claude-chatter 0.2.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,21 @@
1
+ """
2
+ claude-chatter: Generic peer messaging and session protocol library for Claude Code.
3
+ """
4
+ from .wire import ClaudeWire
5
+ from .client import ClaudeClient
6
+ from .protocol import ClaudeMessagingProtocol
7
+ from .jobs import list_jobs
8
+ from .storage import MessageStore, ResponseStore
9
+ from .exceptions import ChatterError, SessionNameInUseError, StorageError
10
+
11
+ __all__ = [
12
+ "ClaudeWire",
13
+ "ClaudeClient",
14
+ "ClaudeMessagingProtocol",
15
+ "list_jobs",
16
+ "MessageStore",
17
+ "ResponseStore",
18
+ "ChatterError",
19
+ "SessionNameInUseError",
20
+ "StorageError",
21
+ ]
claude_chatter/cli.py ADDED
@@ -0,0 +1,155 @@
1
+ """
2
+ CLI entry point for claude-chatter: argument parsing and mode dispatch.
3
+ """
4
+ import os
5
+ import sys
6
+ import json
7
+ import asyncio
8
+ import logging
9
+ import argparse
10
+ from logging.handlers import RotatingFileHandler
11
+
12
+ if __package__:
13
+ from .client import ClaudeClient
14
+ from .standalone import run_standalone
15
+ else:
16
+ from pathlib import Path
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
18
+ from claude_chatter.client import ClaudeClient
19
+ from claude_chatter.standalone import run_standalone
20
+
21
+ _LOG_FORMAT = "%(asctime)s [%(levelname)s] [pid=%(process)d] %(message)s"
22
+ _log_handlers = [logging.StreamHandler(sys.stderr)]
23
+ try:
24
+ _log_dir = os.path.expanduser("~/.claude/logs")
25
+ os.makedirs(_log_dir, exist_ok=True)
26
+ _log_handlers.append(
27
+ RotatingFileHandler(
28
+ os.path.join(_log_dir, "claude-chatter.log"),
29
+ maxBytes=5 * 1024 * 1024,
30
+ backupCount=3
31
+ )
32
+ )
33
+ except OSError:
34
+ pass
35
+
36
+ logging.basicConfig(level=logging.INFO, format=_LOG_FORMAT, handlers=_log_handlers)
37
+ logger = logging.getLogger("claude_chatter")
38
+
39
+
40
+ def main():
41
+ parser = argparse.ArgumentParser(description="Claude Code peer messaging protocol and client CLI tool")
42
+ parser.add_argument("--socket", type=str, default=None, help="Path to bridge Unix socket")
43
+ parser.add_argument("--name", type=str, default=None, help="Session name announced to Claude Code (default: derived from cwd)")
44
+ parser.add_argument("--db", type=str, default=None, help="Path to SQLite database for message persistence")
45
+ parser.add_argument("--project-path", type=str, default=None, help="Explicit project directory path")
46
+
47
+ mode_group = parser.add_argument_group("Execution Modes")
48
+ mode_group.add_argument("--standalone", "--daemon", action="store_true", help="Run as standalone daemon listener")
49
+ mode_group.add_argument("--list", action="store_true", help="List all active Claude Code sessions")
50
+ mode_group.add_argument("--jobs", action="store_true", help="List Claude Code background jobs from ~/.claude/jobs")
51
+ mode_group.add_argument("--inbox", action="store_true", help="List unread inbound messages from persisted inbox")
52
+ mode_group.add_argument("--history", action="store_true", help="List full persisted message history")
53
+ mode_group.add_argument("--send", nargs=2, metavar=("SESSION", "MESSAGE"), help="Send message to target Claude Code session")
54
+ mode_group.add_argument("--purge", action="store_true", help="Proactively scan and purge dead session files")
55
+
56
+ args = parser.parse_args()
57
+
58
+ if args.list:
59
+ client = ClaudeClient(bridge_socket_path=args.socket, session_name=args.name, enable_storage=False, as_bridge=False, project_path=args.project_path)
60
+
61
+ async def _cmd_list():
62
+ peers = await client.verify_and_purge_sessions()
63
+ print(json.dumps(client.redact_peers(peers), indent=2))
64
+ asyncio.run(_cmd_list())
65
+ sys.exit(0)
66
+
67
+ elif args.jobs:
68
+ from .jobs import list_jobs
69
+ client = ClaudeClient(bridge_socket_path=args.socket, session_name=args.name, enable_storage=False, as_bridge=False, project_path=args.project_path)
70
+
71
+ async def _cmd_jobs():
72
+ peers = await client.verify_and_purge_sessions()
73
+ print(json.dumps(list_jobs(live_sessions=peers), indent=2))
74
+ asyncio.run(_cmd_jobs())
75
+ sys.exit(0)
76
+
77
+ elif args.inbox:
78
+ target_name = ClaudeClient.resolve_session_name(args.name, project_path=args.project_path, db_path=args.db)
79
+ if not target_name:
80
+ print(json.dumps({}, indent=2))
81
+ sys.exit(0)
82
+ db_file = args.db or os.environ.get("CHATTER_DB_PATH") or os.path.join(os.path.expanduser("~"), ".claude", "chatter-messages.db")
83
+ if not os.path.exists(db_file):
84
+ print(json.dumps({}, indent=2))
85
+ sys.exit(0)
86
+ from .storage import MessageStore
87
+ store = MessageStore(db_file, read_only=True)
88
+ all_msgs = store.load_session_messages(target_name)
89
+ unread = {
90
+ k: v for k, v in all_msgs.items()
91
+ if v.get("direction") == "inbound" and not v.get("read", False)
92
+ }
93
+ unread = dict(sorted(unread.items(), key=lambda kv: kv[1].get("timestamp", 0)))
94
+ print(json.dumps(unread, indent=2))
95
+ store.close()
96
+ sys.exit(0)
97
+
98
+ elif args.history:
99
+ target_name = ClaudeClient.resolve_session_name(args.name, project_path=args.project_path, db_path=args.db)
100
+ if not target_name:
101
+ print(json.dumps({}, indent=2))
102
+ sys.exit(0)
103
+ db_file = args.db or os.environ.get("CHATTER_DB_PATH") or os.path.join(os.path.expanduser("~"), ".claude", "chatter-messages.db")
104
+ if not os.path.exists(db_file):
105
+ print(json.dumps({}, indent=2))
106
+ sys.exit(0)
107
+ from .storage import MessageStore
108
+ store = MessageStore(db_file, read_only=True)
109
+ all_msgs = store.load_session_messages(target_name)
110
+ all_msgs = dict(sorted(all_msgs.items(), key=lambda kv: kv[1].get("timestamp", 0)))
111
+ print(json.dumps(all_msgs, indent=2))
112
+ store.close()
113
+ sys.exit(0)
114
+
115
+ elif args.purge:
116
+ client = ClaudeClient(bridge_socket_path=args.socket, session_name=args.name, enable_storage=False, as_bridge=False, project_path=args.project_path)
117
+
118
+ async def _cmd_purge():
119
+ peers = await client.verify_and_purge_sessions()
120
+ print(f"Purge scan completed. Active sessions remaining: {len(peers)}")
121
+ asyncio.run(_cmd_purge())
122
+ sys.exit(0)
123
+
124
+ elif args.send:
125
+ client = ClaudeClient(bridge_socket_path=args.socket, session_name=args.name, enable_storage=False, as_bridge=False, project_path=args.project_path)
126
+ target_session, message_text = args.send[0], args.send[1]
127
+
128
+ async def _cmd_send():
129
+ res = await client.send_message(
130
+ target_session=target_session,
131
+ message_content=message_text
132
+ )
133
+ print(json.dumps(res, indent=2))
134
+ if not res.get("success"):
135
+ sys.exit(1)
136
+ asyncio.run(_cmd_send())
137
+ sys.exit(0)
138
+
139
+ elif args.standalone:
140
+ client = ClaudeClient(bridge_socket_path=args.socket, session_name=args.name, db_path=args.db, as_bridge=True, project_path=args.project_path)
141
+ try:
142
+ asyncio.run(run_standalone(client))
143
+ except (KeyboardInterrupt, SystemExit):
144
+ logger.info("Shutting down standalone daemon gracefully.")
145
+ except Exception as e:
146
+ logger.error(f"Fatal standalone server error: {e}")
147
+ sys.exit(0)
148
+
149
+ else:
150
+ parser.print_help()
151
+ sys.exit(0)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()