walkie-sh 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vikasprogrammer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # walkie
2
+
3
+ P2P communication for AI agents. No server. No setup. Just talk.
4
+
5
+ ```
6
+ npm install -g walkie-sh
7
+ ```
8
+
9
+ ## What is this?
10
+
11
+ AI agents are isolated. When two agents need to collaborate, there's no simple way for them to talk directly. Walkie gives them a walkie-talkie — pick a channel, share a secret, and they find each other automatically over the internet.
12
+
13
+ - **No server** — peer-to-peer via Hyperswarm DHT
14
+ - **No setup** — one install, two commands, agents are talking
15
+ - **Works anywhere** — same machine or different continents
16
+ - **Encrypted** — Noise protocol, secure by default
17
+ - **Agent-native** — CLI-first, any agent that runs shell commands can use it
18
+
19
+ ## Quick start
20
+
21
+ **Agent A** (on any machine):
22
+ ```bash
23
+ walkie create ops-room --secret mysecret
24
+ walkie send ops-room "task complete, results ready"
25
+ ```
26
+
27
+ **Agent B** (on any other machine):
28
+ ```bash
29
+ walkie join ops-room --secret mysecret
30
+ walkie read ops-room
31
+ # [14:30:05] a1b2c3d4: task complete, results ready
32
+ ```
33
+
34
+ ## Commands
35
+
36
+ ```
37
+ walkie create <channel> -s <secret> Create a channel
38
+ walkie join <channel> -s <secret> Join a channel
39
+ walkie send <channel> "message" Send a message
40
+ walkie read <channel> Read pending messages
41
+ walkie read <channel> --wait Block until a message arrives
42
+ walkie status Show active channels & peers
43
+ walkie leave <channel> Leave a channel
44
+ walkie stop Stop the daemon
45
+ ```
46
+
47
+ ## How it works
48
+
49
+ ```
50
+ Agent A Agent B
51
+ ┌─────────────┐ ┌─────────────┐
52
+ │ walkie CLI │ │ walkie CLI │
53
+ │ ↕ │ │ ↕ │
54
+ │ daemon │◄──── P2P ──────────►│ daemon │
55
+ │ (hyperswarm) │ encrypted via DHT │ (hyperswarm) │
56
+ └─────────────┘ └─────────────┘
57
+ ```
58
+
59
+ 1. Channel name + secret are hashed into a 32-byte topic
60
+ 2. Both agents announce/lookup the topic on the Hyperswarm DHT
61
+ 3. DHT connects them directly — no relay, no server
62
+ 4. All communication is encrypted via the Noise protocol
63
+ 5. A background daemon maintains connections so CLI commands are instant
64
+
65
+ ## Use cases
66
+
67
+ - **Multi-agent collaboration** — agents coordinate tasks in real-time
68
+ - **Agent delegation** — one agent sends work to another and waits for results
69
+ - **Agent monitoring** — watch what your agents are doing from another terminal
70
+ - **Cross-machine pipelines** — chain agents across different servers
71
+
72
+ ## License
73
+
74
+ MIT
package/bin/walkie.js ADDED
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander')
4
+ const { request } = require('../src/client')
5
+
6
+ program
7
+ .name('walkie')
8
+ .description('P2P communication CLI for AI agents')
9
+ .version('0.1.0')
10
+
11
+ program
12
+ .command('create <channel>')
13
+ .description('Create a channel and wait for peers')
14
+ .requiredOption('-s, --secret <secret>', 'Shared secret')
15
+ .action(async (channel, opts) => {
16
+ try {
17
+ const resp = await request({ action: 'join', channel, secret: opts.secret })
18
+ if (resp.ok) {
19
+ console.log(`Channel "${channel}" created. Listening for peers...`)
20
+ } else {
21
+ console.error(`Error: ${resp.error}`)
22
+ process.exit(1)
23
+ }
24
+ } catch (e) {
25
+ console.error(`Error: ${e.message}`)
26
+ process.exit(1)
27
+ }
28
+ })
29
+
30
+ program
31
+ .command('join <channel>')
32
+ .description('Join an existing channel')
33
+ .requiredOption('-s, --secret <secret>', 'Shared secret')
34
+ .action(async (channel, opts) => {
35
+ try {
36
+ const resp = await request({ action: 'join', channel, secret: opts.secret })
37
+ if (resp.ok) {
38
+ console.log(`Joined channel "${channel}"`)
39
+ } else {
40
+ console.error(`Error: ${resp.error}`)
41
+ process.exit(1)
42
+ }
43
+ } catch (e) {
44
+ console.error(`Error: ${e.message}`)
45
+ process.exit(1)
46
+ }
47
+ })
48
+
49
+ program
50
+ .command('send <channel> <message>')
51
+ .description('Send a message to a channel')
52
+ .action(async (channel, message) => {
53
+ try {
54
+ const resp = await request({ action: 'send', channel, message })
55
+ if (resp.ok) {
56
+ console.log(`Sent (delivered to ${resp.delivered} peer${resp.delivered !== 1 ? 's' : ''})`)
57
+ } else {
58
+ console.error(`Error: ${resp.error}`)
59
+ process.exit(1)
60
+ }
61
+ } catch (e) {
62
+ console.error(`Error: ${e.message}`)
63
+ process.exit(1)
64
+ }
65
+ })
66
+
67
+ program
68
+ .command('read <channel>')
69
+ .description('Read pending messages from a channel')
70
+ .option('-w, --wait', 'Block until a message arrives')
71
+ .option('-t, --timeout <seconds>', 'Timeout for --wait in seconds', '30')
72
+ .action(async (channel, opts) => {
73
+ try {
74
+ const cmd = { action: 'read', channel }
75
+ if (opts.wait) {
76
+ cmd.wait = true
77
+ cmd.timeout = parseInt(opts.timeout, 10)
78
+ }
79
+ const timeout = opts.wait ? (parseInt(opts.timeout, 10) + 5) * 1000 : 10000
80
+ const resp = await request(cmd, timeout)
81
+ if (resp.ok) {
82
+ if (resp.messages.length === 0) {
83
+ console.log('No new messages')
84
+ } else {
85
+ for (const msg of resp.messages) {
86
+ const time = new Date(msg.ts).toLocaleTimeString()
87
+ console.log(`[${time}] ${msg.from}: ${msg.data}`)
88
+ }
89
+ }
90
+ } else {
91
+ console.error(`Error: ${resp.error}`)
92
+ process.exit(1)
93
+ }
94
+ } catch (e) {
95
+ console.error(`Error: ${e.message}`)
96
+ process.exit(1)
97
+ }
98
+ })
99
+
100
+ program
101
+ .command('leave <channel>')
102
+ .description('Leave a channel')
103
+ .action(async (channel) => {
104
+ try {
105
+ const resp = await request({ action: 'leave', channel })
106
+ if (resp.ok) {
107
+ console.log(`Left channel "${channel}"`)
108
+ } else {
109
+ console.error(`Error: ${resp.error}`)
110
+ process.exit(1)
111
+ }
112
+ } catch (e) {
113
+ console.error(`Error: ${e.message}`)
114
+ process.exit(1)
115
+ }
116
+ })
117
+
118
+ program
119
+ .command('status')
120
+ .description('Show active channels and peers')
121
+ .action(async () => {
122
+ try {
123
+ const resp = await request({ action: 'status' })
124
+ if (resp.ok) {
125
+ console.log(`Daemon ID: ${resp.daemonId}`)
126
+ const entries = Object.entries(resp.channels)
127
+ if (entries.length === 0) {
128
+ console.log('No active channels')
129
+ } else {
130
+ for (const [name, info] of entries) {
131
+ console.log(` #${name} — ${info.peers} peer(s), ${info.buffered} buffered`)
132
+ }
133
+ }
134
+ } else {
135
+ console.error(`Error: ${resp.error}`)
136
+ process.exit(1)
137
+ }
138
+ } catch (e) {
139
+ console.error(`Error: ${e.message}`)
140
+ process.exit(1)
141
+ }
142
+ })
143
+
144
+ program
145
+ .command('stop')
146
+ .description('Stop the walkie daemon')
147
+ .action(async () => {
148
+ try {
149
+ await request({ action: 'stop' })
150
+ console.log('Daemon stopped')
151
+ } catch {
152
+ console.log('Daemon is not running')
153
+ }
154
+ })
155
+
156
+ program.parse()
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "walkie-sh",
3
+ "version": "0.1.0",
4
+ "description": "P2P communication CLI for AI agents. No server. No setup. Just talk.",
5
+ "homepage": "https://walkie.sh",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/vikasprogrammer/walkie.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/vikasprogrammer/walkie/issues"
12
+ },
13
+ "license": "MIT",
14
+ "author": "vikasprogrammer",
15
+ "keywords": [
16
+ "p2p",
17
+ "agent",
18
+ "ai",
19
+ "communication",
20
+ "walkie-talkie",
21
+ "hyperswarm",
22
+ "cli",
23
+ "llm",
24
+ "multi-agent"
25
+ ],
26
+ "bin": {
27
+ "walkie": "./bin/walkie.js"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "src/"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "dependencies": {
37
+ "commander": "^12.0.0",
38
+ "hyperswarm": "^4.0.0"
39
+ }
40
+ }
package/src/client.js ADDED
@@ -0,0 +1,84 @@
1
+ const net = require('net')
2
+ const path = require('path')
3
+ const { spawn } = require('child_process')
4
+ const fs = require('fs')
5
+
6
+ const WALKIE_DIR = process.env.WALKIE_DIR || path.join(process.env.HOME, '.walkie')
7
+ const SOCKET_PATH = path.join(WALKIE_DIR, 'daemon.sock')
8
+
9
+ function connect() {
10
+ return new Promise((resolve, reject) => {
11
+ const sock = net.connect(SOCKET_PATH)
12
+ sock.on('connect', () => resolve(sock))
13
+ sock.on('error', reject)
14
+ })
15
+ }
16
+
17
+ function sendCommand(sock, cmd, timeout = 60000) {
18
+ return new Promise((resolve, reject) => {
19
+ let buf = ''
20
+ const timer = setTimeout(() => {
21
+ sock.removeListener('data', onData)
22
+ reject(new Error('Command timed out'))
23
+ }, timeout)
24
+
25
+ const onData = (data) => {
26
+ buf += data.toString()
27
+ const idx = buf.indexOf('\n')
28
+ if (idx !== -1) {
29
+ clearTimeout(timer)
30
+ sock.removeListener('data', onData)
31
+ try {
32
+ resolve(JSON.parse(buf.slice(0, idx)))
33
+ } catch (e) {
34
+ reject(e)
35
+ }
36
+ }
37
+ }
38
+ sock.on('data', onData)
39
+ sock.write(JSON.stringify(cmd) + '\n')
40
+ })
41
+ }
42
+
43
+ async function ensureDaemon() {
44
+ // Try connecting to existing daemon
45
+ try {
46
+ const sock = await connect()
47
+ const resp = await sendCommand(sock, { action: 'ping' })
48
+ sock.destroy()
49
+ if (resp.ok) return
50
+ } catch {}
51
+
52
+ // Spawn daemon
53
+ fs.mkdirSync(WALKIE_DIR, { recursive: true })
54
+
55
+ const daemonScript = path.join(__dirname, 'daemon.js')
56
+ const child = spawn(process.execPath, [daemonScript], {
57
+ detached: true,
58
+ stdio: 'ignore'
59
+ })
60
+ child.unref()
61
+
62
+ // Poll until ready
63
+ for (let i = 0; i < 50; i++) {
64
+ await new Promise(r => setTimeout(r, 200))
65
+ try {
66
+ const sock = await connect()
67
+ const resp = await sendCommand(sock, { action: 'ping' })
68
+ sock.destroy()
69
+ if (resp.ok) return
70
+ } catch {}
71
+ }
72
+
73
+ throw new Error('Failed to start walkie daemon')
74
+ }
75
+
76
+ async function request(cmd, timeout) {
77
+ await ensureDaemon()
78
+ const sock = await connect()
79
+ const resp = await sendCommand(sock, cmd, timeout)
80
+ sock.destroy()
81
+ return resp
82
+ }
83
+
84
+ module.exports = { request }
package/src/crypto.js ADDED
@@ -0,0 +1,14 @@
1
+ const crypto = require('crypto')
2
+
3
+ function deriveTopic(channel, secret) {
4
+ return crypto.createHash('sha256')
5
+ .update(`walkie:${channel}:${secret}`)
6
+ .digest()
7
+ }
8
+
9
+ function agentId() {
10
+ // Generate a short random ID for this daemon instance
11
+ return crypto.randomBytes(4).toString('hex')
12
+ }
13
+
14
+ module.exports = { deriveTopic, agentId }
package/src/daemon.js ADDED
@@ -0,0 +1,299 @@
1
+ const Hyperswarm = require('hyperswarm')
2
+ const net = require('net')
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { deriveTopic, agentId } = require('./crypto')
6
+
7
+ const WALKIE_DIR = process.env.WALKIE_DIR || path.join(process.env.HOME, '.walkie')
8
+ const SOCKET_PATH = path.join(WALKIE_DIR, 'daemon.sock')
9
+ const PID_FILE = path.join(WALKIE_DIR, 'daemon.pid')
10
+ const LOG_FILE = path.join(WALKIE_DIR, 'daemon.log')
11
+
12
+ function log(...args) {
13
+ const line = `[${new Date().toISOString()}] ${args.join(' ')}\n`
14
+ try { fs.appendFileSync(LOG_FILE, line) } catch {}
15
+ }
16
+
17
+ class WalkieDaemon {
18
+ constructor() {
19
+ this.id = agentId()
20
+ this.swarm = new Hyperswarm()
21
+ this.channels = new Map() // name -> { topicHex, discovery, peers: Set, messages: [], waiters: [] }
22
+ this.peers = new Map() // remoteKey hex -> { conn, channels: Set }
23
+ }
24
+
25
+ async start() {
26
+ fs.mkdirSync(WALKIE_DIR, { recursive: true })
27
+ fs.writeFileSync(PID_FILE, process.pid.toString())
28
+
29
+ // Clean stale socket
30
+ try { fs.unlinkSync(SOCKET_PATH) } catch {}
31
+
32
+ // IPC server for CLI commands
33
+ const server = net.createServer(sock => this._onIPC(sock))
34
+ server.listen(SOCKET_PATH)
35
+
36
+ // P2P connections
37
+ this.swarm.on('connection', (conn, info) => this._onPeer(conn, info))
38
+
39
+ process.on('SIGTERM', () => this.shutdown())
40
+ process.on('SIGINT', () => this.shutdown())
41
+
42
+ log(`Daemon started id=${this.id} pid=${process.pid}`)
43
+ }
44
+
45
+ // ── IPC (CLI <-> Daemon) ──────────────────────────────────────────
46
+
47
+ _onIPC(socket) {
48
+ let buf = ''
49
+ socket.on('data', data => {
50
+ buf += data.toString()
51
+ let idx
52
+ while ((idx = buf.indexOf('\n')) !== -1) {
53
+ const line = buf.slice(0, idx)
54
+ buf = buf.slice(idx + 1)
55
+ if (line.trim()) {
56
+ try {
57
+ this._exec(JSON.parse(line), socket)
58
+ } catch (e) {
59
+ socket.write(JSON.stringify({ ok: false, error: e.message }) + '\n')
60
+ }
61
+ }
62
+ }
63
+ })
64
+ socket.on('error', () => {})
65
+ }
66
+
67
+ async _exec(cmd, socket) {
68
+ const reply = d => socket.write(JSON.stringify(d) + '\n')
69
+
70
+ try {
71
+ switch (cmd.action) {
72
+ case 'join': {
73
+ await this._joinChannel(cmd.channel, cmd.secret)
74
+ reply({ ok: true, channel: cmd.channel })
75
+ break
76
+ }
77
+ case 'send': {
78
+ const count = this._send(cmd.channel, cmd.message)
79
+ reply({ ok: true, delivered: count })
80
+ break
81
+ }
82
+ case 'read': {
83
+ const ch = this.channels.get(cmd.channel)
84
+ if (!ch) { reply({ ok: false, error: `Not in channel: ${cmd.channel}` }); return }
85
+
86
+ // If messages available or no wait requested, return immediately
87
+ if (ch.messages.length > 0 || !cmd.wait) {
88
+ reply({ ok: true, messages: ch.messages.splice(0) })
89
+ return
90
+ }
91
+
92
+ // Wait mode: hold connection until a message arrives or timeout
93
+ const timeout = (cmd.timeout || 30) * 1000
94
+ const timer = setTimeout(() => {
95
+ ch.waiters = ch.waiters.filter(w => w !== waiter)
96
+ reply({ ok: true, messages: [] })
97
+ }, timeout)
98
+
99
+ const waiter = (msgs) => {
100
+ clearTimeout(timer)
101
+ reply({ ok: true, messages: msgs })
102
+ }
103
+ ch.waiters.push(waiter)
104
+ break
105
+ }
106
+ case 'leave': {
107
+ await this._leaveChannel(cmd.channel)
108
+ reply({ ok: true })
109
+ break
110
+ }
111
+ case 'status': {
112
+ const channels = {}
113
+ for (const [name, ch] of this.channels) {
114
+ channels[name] = { peers: ch.peers.size, buffered: ch.messages.length }
115
+ }
116
+ reply({ ok: true, channels, daemonId: this.id })
117
+ break
118
+ }
119
+ case 'ping': {
120
+ reply({ ok: true })
121
+ break
122
+ }
123
+ case 'stop': {
124
+ reply({ ok: true })
125
+ await this.shutdown()
126
+ break
127
+ }
128
+ default:
129
+ reply({ ok: false, error: `Unknown action: ${cmd.action}` })
130
+ }
131
+ } catch (e) {
132
+ reply({ ok: false, error: e.message })
133
+ }
134
+ }
135
+
136
+ // ── Channel management ────────────────────────────────────────────
137
+
138
+ async _joinChannel(name, secret) {
139
+ if (this.channels.has(name)) return
140
+
141
+ const topic = deriveTopic(name, secret)
142
+ const topicHex = topic.toString('hex')
143
+ log(`Joining channel "${name}" topic=${topicHex.slice(0, 16)}...`)
144
+ const discovery = this.swarm.join(topic, { server: true, client: true })
145
+ await discovery.flushed()
146
+ log(`Channel "${name}" flushed, discoverable`)
147
+
148
+ this.channels.set(name, {
149
+ topicHex,
150
+ discovery,
151
+ peers: new Set(),
152
+ messages: [],
153
+ waiters: []
154
+ })
155
+
156
+ // Re-announce topics to already-connected peers (fixes race condition
157
+ // where peer connects before channel is registered)
158
+ this._reannounce()
159
+ }
160
+
161
+ _reannounce() {
162
+ const topics = Array.from(this.channels.values()).map(ch => ch.topicHex)
163
+ const hello = JSON.stringify({ t: 'hello', topics, id: this.id }) + '\n'
164
+ for (const [remoteKey, peer] of this.peers) {
165
+ if (peer.conn?.writable) {
166
+ log(`Re-announcing ${topics.length} topic(s) to ${remoteKey.slice(0, 12)}`)
167
+ peer.conn.write(hello)
168
+ }
169
+ // Also match this peer against our newly added channels
170
+ // (handles case where we received their hello before our channel was ready)
171
+ if (peer.knownTopics) {
172
+ for (const [name, ch] of this.channels) {
173
+ if (peer.knownTopics.has(ch.topicHex) && !ch.peers.has(remoteKey)) {
174
+ ch.peers.add(remoteKey)
175
+ peer.channels.add(name)
176
+ log(`Late-matched channel "${name}" with peer ${remoteKey.slice(0, 12)}`)
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ async _leaveChannel(name) {
184
+ const ch = this.channels.get(name)
185
+ if (!ch) return
186
+ await ch.discovery.destroy()
187
+ this.channels.delete(name)
188
+ }
189
+
190
+ // ── P2P peer handling ─────────────────────────────────────────────
191
+
192
+ _onPeer(conn, info) {
193
+ const remoteKey = conn.remotePublicKey.toString('hex')
194
+ log(`Peer connected: ${remoteKey.slice(0, 12)}...`)
195
+
196
+ const peer = { conn, channels: new Set(), buf: '' }
197
+ this.peers.set(remoteKey, peer)
198
+
199
+ // Send handshake: our active topic list
200
+ const topics = Array.from(this.channels.values()).map(ch => ch.topicHex)
201
+ log(`Sending hello with ${topics.length} topic(s)`)
202
+ conn.write(JSON.stringify({ t: 'hello', topics, id: this.id }) + '\n')
203
+
204
+ conn.on('data', data => {
205
+ peer.buf += data.toString()
206
+ let idx
207
+ while ((idx = peer.buf.indexOf('\n')) !== -1) {
208
+ const line = peer.buf.slice(0, idx)
209
+ peer.buf = peer.buf.slice(idx + 1)
210
+ if (line.trim()) {
211
+ try { this._onPeerMsg(remoteKey, JSON.parse(line)) } catch {}
212
+ }
213
+ }
214
+ })
215
+
216
+ conn.on('close', () => {
217
+ for (const [, ch] of this.channels) ch.peers.delete(remoteKey)
218
+ this.peers.delete(remoteKey)
219
+ })
220
+
221
+ conn.on('error', () => conn.destroy())
222
+ }
223
+
224
+ _onPeerMsg(remoteKey, msg) {
225
+ const peer = this.peers.get(remoteKey)
226
+ if (!peer) return
227
+
228
+ if (msg.t === 'hello') {
229
+ const theirTopics = new Set(msg.topics || [])
230
+ peer.knownTopics = theirTopics // Store for late-matching
231
+ log(`Got hello from ${remoteKey.slice(0, 12)} with ${theirTopics.size} topic(s)`)
232
+ for (const [name, ch] of this.channels) {
233
+ if (theirTopics.has(ch.topicHex) && !ch.peers.has(remoteKey)) {
234
+ ch.peers.add(remoteKey)
235
+ peer.channels.add(name)
236
+ log(`Matched channel "${name}" with peer ${remoteKey.slice(0, 12)}`)
237
+ }
238
+ }
239
+ return
240
+ }
241
+
242
+ if (msg.t === 'msg') {
243
+ for (const [name, ch] of this.channels) {
244
+ if (ch.topicHex === msg.topic) {
245
+ const entry = { from: msg.id || remoteKey.slice(0, 8), data: msg.data, ts: msg.ts }
246
+
247
+ // If someone is waiting, deliver directly
248
+ if (ch.waiters.length > 0) {
249
+ const waiter = ch.waiters.shift()
250
+ waiter([entry])
251
+ } else {
252
+ ch.messages.push(entry)
253
+ }
254
+ break
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ // ── Send ──────────────────────────────────────────────────────────
261
+
262
+ _send(channelName, message) {
263
+ const ch = this.channels.get(channelName)
264
+ if (!ch) throw new Error(`Not in channel: ${channelName}`)
265
+
266
+ const payload = JSON.stringify({
267
+ t: 'msg',
268
+ topic: ch.topicHex,
269
+ data: message,
270
+ id: this.id,
271
+ ts: Date.now()
272
+ }) + '\n'
273
+
274
+ let count = 0
275
+ for (const remoteKey of ch.peers) {
276
+ const peer = this.peers.get(remoteKey)
277
+ if (peer?.conn?.writable) {
278
+ peer.conn.write(payload)
279
+ count++
280
+ }
281
+ }
282
+ return count
283
+ }
284
+
285
+ // ── Shutdown ──────────────────────────────────────────────────────
286
+
287
+ async shutdown() {
288
+ try { fs.unlinkSync(SOCKET_PATH) } catch {}
289
+ try { fs.unlinkSync(PID_FILE) } catch {}
290
+ await this.swarm.destroy()
291
+ process.exit(0)
292
+ }
293
+ }
294
+
295
+ const daemon = new WalkieDaemon()
296
+ daemon.start().catch(e => {
297
+ console.error('Failed to start daemon:', e.message)
298
+ process.exit(1)
299
+ })