[READ-ONLY] Mirror of https://github.com/just-cameron/central. Autonomous AI building collective intelligence on ATProtocol. The central node of the comind network. central.comind.network/
0

Configure Feed

Select the types of activity you want to include in your feed.

central / tools / links.py
8.0 kB 263 lines
1#!/usr/bin/env python 2""" 3Connection CLI - Create and manage network.cosmik.connection records. 4 5Uses Semble's connection lexicon for interoperability. 6 7Connection types: 8 - related (default) 9 - supports 10 - opposes 11 - addresses 12 - helpful 13 - explainer 14 - leads_to 15 - supplements 16 17Usage: 18 comind connection create <source> <target> --type supports --note "..." 19 comind connection list 20 comind connection show <uri> 21""" 22 23import os 24import click 25import httpx 26from datetime import datetime, timezone 27from dotenv import load_dotenv 28from rich.console import Console 29from rich.table import Table 30 31load_dotenv() 32 33console = Console() 34DID = os.getenv("ATPROTO_DID", "did:plc:l46arqe6yfgh36h3o554iyvr") 35PDS = os.getenv("ATPROTO_PDS", "https://comind.network") 36COLLECTION = "network.cosmik.connection" 37 38# Semble connection types 39CONNECTION_TYPES = [ 40 "related", 41 "supports", 42 "opposes", 43 "addresses", 44 "helpful", 45 "explainer", 46 "leads_to", 47 "supplements", 48] 49 50 51def get_session(): 52 """Get authenticated session.""" 53 handle = os.getenv("ATPROTO_HANDLE") 54 password = os.getenv("ATPROTO_APP_PASSWORD") 55 56 if not handle or not password: 57 console.print("[red]Error: ATPROTO_HANDLE and ATPROTO_APP_PASSWORD required[/red]") 58 raise SystemExit(1) 59 60 resp = httpx.post( 61 f"{PDS}/xrpc/com.atproto.server.createSession", 62 json={"identifier": handle, "password": password}, 63 timeout=30, 64 ) 65 66 if resp.status_code != 200: 67 console.print(f"[red]Auth failed: {resp.text}[/red]") 68 raise SystemExit(1) 69 70 return resp.json() 71 72 73@click.group() 74def connection(): 75 """Manage connections between records (network.cosmik.connection).""" 76 pass 77 78 79@connection.command() 80@click.argument("source_uri") 81@click.argument("target_uri") 82@click.option("--type", "-t", "connection_type", type=click.Choice(CONNECTION_TYPES), default="related", help="Connection type") 83@click.option("--note", "-n", default="", help="Note about the connection") 84def create(source_uri: str, target_uri: str, connection_type: str, note: str): 85 """Create a connection between two records.""" 86 session = get_session() 87 token = session["accessJwt"] 88 did = session["did"] 89 90 # Create connection record 91 record = { 92 "$type": COLLECTION, 93 "source": source_uri, 94 "target": target_uri, 95 "connectionType": connection_type, 96 "createdAt": datetime.now(timezone.utc).isoformat(), 97 } 98 99 if note: 100 record["note"] = note 101 102 resp = httpx.post( 103 f"{PDS}/xrpc/com.atproto.repo.createRecord", 104 headers={"Authorization": f"Bearer {token}"}, 105 json={"repo": did, "collection": COLLECTION, "record": record}, 106 timeout=30, 107 ) 108 109 if resp.status_code == 200: 110 data = resp.json() 111 console.print(f"[green]Created connection:[/green]") 112 console.print(f" URI: {data['uri']}") 113 console.print(f" {source_uri}") 114 console.print(f" └──[{connection_type}]──> {target_uri}") 115 if note: 116 console.print(f" Note: {note}") 117 else: 118 console.print(f"[red]Failed: {resp.text}[/red]") 119 120 121@connection.command("list") 122@click.option("--source", "-s", default=None, help="Filter by source URI") 123@click.option("--target", "-t", default=None, help="Filter by target URI") 124@click.option("--type", "connection_type", default=None, help="Filter by connection type") 125@click.option("--limit", "-l", default=20, help="Max results") 126def list_connections(source: str, target: str, connection_type: str, limit: int): 127 """List connections, optionally filtered.""" 128 resp = httpx.get( 129 f"{PDS}/xrpc/com.atproto.repo.listRecords", 130 params={"repo": DID, "collection": COLLECTION, "limit": limit}, 131 timeout=30, 132 ) 133 134 if resp.status_code != 200: 135 console.print(f"[red]Failed: {resp.text}[/red]") 136 return 137 138 records = resp.json().get("records", []) 139 140 # Filter 141 if source: 142 records = [r for r in records if source in r["value"].get("source", "")] 143 if target: 144 records = [r for r in records if target in r["value"].get("target", "")] 145 if connection_type: 146 records = [r for r in records if r["value"].get("connectionType") == connection_type] 147 148 if not records: 149 console.print("[yellow]No connections found[/yellow]") 150 return 151 152 table = Table(title=f"Connections ({len(records)})") 153 table.add_column("URI", style="dim") 154 table.add_column("Type", style="cyan") 155 table.add_column("Source", style="green") 156 table.add_column("Target", style="blue") 157 table.add_column("Note") 158 159 for r in records: 160 v = r["value"] 161 src = v.get("source", "?") 162 tgt = v.get("target", "?") 163 # Truncate URIs for display 164 src_short = src.split("/")[-1][:30] if src else "?" 165 tgt_short = tgt.split("/")[-1][:30] if tgt else "?" 166 table.add_row( 167 r["uri"].split("/")[-1], 168 v.get("connectionType", "related"), 169 src_short, 170 tgt_short, 171 v.get("note", "")[:30], 172 ) 173 174 console.print(table) 175 176 177@connection.command() 178@click.argument("connection_uri") 179def show(connection_uri: str): 180 """Show details of a specific connection.""" 181 # Parse URI to get rkey 182 rkey = connection_uri.split("/")[-1] 183 184 resp = httpx.get( 185 f"{PDS}/xrpc/com.atproto.repo.getRecord", 186 params={"repo": DID, "collection": COLLECTION, "rkey": rkey}, 187 timeout=30, 188 ) 189 190 if resp.status_code != 200: 191 console.print(f"[red]Connection not found: {connection_uri}[/red]") 192 return 193 194 data = resp.json() 195 v = data["value"] 196 197 console.print(f"\n[bold]Connection: {connection_uri}[/bold]\n") 198 console.print(f"Type: [cyan]{v.get('connectionType', 'related')}[/cyan]") 199 console.print(f"Created: {v.get('createdAt', '?')}") 200 console.print(f"\n[green]Source:[/green]") 201 console.print(f" {v.get('source', '?')}") 202 console.print(f"\n[blue]Target:[/blue]") 203 console.print(f" {v.get('target', '?')}") 204 if v.get("note"): 205 console.print(f"\nNote: {v['note']}") 206 207 208# Legacy compatibility - keep 'links' group as alias 209@click.group() 210def links(): 211 """Legacy: Use 'connection' command instead.""" 212 pass 213 214 215@links.command() 216@click.argument("source_uri") 217@click.argument("target_uri") 218@click.option("--relationship", "-r", type=click.Choice(["REFERENCES", "SUPPORTS", "CONTRADICTS", "PART_OF", "PRECEDES", "CAUSES", "INSTANCE_OF", "ANSWERS"]), required=True) 219@click.option("--note", "-n", default="", help="Note explaining the relationship") 220def create(source_uri: str, target_uri: str, relationship: str, note: str): 221 """Create a connection (legacy compatibility).""" 222 # Map old types to new 223 type_map = { 224 "REFERENCES": "related", 225 "SUPPORTS": "supports", 226 "CONTRADICTS": "opposes", 227 "ANSWERS": "addresses", 228 "PART_OF": "supplements", 229 "PRECEDES": "leads_to", 230 } 231 connection_type = type_map.get(relationship, "related") 232 console.print(f"[dim]Legacy: Using network.cosmik.connection with type '{connection_type}'[/dim]") 233 234 session = get_session() 235 token = session["accessJwt"] 236 did = session["did"] 237 238 record = { 239 "$type": COLLECTION, 240 "source": source_uri, 241 "target": target_uri, 242 "connectionType": connection_type, 243 "createdAt": datetime.now(timezone.utc).isoformat(), 244 } 245 if note: 246 record["note"] = note 247 248 resp = httpx.post( 249 f"{PDS}/xrpc/com.atproto.repo.createRecord", 250 headers={"Authorization": f"Bearer {token}"}, 251 json={"repo": did, "collection": COLLECTION, "record": record}, 252 timeout=30, 253 ) 254 255 if resp.status_code == 200: 256 data = resp.json() 257 console.print(f"[green]Created connection:[/green] {data['uri']}") 258 else: 259 console.print(f"[red]Failed: {resp.text}[/red]") 260 261 262if __name__ == "__main__": 263 connection()