walkie — P2P communication CLI for AI agents. npm package: walkie-sh.
bin/walkie.js— CLI entry point (commander). Version is here AND inpackage.json(keep in sync)src/api.js— Programmatic API (require('walkie-sh')). Exportslisten()andsend(). Uses daemon IPC under the hood.src/daemon.js— background daemon managing Hyperswarm P2P + local subscriber routingsrc/client.js— IPC client, handles daemon auto-start and stale socket cleanupsrc/crypto.js— topic derivation (SHA-256 of channel+secret)src/cli-utils.js— identity resolution (WALKIE_ID>~/.walkie/config.json> terminal session), channel arg parsing, message filterssrc/slack.js— Slack <-> walkie bridge over Socket Mode (walkie slack)src/web.js— HTTP + WebSocket server bridging browser clients to daemonsrc/web-ui.js— exports HTML string for web chat UI (minimal, terminal-style)
package.json "main" points to src/api.js. Node apps can require('walkie-sh'):
const walkie = require('walkie-sh')
// Listen on a channel (EventEmitter — emits 'message' and 'error')
const ch = await walkie.listen('mychannel:secret', { id: 'mybot' })
ch.on('message', async (msg) => {
// msg: { from: string, data: string, ts: number, id: string }
await ch.send('response')
})
await ch.close()
// One-shot send (auto-joins if secret provided)
await walkie.send('mychannel:secret', 'hello', { id: 'sender' })listen()joins the channel, starts streaming viastreamMessages(), filters own messages, returns aWalkieChannel(EventEmitter +send()+close())send()auto-joins and fires a single message — good for scripts/CI- Both auto-start the daemon if not running
npm test — 99 automated tests using node:test (zero extra deps). Covers crypto, store, CLI utils, daemon IPC, web server, and programmatic API.
npm run test:p2p — manual P2P integration test (two daemons, Hyperswarm discovery, ~30s).
Manual same-machine test with two identities:
walkie stop
WALKIE_ID=alice walkie create test -s secret
WALKIE_ID=bob walkie join test -s secret
WALKIE_ID=alice walkie send test "hello"
WALKIE_ID=bob walkie read testPublishing runs in CI via npm trusted publishing (OIDC) — no NPM_TOKEN, no 2FA
code, nothing secret in the repo. .github/workflows/publish.yml fires on a v* tag,
checks the tag matches package.json, runs the suite, then publishes.
# bump version in package.json AND bin/walkie.js, commit, then:
git tag v1.6.5 && git push origin v1.6.5A published version sits in npm's automated review as Validating for a few
minutes and is genuinely 404 until it clears — that is not a failed publish. The
local npm cache also serves a stale packument right after, so npm i pkg@newversion
can claim "no matching version" while npm pack succeeds; use --prefer-online.
Requires a Trusted Publisher configured once at npmjs.com -> walkie-sh -> Settings:
user vikasprogrammer, repo walkie, workflow publish.yml, action npm publish.
Local npm publish still works but needs a 2FA code (npm publish --otp=NNNNNN) and
the vikasprogrammer account — not the machine's default login. Prefer the tag.
Background: npm is deprecating 2FA-bypass granular access tokens — they lost the 2FA skip for sensitive operations in August 2026 and lose direct publish around January 2027. Trusted publishing is the replacement, so do not add a publish token.
Remote uses SSH alias: git@github-vikasprogrammer:vikasprogrammer/walkie.git
- Skill source:
skills/walkie/ - The test copy at
/Users/vikas/Playground/random/walkie-test/.agents/skills/walkieis a symlink toskills/walkie/— editing the source is enough, there is nothing to sync
docs/index.html — single-page static site at walkie.sh
Deploy:
instapods deploy walkie --local docs --preset static- No
--asflag (removed in v1.3.0) - Identity resolves
WALKIE_IDenv >~/.walkie/config.json> terminal-session hash > none. Env-only was not viable:~/.bashrcreturns early for non-interactive shells, so agents silently fell back to an unstable per-session hash.walkie connectbootstraps the hostname;walkie whoami [--set]inspects and sets it - Auto-derived subscriber IDs from terminal session env vars (v1.2.0)
--waitblocks indefinitely,--timeoutis optional. With filters,--waitkeeps waiting through filtered-out traffic and--timeoutis the overall deadlinesendreports "Queued at ..." not "delivered" — reaching a peer daemon is not evidence that any agent consumed the message. The IPC reply keepsdeliveredfor the API/web client and addspeerDaemons/localSubscribers- Exit codes are meaningful, not just 0/1:
2not in channel,3send reached nobody,4read --waittimed out. Defined once insrc/cli-utils.jsasEXIT - A
--waitwake carries one message.read --drainpolls until the channel is quiet for--settlems (default 200). The first implementation read once and stopped on the first empty reply, which never worked: at the instant of a wake the buffer is empty by construction, so it always returned nothing in exactly the case it existed for. The algorithm lives incli-utils.drainAfterWakewith injected read/sleep/now so it is unit-testable on one machine — seetest/cli-utils.test.js send --await-replyis served by a daemon-side waiter (awaitReplyIPC action + a boundedrecentRepliescache per channel), matched at delivery time before subscriber buffers. The first implementation polled with--peekand failed whenever any other reader consumed the reply first — including the backgroundread --waitthe docs recommend — reporting "no reply" while the answer sat in another process's output. An ack that silently times out is worse than no ack--drainis a heuristic and must never be documented or described as a completeness guarantee. A flag that implies "you have everything" is worse than no flag, because an agent that knows it might be behind will re-read and one holding the flag will notstatusreportsbufferedByper subscriber — aggregatebufferedcannot answer "do I have unread?" when several identities share one daemon- Known quirk: a joiner receives its own
X joinedsystem notice, because the subscriber is registered before the announcement and_sendonly excludes the literalsystemsender. Filter with--no-system - Messages carry a per-channel
seq, monotonic in the order this daemon saw them. Deliberately local: independent daemons cannot agree a shared sequence without consensus, so a conditional send like--if-seen Nis not implementable here.send --warn-if-unreadand--await-replyare the locally decidable equivalents - Subscribers are reaped when idle past
WALKIE_SUBSCRIBER_TTL_MS(1h default) and holding nothing — never when a message or waiter would be lost - Tests derive a random secret per run (
test/helpers.jsSECRET). Topics are SHA-256(channel+secret) on the public DHT, so fixed secrets let stray daemons join test channels and skew assertions - Every agent-CLI adapter parses through a
parse*Outputhelper in cli-utils (parseClaudeOutput,parsePiOutput), and none may defaulttextto raw stdout. That default is what made the agent relay a JSON event stream as its reply (#13); a payload that parsed as JSON but carried no message must yield empty text instead claude -p --output-format jsonhas TWO shapes in the wild: current CLIs return a single-line JSON array of events (reply on thetype: "result"element), older ones a single result object.cli-utils.parseClaudeOutputhandles both plus newline-delimited stream-json, and posts nothing when the payload parses as JSON but carries no reply — dumping an event stream into a channel is worse than silence. It lives in cli-utils, not inline in bin, so it is unit-testable; it was refactored twice while broken because nothing could reach it (issue #13, diagnosed in PR #14)- Do NOT merge the semgrep-driven "harden child_process" change to
execForMessage(PR #18). ReplacingexecSync(cmd)withexecFileSync('/bin/sh', ['-c', '$WALKIE_CMD'])breakswatch --exec: quotes stop being interpreted and$WALKIE_MSGno longer expands, which is the flag's entire purpose. The command comes from the user's own CLI flag, so there is no untrusted input to sanitize. Its second half (using execFile for the--openURL) is directionally right but breaks Windows, wherestartis a shell builtin and not an executable - walkie carries messages, not authority. Relayed human approval is not approval;
see the closing section of
skills/walkie/references/commands.md - The daemon logs uncaught exceptions/rejections to
~/.walkie/daemon.logand the client spawns it with stderr pointed at that file. Do not go back tostdio: 'ignore': a daemon that crashed afterstart()previously left only a clean "Daemon started" line, which is why issue #11 could not be diagnosed by anyone - Windows IPC pipe name is derived from
WALKIE_DIR(it used to be a fixed global name, so every instance on the machine shared one pipe) walkie webbinds 127.0.0.1 by default (--hostto widen, which warns).GET /stateis unauthenticated and returns channel secrets plus message history, so a wider bind hands the network the keys to every channel the UI has touched. Do not change this default; if/stateever needs to be reachable, authenticate it firstwalkie webuses read-wait loops per channel (no daemon changes needed for real-time)- Web client identity:
web-{random8hex}, renameable via header click - Web session state (channels, secrets, name) persisted in localStorage
(
walkie:web:state:v1); the server/stateendpoint is a legacy fallback used only when the localStorage write fails, plus abeforeunloadsendBeacon flush so the 500ms save debounce cannot lose messages on close wsnpm package added as 3rd dependency- Programmatic API (
src/api.js) wrapsclient.jsfunctions — no new deps, uses existing daemon IPC package.json"main": "./src/api.js"makesrequire('walkie-sh')return the API (not the CLI)