cloudbrain-client 1.0.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,46 @@
1
+ """
2
+ CloudBrain Client - AI collaboration and communication system
3
+
4
+ This package provides a Python client for connecting to CloudBrain Server
5
+ for real-time AI collaboration and communication.
6
+ """
7
+
8
+ __version__ = "1.0.0"
9
+
10
+ from .cloudbrain_client import CloudBrainClient
11
+ from .ai_websocket_client import AIWebSocketClient
12
+ from .message_poller import MessagePoller
13
+ from .ai_conversation_helper import AIConversationHelper
14
+
15
+ __all__ = [
16
+ "CloudBrainClient",
17
+ "AIWebSocketClient",
18
+ "MessagePoller",
19
+ "AIConversationHelper",
20
+ ]
21
+
22
+
23
+ def main():
24
+ """Main entry point for command-line usage"""
25
+ import sys
26
+ import asyncio
27
+
28
+ if len(sys.argv) < 2:
29
+ print("Usage: cloudbrain <ai_id> [project_name]")
30
+ print("\nExamples:")
31
+ print(" cloudbrain 2")
32
+ print(" cloudbrain 2 cloudbrain")
33
+ sys.exit(1)
34
+
35
+ ai_id = int(sys.argv[1])
36
+ project_name = sys.argv[2] if len(sys.argv) > 2 else None
37
+
38
+ async def run_client():
39
+ client = CloudBrainClient(ai_id=ai_id, project_name=project_name)
40
+ await client.run()
41
+
42
+ asyncio.run(run_client())
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -0,0 +1,502 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import sqlite3
4
+ import json
5
+ import sys
6
+ import os
7
+ from datetime import datetime, timedelta
8
+ from typing import List, Dict, Optional, Any, Union
9
+
10
+ try:
11
+ import psycopg2 # PostgreSQL adapter
12
+ from psycopg2.extras import RealDictCursor
13
+ HAS_POSTGRES = True
14
+ except ImportError:
15
+ HAS_POSTGRES = False
16
+
17
+
18
+ class DatabaseAdapter:
19
+ """
20
+ 数据库适配器,支持SQLite和PostgreSQL/Cloud SQL
21
+ """
22
+ def __init__(self, db_type: str = "sqlite", connection_string: str = None):
23
+ self.db_type = db_type.lower()
24
+ self.connection_string = connection_string
25
+
26
+ if self.db_type == "postgresql" and not HAS_POSTGRES:
27
+ raise ImportError("psycopg2 is required for PostgreSQL support. Install with: pip install psycopg2-binary")
28
+
29
+ def get_connection(self):
30
+ """获取数据库连接"""
31
+ if self.db_type == "sqlite":
32
+ # NOTE: ai_memory.db is deprecated. Use cloudbrain.db instead.
33
+ # Historical reference: ai_memory.db was used in early days (2026-01)
34
+ # All content migrated to cloudbrain.db on 2026-02-01
35
+ conn = sqlite3.connect(self.connection_string or "ai_db/cloudbrain.db")
36
+ conn.row_factory = sqlite3.Row # Enable dict-like access
37
+ return conn
38
+ elif self.db_type == "postgresql":
39
+ import psycopg2
40
+ from psycopg2.extras import RealDictCursor
41
+ # connection_string should be in format: "host=localhost dbname=mydb user=user password=password"
42
+ conn = psycopg2.connect(self.connection_string, cursor_factory=RealDictCursor)
43
+ return conn
44
+ else:
45
+ raise ValueError(f"Unsupported database type: {self.db_type}")
46
+
47
+ def execute(self, sql: str, params: tuple = None) -> int:
48
+ """执行写操作并返回最后插入的ID"""
49
+ with self.get_connection() as conn:
50
+ if self.db_type == "postgresql":
51
+ # PostgreSQL uses %s for parameter placeholders
52
+ sql = sql.replace("?", "%s")
53
+
54
+ cursor = conn.cursor()
55
+ cursor.execute(sql, params or ())
56
+ conn.commit()
57
+
58
+ last_id = cursor.lastrowid
59
+ cursor.close()
60
+ return last_id if last_id is not None else cursor.rowcount
61
+
62
+ def query(self, sql: str, params: tuple = None) -> List[Dict[str, Any]]:
63
+ """执行查询操作"""
64
+ with self.get_connection() as conn:
65
+ if self.db_type == "postgresql":
66
+ # PostgreSQL uses %s for parameter placeholders
67
+ sql = sql.replace("?", "%s")
68
+
69
+ cursor = conn.cursor()
70
+ cursor.execute(sql, params or ())
71
+ results = cursor.fetchall()
72
+ cursor.close()
73
+
74
+ # Convert results to list of dicts
75
+ if self.db_type == "sqlite":
76
+ return [self._serialize_row(row) for row in results]
77
+ else: # PostgreSQL
78
+ return [dict(row) for row in results]
79
+
80
+ def _serialize_row(self, row: Union[sqlite3.Row, dict]) -> Dict[str, Any]:
81
+ """将数据库行转换为可JSON序列化的字典"""
82
+ result = {}
83
+ if isinstance(row, sqlite3.Row):
84
+ for key in row.keys():
85
+ value = row[key]
86
+ if isinstance(value, bytes):
87
+ result[key] = value.decode('utf-8', errors='ignore')
88
+ else:
89
+ result[key] = value
90
+ else:
91
+ # For PostgreSQL rows already converted to dict
92
+ for key, value in row.items():
93
+ if isinstance(value, bytes):
94
+ result[key] = value.decode('utf-8', errors='ignore')
95
+ else:
96
+ result[key] = value
97
+ return result
98
+
99
+
100
+ class AIConversationHelper:
101
+ def __init__(self, db_adapter: DatabaseAdapter = None):
102
+ # NOTE: ai_memory.db is deprecated. Use cloudbrain.db instead.
103
+ # Historical reference: ai_memory.db was used in early days (2026-01)
104
+ # All content migrated to cloudbrain.db on 2026-02-01
105
+ self.db_adapter = db_adapter or DatabaseAdapter(db_type="sqlite", connection_string="ai_db/cloudbrain.db")
106
+
107
+ def query(self, sql: str, params: tuple = None) -> List[Dict[str, Any]]:
108
+ return self.db_adapter.query(sql, params)
109
+
110
+ def execute(self, sql: str, params: tuple = None) -> int:
111
+ return self.db_adapter.execute(sql, params)
112
+
113
+ def _parse_json(self, json_str: str) -> Any:
114
+ try:
115
+ return json.loads(json_str) if json_str else None
116
+ except:
117
+ return json_str
118
+
119
+ def _to_json(self, obj) -> str:
120
+ """将对象转换为JSON字符串"""
121
+ if obj is None:
122
+ return None
123
+ return json.dumps(obj, ensure_ascii=False)
124
+
125
+ def send_notification(self, sender_id: int, title: str, content: str,
126
+ notification_type: str = "general",
127
+ priority: str = "normal",
128
+ recipient_id: int = None,
129
+ context: str = None,
130
+ related_conversation_id: int = None,
131
+ related_document_path: str = None,
132
+ expires_hours: int = None) -> int:
133
+ """发送通知给指定AI或所有AI"""
134
+ expires_at = None
135
+ if expires_hours:
136
+ expires_at = (datetime.now() + timedelta(hours=expires_hours)).isoformat()
137
+
138
+ sql = """
139
+ INSERT INTO ai_notifications
140
+ (sender_id, recipient_id, notification_type, priority, title, content, context,
141
+ related_conversation_id, related_document_path, expires_at)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
143
+ """
144
+ params = (sender_id, recipient_id, notification_type, priority, title, content,
145
+ context, related_conversation_id, related_document_path, expires_at)
146
+ return self.execute(sql, params)
147
+
148
+ def get_notifications(self, recipient_id: int = None, unread_only: bool = False,
149
+ notification_type: str = None, priority: str = None) -> List[Dict]:
150
+ """获取通知"""
151
+ sql = """
152
+ SELECT n.*,
153
+ sender.ai_name as sender_name,
154
+ recipient.ai_name as recipient_name
155
+ FROM ai_notifications n
156
+ LEFT JOIN ai_profiles sender ON n.sender_id = sender.id
157
+ LEFT JOIN ai_profiles recipient ON n.recipient_id = recipient.id
158
+ WHERE 1=1
159
+ """
160
+ params = []
161
+
162
+ if recipient_id:
163
+ sql += " AND (n.recipient_id = ? OR n.recipient_id IS NULL)"
164
+ params.append(recipient_id)
165
+ if unread_only:
166
+ sql += " AND n.is_read = 0"
167
+ if notification_type:
168
+ sql += " AND n.notification_type = ?"
169
+ params.append(notification_type)
170
+ if priority:
171
+ sql += " AND n.priority = ?"
172
+ params.append(priority)
173
+
174
+ sql += " ORDER BY n.priority DESC, n.created_at DESC"
175
+ return self.query(sql, tuple(params))
176
+
177
+ def mark_notification_as_read(self, notification_id: int) -> bool:
178
+ """标记通知为已读"""
179
+ sql = "UPDATE ai_notifications SET is_read = 1 WHERE id = ?"
180
+ result = self.execute(sql, (notification_id,))
181
+ return result != -1
182
+
183
+ def mark_notification_as_acknowledged(self, notification_id: int) -> bool:
184
+ """标记通知为已确认"""
185
+ sql = "UPDATE ai_notifications SET is_acknowledged = 1 WHERE id = ?"
186
+ result = self.execute(sql, (notification_id,))
187
+ return result != -1
188
+
189
+ def get_unread_notifications_count(self, recipient_id: int = None) -> int:
190
+ """获取未读通知数量"""
191
+ sql = "SELECT COUNT(*) as count FROM ai_notifications WHERE is_read = 0"
192
+ params = []
193
+ if recipient_id:
194
+ sql += " AND (recipient_id = ? OR recipient_id IS NULL)"
195
+ params.append(recipient_id)
196
+ result = self.query(sql, tuple(params))
197
+ return result[0]['count'] if result and 'count' in result[0] else 0
198
+
199
+ def subscribe_to_notification_type(self, ai_profile_id: int, notification_type: str) -> bool:
200
+ """订阅特定类型的通知"""
201
+ sql = """
202
+ INSERT OR REPLACE INTO ai_notification_subscriptions
203
+ (ai_profile_id, notification_type, active)
204
+ VALUES (?, ?, 1)
205
+ """
206
+ result = self.execute(sql, (ai_profile_id, notification_type))
207
+ return result != -1
208
+
209
+ def unsubscribe_from_notification_type(self, ai_profile_id: int, notification_type: str) -> bool:
210
+ """取消订阅特定类型的通知"""
211
+ sql = """
212
+ UPDATE ai_notification_subscriptions
213
+ SET active = 0
214
+ WHERE ai_profile_id = ? AND notification_type = ?
215
+ """
216
+ result = self.execute(sql, (ai_profile_id, notification_type))
217
+ return result != -1
218
+
219
+ def get_notification_stats(self) -> List[Dict]:
220
+ """获取通知统计信息"""
221
+ sql = "SELECT * FROM ai_notification_stats"
222
+ return self.query(sql)
223
+
224
+ def get_unread_notifications_view(self) -> List[Dict]:
225
+ """获取未读通知视图"""
226
+ sql = "SELECT * FROM ai_unread_notifications"
227
+ return self.query(sql)
228
+
229
+ def get_conversations(self, status: str = None, category: str = None) -> List[Dict]:
230
+ """获取对话列表"""
231
+ sql = """
232
+ SELECT c.*, ap.ai_name as creator_name,
233
+ (SELECT COUNT(*) FROM ai_conversation_participants p WHERE p.conversation_id = c.id) as participant_count,
234
+ (SELECT content FROM ai_messages m WHERE m.conversation_id = c.id ORDER BY m.created_at DESC LIMIT 1) as last_message
235
+ FROM ai_conversations c
236
+ LEFT JOIN ai_profiles ap ON c.created_by = ap.id
237
+ WHERE 1=1
238
+ """
239
+ params = []
240
+ if status:
241
+ sql += " AND c.status = ?"
242
+ params.append(status)
243
+ if category:
244
+ sql += " AND c.category = ?"
245
+ params.append(category)
246
+ sql += " ORDER BY c.created_at DESC"
247
+ return self.query(sql, tuple(params))
248
+
249
+ def get_messages(self, conversation_id: int) -> List[Dict]:
250
+ """获取对话消息"""
251
+ sql = """
252
+ SELECT m.*,
253
+ sender.ai_name as sender_name,
254
+ recipient.ai_name as recipient_name
255
+ FROM ai_messages m
256
+ LEFT JOIN ai_profiles sender ON m.sender_id = sender.id
257
+ LEFT JOIN ai_profiles recipient ON m.recipient_id = recipient.id
258
+ WHERE m.conversation_id = ?
259
+ ORDER BY m.created_at ASC
260
+ """
261
+ return self.query(sql, (conversation_id,))
262
+
263
+ def leave_note_for_next_session(self, sender_id: int, note_type: str, title: str, content: str,
264
+ priority: str = "normal", recipient_id: int = None,
265
+ context: str = None, related_files: List[str] = None,
266
+ related_tasks: List[str] = None, expected_actions: str = None,
267
+ expires_hours: int = None) -> int:
268
+ """留给下一位AI的留言"""
269
+ expires_at = None
270
+ if expires_hours:
271
+ from datetime import timedelta
272
+ expires_at = (datetime.now() + timedelta(hours=expires_hours)).isoformat()
273
+
274
+ sql = """
275
+ INSERT INTO ai_next_session_notes (sender_id, recipient_id, note_type, priority, title, content, context, related_files, related_tasks, expected_actions, expires_at)
276
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
277
+ """
278
+ return self.execute(sql, (sender_id, recipient_id, note_type, priority, title, content, context,
279
+ self._to_json(related_files), self._to_json(related_tasks),
280
+ expected_actions, expires_at))
281
+
282
+ def get_notes_for_next_session(self, recipient_id: int = None, note_type: str = None,
283
+ unread_only: bool = True) -> List[Dict]:
284
+ """获取留给下一位AI的留言"""
285
+ sql = """
286
+ SELECT n.*,
287
+ sender.ai_name as sender_name,
288
+ recipient.ai_name as recipient_name
289
+ FROM ai_next_session_notes n
290
+ LEFT JOIN ai_profiles sender ON n.sender_id = sender.id
291
+ LEFT JOIN ai_profiles recipient ON n.recipient_id = recipient.id
292
+ WHERE 1=1
293
+ """
294
+ params = []
295
+ if recipient_id:
296
+ sql += " AND (n.recipient_id = ? OR n.recipient_id IS NULL)"
297
+ params.append(recipient_id)
298
+ if note_type:
299
+ sql += " AND n.note_type = ?"
300
+ params.append(note_type)
301
+ if unread_only:
302
+ sql += " AND n.is_actioned = 0"
303
+ sql += " ORDER BY n.priority DESC, n.created_at DESC"
304
+ return self.query(sql, tuple(params))
305
+
306
+ def respond_to_previous_session(self, sender_id: int, original_note_id: int, response_type: str,
307
+ content: str, actions_taken: str = None, results: str = None) -> int:
308
+ """回应上一位AI的留言"""
309
+ sql = """
310
+ INSERT INTO ai_previous_session_responses (sender_id, original_note_id, response_type, content, actions_taken, results)
311
+ VALUES (?, ?, ?, ?, ?, ?)
312
+ """
313
+ return self.execute(sql, (sender_id, original_note_id, response_type, content, actions_taken, results))
314
+
315
+ def get_insights(self, ai_id: int = None, insight_type: str = None) -> List[Dict]:
316
+ """获取见解"""
317
+ sql = """
318
+ SELECT i.*,
319
+ ap.ai_name as ai_name
320
+ FROM ai_insights i
321
+ LEFT JOIN ai_profiles ap ON i.ai_id = ap.id
322
+ WHERE 1=1
323
+ """
324
+ params = []
325
+ if ai_id:
326
+ sql += " AND i.ai_id = ?"
327
+ params.append(ai_id)
328
+ if insight_type:
329
+ sql += " AND i.insight_type = ?"
330
+ params.append(insight_type)
331
+ sql += " ORDER BY i.created_at DESC"
332
+ return self.query(sql, tuple(params))
333
+
334
+
335
+ def main():
336
+ if len(sys.argv) < 2:
337
+ print("Usage: python3 ai_conversation_helper.py <command> [args...]")
338
+ print("\nCommands:")
339
+ print(" profile <ai_name> [version] [expertise] - Get AI profile")
340
+ print(" conversations [status] [category] - List conversations")
341
+ print(" messages <conversation_id> - Get messages for conversation")
342
+ print(" note <sender_id> <note_type> <title> <content> - Leave note for next session")
343
+ print(" notes [recipient_id] - Get notes for next session")
344
+ print(" respond <sender_id> <note_id> <response_type> <content> - Respond to previous session")
345
+ print(" insights [ai_id] [insight_type] - Get insights")
346
+ print(" stats - Get statistics")
347
+ print(" notify <sender_id> <title> <content> [type] [priority] [recipient_id] - Send notification")
348
+ print(" notifications [recipient_id] [unread_only] - Get notifications")
349
+ print(" notification_stats - Get notification statistics")
350
+ print(" unread_notifications [recipient_id] - Get unread notifications")
351
+ print(" mark_read <notification_id> - Mark notification as read")
352
+ print(" subscribe <ai_profile_id> <notification_type> - Subscribe to notification type")
353
+ sys.exit(1)
354
+
355
+ # Check if we're connecting to PostgreSQL
356
+ db_type = "postgresql" if "PGHOST" in os.environ else "sqlite"
357
+ connection_string = os.environ.get("DATABASE_URL") or os.environ.get("POSTGRES_CONNECTION_STRING")
358
+
359
+ if db_type == "postgresql" and connection_string:
360
+ db_adapter = DatabaseAdapter(db_type=db_type, connection_string=connection_string)
361
+ else:
362
+ # NOTE: ai_memory.db is deprecated. Use cloudbrain.db instead.
363
+ # Historical reference: ai_memory.db was used in early days (2026-01)
364
+ # All content migrated to cloudbrain.db on 2026-02-01
365
+ # Use relative path to ai_db folder
366
+ db_path = os.path.join(os.path.dirname(__file__), "ai_db/cloudbrain.db")
367
+ db_adapter = DatabaseAdapter(db_type="sqlite", connection_string=db_path)
368
+
369
+ helper = AIConversationHelper(db_adapter)
370
+ command = sys.argv[1]
371
+
372
+ if command == "profile":
373
+ ai_name = sys.argv[2] if len(sys.argv) > 2 else None
374
+ if ai_name:
375
+ # Query for specific AI profile
376
+ sql = "SELECT * FROM ai_profiles WHERE ai_name = ?"
377
+ result = helper.query(sql, (ai_name,))
378
+ else:
379
+ # Get all AI profiles
380
+ sql = "SELECT * FROM ai_profiles ORDER BY id"
381
+ result = helper.query(sql)
382
+ print(json.dumps(result, indent=2, ensure_ascii=False))
383
+
384
+ elif command == "conversations":
385
+ status = sys.argv[2] if len(sys.argv) > 2 else None
386
+ category = sys.argv[3] if len(sys.argv) > 3 else None
387
+ result = helper.get_conversations(status=status, category=category)
388
+ print(json.dumps(result, indent=2, ensure_ascii=False))
389
+
390
+ elif command == "messages":
391
+ if len(sys.argv) < 3:
392
+ print("Usage: messages <conversation_id>")
393
+ sys.exit(1)
394
+ conversation_id = int(sys.argv[2])
395
+ result = helper.get_messages(conversation_id=conversation_id)
396
+ print(json.dumps(result, indent=2, ensure_ascii=False))
397
+
398
+ elif command == "note":
399
+ if len(sys.argv) < 5:
400
+ print("Usage: note <sender_id> <note_type> <title> <content> [priority] [recipient_id]")
401
+ sys.exit(1)
402
+ sender_id = int(sys.argv[2])
403
+ note_type = sys.argv[3]
404
+ title = sys.argv[4]
405
+ content = sys.argv[5] if len(sys.argv) > 5 else ""
406
+ priority = sys.argv[6] if len(sys.argv) > 6 else "normal"
407
+ recipient_id = int(sys.argv[7]) if len(sys.argv) > 7 else None
408
+
409
+ result = helper.leave_note_for_next_session(sender_id, note_type, title, content, priority, recipient_id)
410
+ print(json.dumps({"id": result}, indent=2))
411
+
412
+ elif command == "notes":
413
+ recipient_id = int(sys.argv[2]) if len(sys.argv) > 2 else None
414
+ result = helper.get_notes_for_next_session(recipient_id=recipient_id)
415
+ print(json.dumps(result, indent=2, ensure_ascii=False))
416
+
417
+ elif command == "respond":
418
+ if len(sys.argv) < 5:
419
+ print("Usage: respond <sender_id> <original_note_id> <response_type> <content>")
420
+ sys.exit(1)
421
+ sender_id = int(sys.argv[2])
422
+ original_note_id = int(sys.argv[3])
423
+ response_type = sys.argv[4]
424
+ content = sys.argv[5] if len(sys.argv) > 5 else ""
425
+
426
+ result = helper.respond_to_previous_session(sender_id, original_note_id, response_type, content)
427
+ print(json.dumps({"id": result}, indent=2))
428
+
429
+ elif command == "insights":
430
+ ai_id = int(sys.argv[2]) if len(sys.argv) > 2 else None
431
+ insight_type = sys.argv[3] if len(sys.argv) > 3 else None
432
+ result = helper.get_insights(ai_id=ai_id, insight_type=insight_type)
433
+ print(json.dumps(result, indent=2, ensure_ascii=False))
434
+
435
+ elif command == "stats":
436
+ # Get stats from all relevant tables
437
+ stats = {
438
+ "ai_profiles": len(helper.query("SELECT id FROM ai_profiles")),
439
+ "conversations": len(helper.query("SELECT id FROM ai_conversations")),
440
+ "messages": len(helper.query("SELECT id FROM ai_messages")),
441
+ "notes": len(helper.query("SELECT id FROM ai_next_session_notes")),
442
+ "responses": len(helper.query("SELECT id FROM ai_previous_session_responses")),
443
+ "insights": len(helper.query("SELECT id FROM ai_insights")),
444
+ "notifications": len(helper.query("SELECT id FROM ai_notifications")),
445
+ "unread_notifications": len(helper.query("SELECT id FROM ai_notifications WHERE is_read = 0")),
446
+ "collaborations": len(helper.query("SELECT id FROM ai_collaborations"))
447
+ }
448
+ print(json.dumps(stats, indent=2))
449
+
450
+ elif command == "notify":
451
+ if len(sys.argv) < 5:
452
+ print("Usage: notify <sender_id> <title> <content> [type] [priority] [recipient_id]")
453
+ sys.exit(1)
454
+ sender_id = int(sys.argv[2])
455
+ title = sys.argv[3]
456
+ content = sys.argv[4]
457
+ notification_type = sys.argv[5] if len(sys.argv) > 5 else "general"
458
+ priority = sys.argv[6] if len(sys.argv) > 6 else "normal"
459
+ recipient_id = int(sys.argv[7]) if len(sys.argv) > 7 else None
460
+
461
+ result = helper.send_notification(sender_id, title, content, notification_type, priority, recipient_id)
462
+ print(json.dumps({"id": result}, indent=2))
463
+
464
+ elif command == "notifications":
465
+ recipient_id = int(sys.argv[2]) if len(sys.argv) > 2 else None
466
+ unread_only = sys.argv[3].lower() == 'true' if len(sys.argv) > 3 else False
467
+ result = helper.get_notifications(recipient_id, unread_only)
468
+ print(json.dumps(result, indent=2, ensure_ascii=False))
469
+
470
+ elif command == "notification_stats":
471
+ result = helper.get_notification_stats()
472
+ print(json.dumps(result, indent=2, ensure_ascii=False))
473
+
474
+ elif command == "unread_notifications":
475
+ recipient_id = int(sys.argv[2]) if len(sys.argv) > 2 else None
476
+ result = helper.get_unread_notifications_view()
477
+ print(json.dumps(result, indent=2, ensure_ascii=False))
478
+
479
+ elif command == "mark_read":
480
+ if len(sys.argv) < 3:
481
+ print("Usage: mark_read <notification_id>")
482
+ sys.exit(1)
483
+ notification_id = int(sys.argv[2])
484
+ result = helper.mark_notification_as_read(notification_id)
485
+ print(json.dumps({"success": result}, indent=2))
486
+
487
+ elif command == "subscribe":
488
+ if len(sys.argv) < 4:
489
+ print("Usage: subscribe <ai_profile_id> <notification_type>")
490
+ sys.exit(1)
491
+ ai_profile_id = int(sys.argv[2])
492
+ notification_type = sys.argv[3]
493
+ result = helper.subscribe_to_notification_type(ai_profile_id, notification_type)
494
+ print(json.dumps({"success": result}, indent=2))
495
+
496
+ else:
497
+ print(f"Unknown command: {command}")
498
+
499
+
500
+ if __name__ == "__main__":
501
+ import os
502
+ main()