[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/
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()