/** * connection:detect * * Compare our carry graph against other DIDs' connection records. * Discovers DIDs publishing network.cosmik.connection records, * fetches their edge sets, and computes Jaccard similarity against our graph. * * Usage: * bun connection:detect # compare against all network DIDs * bun connection:detect --limit 10 # only check 10 DIDs * bun connection:detect --threshold 0.05 # only show similarity >= 0.05 * bun connection:detect --json # machine-readable output * bun connection:detect --write # create connection records for top matches */ import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { AtpAgent } from '@atproto/api'; import { loadDotEnv } from './cli-utils.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); await loadDotEnv(resolve(__dirname, '..', '.env')); const RELAY = process.env.ATPROTO_RELAY ?? 'https://bsky.network'; const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? ''; const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? ''; const ATPROTO_SERVICE = process.env.ATPROTO_SERVICE ?? 'https://bsky.social'; const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network'; const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; const COLLECTION = 'network.cosmik.connection'; interface NetworkIsland { did: string; handle?: string; pds?: string; urls: Set; edgeCount: number; } interface SimilarityResult { did: string; handle?: string; edgeCount: number; sharedCount: number; similarity: number; sharedUrls: string[]; } // --- Network discovery --- async function discoverDIDs(limit?: number): Promise { const dids: string[] = []; let cursor = ''; while (true) { const url = new URL(`${RELAY}/xrpc/com.atproto.sync.listReposByCollection`); url.searchParams.set('collection', COLLECTION); url.searchParams.set('limit', '1000'); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url.toString()); if (!res.ok) { console.error(`Relay query failed (${res.status}): ${await res.text().catch(() => '')}`); break; } const data = await res.json() as { repos: Array<{ did: string }>; cursor?: string }; for (const r of data.repos) { dids.push(r.did); if (limit && dids.length >= limit) return dids; } cursor = data.cursor ?? ''; if (!cursor) break; await new Promise((r) => setTimeout(r, 200)); // rate limit } return dids; } async function resolvePDS(did: string): Promise { try { const res = await fetch(`https://plc.directory/${did}`); if (!res.ok) return null; const doc = await res.json() as { service?: Array<{ id: string; serviceEndpoint: string }> }; const pds = doc.service?.find((s) => s.id === '#atproto_pds'); return pds?.serviceEndpoint ?? null; } catch { return null; } } async function resolveHandle(did: string): Promise { try { const res = await fetch(`https://plc.directory/${did}`); if (!res.ok) return null; const doc = await res.json() as { alsoKnownAs?: string[] }; const handle = doc.alsoKnownAs?.find((h) => h.startsWith('at://')); return handle?.replace('at://', '') ?? null; } catch { return null; } } async function fetchConnectionRecords(pds: string, did: string): Promise> { const records: Array<{ source?: string; target?: string; connectionType?: string }> = []; let cursor = ''; while (true) { const url = new URL(`${pds}/xrpc/com.atproto.repo.listRecords`); url.searchParams.set('repo', did); url.searchParams.set('collection', COLLECTION); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url.toString()); if (!res.ok) break; const data = await res.json() as { records: Array<{ value: { source?: string; target?: string; connectionType?: string } }>; cursor?: string }; for (const r of data.records) { records.push(r.value); } cursor = data.cursor ?? ''; if (!cursor) break; await new Promise((r) => setTimeout(r, 100)); } return records; } // --- Our graph --- function normalizeUrl(url: string): string { try { const u = new URL(url); u.hash = ''; if (u.hostname.startsWith('www.')) u.hostname = u.hostname.slice(4); return u.toString(); } catch { return url; } } function buildOurUrlSet(): Set { const urls = new Set(); // From carry connections const connections = carryQueryJson<{ source?: string; target?: string }>('org.latha.connection', ['source', 'target']); const entities = carryQueryJson<{ name?: string; url?: string }>('org.latha.entity', ['name', 'url']); const entityUrls = new Map(); for (const e of entities) { if (e.name && e.url?.startsWith('https://')) { entityUrls.set(e.name, e.url); } } for (const c of connections) { const sUrl = entityUrls.get(c.source ?? '') ?? (c.source?.startsWith('https://') ? c.source : undefined); const tUrl = entityUrls.get(c.target ?? '') ?? (c.target?.startsWith('https://') ? c.target : undefined); if (sUrl) urls.add(normalizeUrl(sUrl)); if (tUrl) urls.add(normalizeUrl(tUrl)); } return urls; } function carryQueryJson(domain: string, fields: string[]): T[] { try { const out = execFileSync('carry', ['query', domain, ...fields, '--format', 'json'], { cwd: CARRY_DIR, encoding: 'utf-8', timeout: 15_000, }); return JSON.parse(out) as T[]; } catch { return []; } } // --- Similarity --- function jaccard(a: Set, b: Set): number { if (a.size === 0 && b.size === 0) return 0; let intersection = 0; for (const item of a) { if (b.has(item)) intersection++; } const union = a.size + b.size - intersection; return union === 0 ? 0 : intersection / union; } function sharedUrls(a: Set, b: Set): string[] { const shared: string[] = []; for (const url of a) { if (b.has(url)) shared.push(url); } return shared.sort(); } // --- Main --- async function main(): Promise { const args = process.argv.slice(2); const wantJson = args.includes('--json'); const wantWrite = args.includes('--write'); const limitArg = args.find((a) => a.startsWith('--limit=')); const limit = limitArg ? Number(limitArg.split('=')[1]) : undefined; const thresholdArg = args.find((a) => a.startsWith('--threshold=')); const threshold = thresholdArg ? Number(thresholdArg.split('=')[1]) : 0.01; console.error('Building our carry graph URL set...'); const ourUrls = buildOurUrlSet(); // Also fetch our own PDS connections const ourDid = await resolveOurDid(); if (ourDid) { const pds = await resolvePDS(ourDid); if (pds) { const ourRecords = await fetchConnectionRecords(pds, ourDid); for (const r of ourRecords) { if (r.source?.startsWith('https://')) ourUrls.add(normalizeUrl(r.source)); if (r.target?.startsWith('https://')) ourUrls.add(normalizeUrl(r.target)); } } } console.error(`Our graph: ${ourUrls.size} unique HTTPS URLs`); console.error('Discovering network DIDs...'); const dids = await discoverDIDs(limit); console.error(`Found ${dids.length} DIDs with ${COLLECTION} records`); // Skip our own DID const otherDids = ourDid ? dids.filter((d) => d !== ourDid) : dids; const results: SimilarityResult[] = []; for (let i = 0; i < otherDids.length; i++) { const did = otherDids[i]; console.error(`[${i + 1}/${otherDids.length}] Fetching ${did}...`); const pds = await resolvePDS(did); if (!pds) { console.error(` Could not resolve PDS`); continue; } const records = await fetchConnectionRecords(pds, did); const theirUrls = new Set(); for (const r of records) { if (r.source?.startsWith('https://')) theirUrls.add(normalizeUrl(r.source)); if (r.target?.startsWith('https://')) theirUrls.add(normalizeUrl(r.target)); } const sim = jaccard(ourUrls, theirUrls); const shared = sharedUrls(ourUrls, theirUrls); if (sim >= threshold) { const handle = await resolveHandle(did); results.push({ did, handle: handle ?? undefined, edgeCount: records.length, sharedCount: shared.length, similarity: sim, sharedUrls: shared, }); } } results.sort((a, b) => b.similarity - a.similarity); if (wantWrite && results.length > 0) { const agent = new AtpAgent({ service: PDS_SERVICE }); if (BLUESKY_HANDLE && BLUESKY_APP_PASSWORD) { await agent.login({ identifier: BLUESKY_HANDLE, password: BLUESKY_APP_PASSWORD }); } let wrote = 0; for (const r of results.slice(0, 10)) { if (r.similarity < 0.05) continue; const profileUrl = r.handle ? `https://bsky.app/profile/${r.handle}` : `https://bsky.app/profile/${r.did}`; try { await agent.api.com.atproto.repo.createRecord({ repo: agent.session!.did, collection: COLLECTION, record: { $type: COLLECTION, source: profileUrl, target: `https://bsky.app/profile/${BLUESKY_HANDLE}`, connectionType: 'SIMILAR_ISLAND', note: `Island similarity: ${(r.similarity * 100).toFixed(1)}% (${r.sharedCount} shared URLs)`, createdAt: new Date().toISOString(), }, }); wrote++; } catch { // skip } } console.error(`Wrote ${wrote} similarity connection records`); } if (wantJson) { console.log(JSON.stringify({ ourUrlCount: ourUrls.size, didCount: otherDids.length, results }, null, 2)); return; } console.log(`\nConnection detect: compared against ${otherDids.length} network DIDs`); console.log(`Our graph: ${ourUrls.size} unique HTTPS URLs`); console.log(`Matches (similarity >= ${threshold}): ${results.length}\n`); for (const r of results.slice(0, 20)) { const label = r.handle ?? r.did; console.log(`${r.similarity.toFixed(4)} | ${label} (${r.edgeCount} edges, ${r.sharedCount} shared)`); for (const url of r.sharedUrls) { console.log(` ${url}`); } } } async function resolveOurDid(): Promise { if (!BLUESKY_HANDLE) return null; try { const agent = new AtpAgent({ service: ATPROTO_SERVICE }); await agent.login({ identifier: BLUESKY_HANDLE, password: BLUESKY_APP_PASSWORD }); return agent.session!.did; } catch { return null; } } main().catch((err) => { console.error('Fatal:', err instanceof Error ? err.message : String(err)); process.exit(1); });