This repository has no description
0

Configure Feed

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

stigmergic / src / island-publish.ts
6.4 kB 183 lines
1/** 2 * island:publish 3 * 4 * Publish a discovered connected component as an org.latha.island record 5 * on the user's PDS. Reads the SQLite cache from island:discover. 6 * 7 * The canonical island ID is the first 12 hex chars of the SHA-256 of the 8 * lexmin vertex (lexicographically smallest URL in the component). This is 9 * stable across runs — the same component always gets the same ID. 10 * 11 * Usage: 12 * bun island:publish # list islands with their IDs 13 * bun island:publish a1b2c3d4e5f6 # publish by canonical ID 14 * bun island:publish a1b2c3d4e5f6 --dry-run # preview without writing 15 * bun island:publish a1b2c3d4e5f6 --json # JSON output 16 */ 17 18import { resolve, dirname } from 'node:path'; 19import { fileURLToPath } from 'node:url'; 20import { AtpAgent } from '@atproto/api'; 21import { loadDotEnv } from './cli-utils.js'; 22import { openDb, findComponents, findComponentById, fetchComponentEdges, resolveDidHandles, domainFromUrl, type Component, type EdgeRow } from './island-shared.js'; 23 24const __dirname = dirname(fileURLToPath(import.meta.url)); 25await loadDotEnv(resolve(__dirname, '..', '.env')); 26await loadDotEnv('/home/nandi/code/swarm/.env'); // fallback for shared credentials 27 28const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? ''; 29const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? ''; 30const PDS_IDENTIFIER = process.env.PDS_IDENTIFIER ?? BLUESKY_HANDLE; 31const PDS_APP_PASSWORD = process.env.PDS_APP_PASSWORD ?? BLUESKY_APP_PASSWORD; 32const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network'; 33 34// --- Build island record --- 35 36function buildIslandRecord(component: Component, edges: EdgeRow[], didHandles: Map<string, string | null>) { 37 const lexmin = component.lexmin; 38 const domain = domainFromUrl(lexmin); 39 const nodeCount = component.nodes.size; 40 const edgeCount = edges.length; 41 const contributorCount = component.dids.size; 42 43 const connections = edges.map((e) => ({ 44 uri: '', // no AT URI for discovered edges — they're aggregated from multiple DIDs 45 source: e.source, 46 target: e.target, 47 ...(e.relation ? { connectionType: e.relation } : {}), 48 note: `from ${didHandles.get(e.did) ?? e.did}`, 49 })); 50 51 const title = `Island: ${domain} (${nodeCount} nodes, ${edgeCount} edges)`; 52 const themes = ['discovered-island']; 53 const synthesis = [ 54 `Discovered island centered on ${domain}.`, 55 `${nodeCount} nodes connected by ${edgeCount} edges, contributed by ${contributorCount} DIDs.`, 56 `Lexmin vertex: ${lexmin}`, 57 `Contributors: ${[...component.dids].map((d) => didHandles.get(d) ?? d).join(', ')}`, 58 ].join('\n\n'); 59 60 return { 61 $type: 'org.latha.island', 62 source: { 63 uri: lexmin, 64 collection: 'network.cosmik.connection', 65 }, 66 connections, 67 analysis: { 68 title, 69 themes, 70 synthesis, 71 }, 72 createdAt: new Date().toISOString(), 73 }; 74} 75 76// --- Main --- 77 78async function main(): Promise<void> { 79 const args = process.argv.slice(2); 80 const wantJson = args.includes('--json'); 81 const dryRun = args.includes('--dry-run'); 82 const targetId = args.find((a) => !a.startsWith('--')); 83 84 // Open DB 85 let db; 86 try { 87 db = openDb(); 88 const count = (db.query('SELECT COUNT(*) as c FROM edges').get() as { c: number } | null)?.c ?? 0; 89 if (count === 0) { 90 console.error('No edges found. Run `bun island:discover` first.'); 91 process.exit(1); 92 } 93 } catch { 94 console.error('Database not found. Run `bun island:discover` first.'); 95 process.exit(1); 96 } 97 98 console.error('Finding components...'); 99 const components = findComponents(db); 100 101 // No positional arg: list all islands 102 if (!targetId) { 103 if (wantJson) { 104 console.log(JSON.stringify(components.map((c) => ({ 105 id: c.islandId, 106 lexmin: c.lexmin, 107 nodeCount: c.nodes.size, 108 didCount: c.dids.size, 109 })), null, 2)); 110 } else { 111 console.log(`\n${components.length} islands:\n`); 112 for (const c of components.slice(0, 50)) { 113 console.log(` ${c.islandId} ${c.nodes.size} nodes ${domainFromUrl(c.lexmin)}`); 114 } 115 if (components.length > 50) console.log(` ... and ${components.length - 50} more`); 116 console.log(`\nUsage: bun island:publish <island-id> [--dry-run] [--json]`); 117 } 118 db.close(); 119 return; 120 } 121 122 const component = components.find((c) => c.islandId === targetId); 123 if (!component) { 124 console.error(`Island ${targetId} not found. Run without args to list available islands.`); 125 process.exit(1); 126 } 127 128 console.error(`Island ${component.islandId}: ${component.nodes.size} nodes, ${component.dids.size} DIDs (lexmin: ${component.lexmin})`); 129 130 const edges = fetchComponentEdges(db, component); 131 const didHandles = resolveDidHandles(db, component.dids); 132 const record = buildIslandRecord(component, edges, didHandles); 133 134 if (dryRun) { 135 console.error('[dry-run] Would publish:'); 136 if (wantJson) { 137 console.log(JSON.stringify({ islandId: component.islandId, record }, null, 2)); 138 } else { 139 console.log(`Title: ${record.analysis.title}`); 140 console.log(`Source: ${record.source.uri}`); 141 console.log(`Connections: ${record.connections.length}`); 142 console.log(`Themes: ${record.analysis.themes.join(', ')}`); 143 console.log(`\nSynthesis:\n${record.analysis.synthesis}`); 144 console.log(`\nConnections:`); 145 for (const c of record.connections) { 146 console.log(` ${c.source} -> ${c.target} ${c.connectionType ?? ''} (${c.note})`); 147 } 148 } 149 db.close(); 150 return; 151 } 152 153 // Publish to PDS 154 if (!PDS_IDENTIFIER || !PDS_APP_PASSWORD) { 155 console.error('PDS_IDENTIFIER and PDS_APP_PASSWORD (or BLUESKY_IDENTIFIER/BLUESKY_APP_PASSWORD) must be set'); 156 process.exit(1); 157 } 158 159 const agent = new AtpAgent({ service: PDS_SERVICE }); 160 await agent.login({ identifier: PDS_IDENTIFIER, password: PDS_APP_PASSWORD }); 161 162 const result = await agent.api.com.atproto.repo.createRecord({ 163 repo: agent.session!.did, 164 collection: 'org.latha.island', 165 record, 166 }); 167 168 const atUri = result.data.uri; 169 console.error(`Published: ${atUri}`); 170 171 if (wantJson) { 172 console.log(JSON.stringify({ islandId: component.islandId, atUri, record }, null, 2)); 173 } else { 174 console.log(atUri); 175 } 176 177 db.close(); 178} 179 180main().catch((err) => { 181 console.error('Fatal:', err instanceof Error ? err.message : String(err)); 182 process.exit(1); 183});