OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore commits / 38cd1dc

Add Phase 1 Pear terminal app implementation
Implements P2P social network terminal client with: - keet-identity-key for persistent HD keypair identities - Per-user writable Hypercore feeds - Sparse replication (only followed users) - Hyperswarm P2P networking - Commands: init, post, follow, unfollow, timeline, whoami, follows Architecture: - Each user has their own writable feed - Following = joining Hyperswarm topic + replicating read-only - Timeline merges all followed feeds by timestamp - Works with both Node.js and Pear runtime Tested commands: - init: Creates persistent identity with keet-identity-key - post: Appends messages to personal feed - timeline: Displays merged timeline from all feeds - whoami: Shows public key and feed keys Next phases: - Relay integration for web browser access - Web UI for browser-based access - Native desktop GUI research Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
38cd1dc832471eabdf9da24881644ddacdc4085a
by plankatron <fleeky@gmail.com> · 2026-01-20T18:11:39.000Z

Changed files (3) +497 -0

pear-app/.pearignore +20 -0
……@@ -0,0 +1,20 @@
1# Pear stage ignore file
2# CRITICAL: Do NOT ignore node_modules (module resolution bug under pear://)
3
4# Development files
5.git
6.gitignore
7*.log
8.DS_Store
9
10# Test files
11test/
12
13# Documentation
14README.md
15docs/
16
17# Build artifacts
18*.swp
19*.swo
20*~
pear-app/index.js +443 -0
……@@ -0,0 +1,443 @@
1#!/usr/bin/env node
2/** @typedef {import('pear-interface')} */ /* global Pear */
3
4/**
5 * Wirecore Pear App - P2P Social Network Terminal Client
6 *
7 * Architecture:
8 * - Each user has their own writable Hypercore feed
9 * - Following = joining their Hyperswarm topic and replicating read-only
10 * - Timeline = merge all followed feeds by timestamp
11 * - Sparse replication = only followed users, not entire network
12 */
13
14import Corestore from 'corestore'
15import Hyperswarm from 'hyperswarm'
16import Hyperbee from 'hyperbee'
17import crypto from 'hypercore-crypto'
18import b4a from 'b4a'
19import goodbye from 'graceful-goodbye'
20import IdentityKey from 'keet-identity-key'
21
22// Check if running in Pear or Node
23const isPear = typeof Pear !== 'undefined'
24
25// Configuration
26const STORAGE_PATH = isPear && Pear.config.storage ? Pear.config.storage : './wirecore-storage'
27
28// Global state
29let store = null
30let swarm = null
31let myFeed = null
32let metaBee = null
33let identity = null
34let follows = new Map() // discoveryKey -> { feed, publicKey, name }
35
36/**
37 * Initialize Corestore and load state (without starting swarm)
38 */
39async function loadState() {
40 if (store) return // Already loaded
41
42 store = new Corestore(STORAGE_PATH)
43 await store.ready()
44
45 // Load identity
46 const identityCore = store.get({ name: 'identity' })
47 await identityCore.ready()
48
49 if (identityCore.length === 0) {
50 return null // Not initialized
51 }
52
53 const identitySeed = await identityCore.get(0)
54 identity = await IdentityKey.from({ seed: identitySeed })
55
56 // Load personal feed
57 myFeed = store.get({ name: 'my-feed' })
58 await myFeed.ready()
59
60 // Load metadata
61 metaBee = new Hyperbee(store.get({ name: 'metadata' }), {
62 keyEncoding: 'utf-8',
63 valueEncoding: 'json'
64 })
65 await metaBee.ready()
66
67 return true
68}
69
70/**
71 * Start Hyperswarm networking
72 */
73async function startSwarm() {
74 if (swarm) return
75
76 swarm = new Hyperswarm()
77
78 swarm.on('connection', (socket, peerInfo) => {
79 console.log('🔗 Connected to peer')
80 store.replicate(socket)
81 })
82
83 // Announce our feed
84 const discovery = swarm.join(myFeed.discoveryKey, { server: true, client: false })
85 await discovery.flushed()
86
87 // Load and join followed feeds
88 await loadFollows()
89
90 // Graceful shutdown
91 goodbye(() => cleanup())
92}
93
94/**
95 * Load existing follows from metadata and join swarm topics
96 */
97async function loadFollows() {
98 const node = await metaBee.get('follows')
99 if (!node) return
100
101 const followList = node.value
102
103 for (const { publicKey, name } of followList) {
104 const pubKeyBuf = b4a.from(publicKey, 'hex')
105 const feedKey = crypto.discoveryKey(pubKeyBuf)
106
107 const feed = store.get(pubKeyBuf)
108 await feed.ready()
109
110 if (swarm) {
111 swarm.join(feedKey, { server: false, client: true })
112 await swarm.flush()
113 }
114
115 follows.set(b4a.toString(feedKey, 'hex'), {
116 feed,
117 publicKey,
118 name: name || publicKey.substring(0, 16)
119 })
120 }
121}
122
123/**
124 * Save follows to metadata
125 */
126async function saveFollows() {
127 const followList = []
128 for (const [discoveryKey, { publicKey, name }] of follows.entries()) {
129 followList.push({ publicKey, name })
130 }
131 await metaBee.put('follows', followList)
132}
133
134/**
135 * Cleanup on exit
136 */
137async function cleanup() {
138 if (swarm) {
139 await swarm.destroy()
140 swarm = null
141 }
142 if (store) {
143 await store.close()
144 store = null
145 }
146}
147
148/**
149 * Command: Initialize new identity
150 */
151async function cmdInit() {
152 const exists = await loadState()
153 if (exists) {
154 console.log('⚠️ Already initialized')
155 const publicKey = b4a.toString(identity.identityPublicKey, 'hex')
156 console.log('Your public key:', publicKey)
157 return
158 }
159
160 console.log('📝 Initializing Wirecore...')
161
162 // Create Corestore
163 store = new Corestore(STORAGE_PATH)
164 await store.ready()
165
166 // Generate new identity
167 const identitySeed = crypto.randomBytes(32)
168 const identityCore = store.get({ name: 'identity' })
169 await identityCore.ready()
170 await identityCore.append(identitySeed)
171
172 identity = await IdentityKey.from({ seed: identitySeed })
173 const publicKey = b4a.toString(identity.identityPublicKey, 'hex')
174
175 // Create personal feed
176 myFeed = store.get({ name: 'my-feed' })
177 await myFeed.ready()
178
179 // Create metadata
180 metaBee = new Hyperbee(store.get({ name: 'metadata' }), {
181 keyEncoding: 'utf-8',
182 valueEncoding: 'json'
183 })
184 await metaBee.ready()
185 await metaBee.put('follows', [])
186
187 console.log('✅ Wirecore initialized!')
188 console.log('Your public key:', publicKey)
189 console.log('Feed key:', b4a.toString(myFeed.key, 'hex'))
190}
191
192/**
193 * Command: Post a message
194 */
195async function cmdPost(message) {
196 await loadState()
197 if (!myFeed) {
198 console.error('❌ Not initialized. Run: init')
199 return
200 }
201
202 const timestamp = Date.now()
203 const entry = {
204 type: 'post',
205 text: message,
206 timestamp,
207 author: b4a.toString(myFeed.key, 'hex')
208 }
209
210 await myFeed.append(Buffer.from(JSON.stringify(entry)))
211 console.log('✅ Posted:', message)
212}
213
214/**
215 * Command: Follow a user
216 */
217async function cmdFollow(publicKeyHex, name) {
218 await loadState()
219 if (!metaBee) {
220 console.error('❌ Not initialized. Run: init')
221 return
222 }
223
224 const pubKeyBuf = b4a.from(publicKeyHex, 'hex')
225 const feedKey = crypto.discoveryKey(pubKeyBuf)
226 const discoveryKeyHex = b4a.toString(feedKey, 'hex')
227
228 if (follows.has(discoveryKeyHex)) {
229 console.log('⚠️ Already following this user')
230 return
231 }
232
233 const feed = store.get(pubKeyBuf)
234 await feed.ready()
235
236 follows.set(discoveryKeyHex, {
237 feed,
238 publicKey: publicKeyHex,
239 name: name || publicKeyHex.substring(0, 16)
240 })
241
242 await saveFollows()
243
244 console.log(`✅ Following: ${name || publicKeyHex.substring(0, 16)}`)
245 console.log(` Feed key: ${publicKeyHex}`)
246}
247
248/**
249 * Command: Unfollow a user
250 */
251async function cmdUnfollow(publicKeyHex) {
252 await loadState()
253 if (!metaBee) {
254 console.error('❌ Not initialized. Run: init')
255 return
256 }
257
258 const pubKeyBuf = b4a.from(publicKeyHex, 'hex')
259 const feedKey = crypto.discoveryKey(pubKeyBuf)
260 const discoveryKeyHex = b4a.toString(feedKey, 'hex')
261
262 if (!follows.has(discoveryKeyHex)) {
263 console.log('⚠️ Not following this user')
264 return
265 }
266
267 follows.delete(discoveryKeyHex)
268 await saveFollows()
269
270 console.log(`✅ Unfollowed: ${publicKeyHex.substring(0, 16)}...`)
271}
272
273/**
274 * Command: View timeline
275 */
276async function cmdTimeline(limit = 20) {
277 await loadState()
278 if (!myFeed) {
279 console.error('❌ Not initialized. Run: init')
280 return
281 }
282
283 // Start swarm to fetch remote data
284 console.log('📡 Connecting to network...')
285 await startSwarm()
286
287 // Wait a bit for connections
288 await new Promise(resolve => setTimeout(resolve, 2000))
289
290 const messages = []
291
292 // Read own feed
293 for (let i = Math.max(0, myFeed.length - limit); i < myFeed.length; i++) {
294 try {
295 const block = await myFeed.get(i, { wait: false })
296 if (!block) continue
297 const entry = JSON.parse(block.toString())
298 messages.push({ ...entry, isOwn: true })
299 } catch (err) {
300 // Skip
301 }
302 }
303
304 // Read followed feeds
305 for (const [discoveryKey, { feed, name, publicKey }] of follows.entries()) {
306 const feedLimit = Math.min(limit, feed.length)
307 for (let i = Math.max(0, feed.length - feedLimit); i < feed.length; i++) {
308 try {
309 const block = await feed.get(i, { wait: false, timeout: 1000 })
310 if (!block) continue
311 const entry = JSON.parse(block.toString())
312 messages.push({ ...entry, author: name || publicKey.substring(0, 16), isOwn: false })
313 } catch (err) {
314 // Sparse replication may not have all blocks yet
315 continue
316 }
317 }
318 }
319
320 // Sort by timestamp
321 messages.sort((a, b) => b.timestamp - a.timestamp)
322
323 // Display
324 console.log(`\n📰 Timeline (${messages.slice(0, limit).length} messages):\n`)
325 for (const msg of messages.slice(0, limit)) {
326 const time = new Date(msg.timestamp).toLocaleString()
327 const prefix = msg.isOwn ? '💬 You' : `👤 ${msg.author}`
328 console.log(`${prefix} (${time}):`)
329 console.log(` ${msg.text}`)
330 console.log('')
331 }
332}
333
334/**
335 * Command: Show own public key
336 */
337async function cmdWhoami() {
338 await loadState()
339 if (!identity) {
340 console.error('❌ Not initialized. Run: init')
341 return
342 }
343
344 console.log('Your public key:', b4a.toString(identity.identityPublicKey, 'hex'))
345 console.log('Feed key:', b4a.toString(myFeed.key, 'hex'))
346 console.log('Discovery key:', b4a.toString(myFeed.discoveryKey, 'hex'))
347}
348
349/**
350 * Command: List followed users
351 */
352async function cmdFollows() {
353 await loadState()
354 if (!metaBee) {
355 console.error('❌ Not initialized. Run: init')
356 return
357 }
358
359 await loadFollows()
360
361 if (follows.size === 0) {
362 console.log('📭 Not following anyone yet')
363 return
364 }
365
366 console.log(`\n👥 Following ${follows.size} users:\n`)
367 for (const [discoveryKey, { publicKey, name, feed }] of follows.entries()) {
368 console.log(` ${name || publicKey.substring(0, 16)}`)
369 console.log(` Feed key: ${publicKey}`)
370 console.log(` Messages: ${feed.length}`)
371 console.log('')
372 }
373}
374
375/**
376 * Main CLI handler
377 */
378async function main() {
379 const args = isPear ? (Pear.config.args || []) : process.argv.slice(2)
380
381 if (args.length === 0) {
382 console.log('Usage: ' + (isPear ? 'pear run .' : 'node index.js') + ' <command> [args...]')
383 console.log('')
384 console.log('Commands:')
385 console.log(' init - Initialize Wirecore')
386 console.log(' post <message> - Post a message')
387 console.log(' follow <pubkey> [name] - Follow a user')
388 console.log(' unfollow <pubkey> - Unfollow a user')
389 console.log(' timeline [limit] - View timeline')
390 console.log(' whoami - Show your public key')
391 console.log(' follows - List followed users')
392 return
393 }
394
395 const [command, ...params] = args
396
397 try {
398 switch (command) {
399 case 'init':
400 await cmdInit()
401 break
402
403 case 'post':
404 await cmdPost(params.join(' '))
405 break
406
407 case 'follow':
408 await cmdFollow(params[0], params[1])
409 break
410
411 case 'unfollow':
412 await cmdUnfollow(params[0])
413 break
414
415 case 'timeline':
416 await cmdTimeline(parseInt(params[0]) || 20)
417 break
418
419 case 'whoami':
420 await cmdWhoami()
421 break
422
423 case 'follows':
424 await cmdFollows()
425 break
426
427 default:
428 console.log('Unknown command:', command)
429 console.log('Run without arguments to see usage')
430 }
431 } catch (err) {
432 console.error('❌ Error:', err.message)
433 console.error(err.stack)
434 } finally {
435 await cleanup()
436 }
437}
438
439// Run main
440main().catch(err => {
441 console.error('Fatal error:', err)
442 process.exit(1)
443})
pear-app/package.json +34 -0
……@@ -0,0 +1,34 @@
1{
2 "name": "wirecore-pear",
3 "version": "1.0.0",
4 "description": "Wirecore P2P social network - Pear terminal app",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "wirecore-pear",
9 "type": "terminal",
10 "gui": {
11 "backgroundColor": "#1e1e1e"
12 }
13 },
14 "scripts": {
15 "dev": "pear run -d .",
16 "stage": "pear stage .",
17 "seed": "pear seed .",
18 "test": "brittle test/*.js"
19 },
20 "dependencies": {
21 "keet-identity-key": "^3.1.0",
22 "corestore": "^6.18.4",
23 "hypercore": "^10.37.8",
24 "hyperswarm": "^4.8.2",
25 "hyperbee": "^2.20.1",
26 "hypercore-crypto": "^3.4.1",
27 "b4a": "^1.6.6",
28 "compact-encoding": "^2.15.0",
29 "graceful-goodbye": "^1.3.0"
30 },
31 "devDependencies": {
32 "brittle": "^3.7.0"
33 }
34}