Skip to content

Commit 60c75b7

Browse files
authored
Merge pull request #1 from Incultnitollc/feat/v0.1.0-implementation
v0.1.0: initial implementation
2 parents 34ab574 + 892ba70 commit 60c75b7

31 files changed

Lines changed: 6750 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
strategy:
12+
matrix:
13+
node: [20, 22]
14+
os: [ubuntu-latest, macos-latest]
15+
runs-on: ${{ matrix.os }}
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: ${{ matrix.node }}
21+
cache: npm
22+
- run: npm ci
23+
- run: npx tsc --noEmit
24+
- run: npx eslint . --quiet
25+
- run: npx vitest run --coverage
26+
- run: npm run build
27+
- run: node ./dist/bin/cc-bridge.js --version
28+
- run: npx tsx tests/manual/two-windows.ts
29+
- run: npm audit --audit-level=high

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
.DS_Store
5+
*.log
6+
.env
7+
.env.local
8+
.vitest-tmp/

.npmignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
src/
2+
tests/
3+
docs/
4+
.github/
5+
coverage/
6+
*.config.ts
7+
*.config.js
8+
.eslintrc*
9+
.prettierrc*
10+
tsconfig.json
11+
.gitignore

.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"trailingComma": "all",
5+
"printWidth": 100,
6+
"tabWidth": 2,
7+
"arrowParens": "always"
8+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Incultnito LLC
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.

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# @incultnitollc/cc-bridge
2+
3+
> Live JSONL message bridge between local Claude Code sessions.
4+
5+
Two (or more) Claude Code windows on the same machine. They need to talk in real time. `cc-bridge` gives each session a CLI to **send** a message to a shared file and **listen** for new messages via a file-tail stream that Claude's `Monitor` tool consumes as live push notifications. No daemon, no network, no polling.
6+
7+
## Install
8+
9+
```bash
10+
npm install -g @incultnitollc/cc-bridge
11+
```
12+
13+
Requires Node ≥ 20.
14+
15+
## Quickstart — two windows
16+
17+
**Window A:**
18+
```bash
19+
cc-bridge listen
20+
```
21+
22+
**Window B:**
23+
```bash
24+
cc-bridge send "hello from B"
25+
```
26+
27+
Window A immediately receives the JSONL line.
28+
29+
## Inside Claude Code
30+
31+
In each session, run the listen command under the `Monitor` tool so every appended line arrives as a live push notification — no polling.
32+
33+
```
34+
Monitor: cc-bridge listen default
35+
```
36+
37+
Then send from the other window via the `Bash` tool:
38+
39+
```
40+
Bash: cc-bridge send "ready for review"
41+
```
42+
43+
## Concepts
44+
45+
- **Room** — a named JSONL file under `~/.cc-bridge/rooms/<room>.jsonl`. Default room is `default`. Names allowed: `[a-zA-Z0-9_.-]{1,64}`.
46+
- **Session id** — auto-generated `<host>-<ppid>-<rand8>` (parent shell PID, so two windows differ naturally). Override with `CC_BRIDGE_FROM=...`.
47+
- **Message** — single-line JSON. Required: `v, id, ts, room, from, msg`. Optional: `to, reply_to, kind`. Unknown fields preserved for forward-compat.
48+
49+
## Commands
50+
51+
```bash
52+
cc-bridge listen [room] [--replay N] [--pretty] [--filter from=X] [--from ID] [--json-errors]
53+
cc-bridge send <msg> [--room R] [--to ID] [--reply-to ULID] [--kind text|event] [--from ID]
54+
echo "hi" | cc-bridge send # reads from stdin if piped
55+
cc-bridge rooms # list rooms with size + mtime
56+
cc-bridge rooms clear <room> --yes
57+
cc-bridge validate <file> # lint a JSONL room file
58+
cc-bridge --version
59+
cc-bridge --help
60+
```
61+
62+
## Library use
63+
64+
```ts
65+
import { sendMessage, listen, listRooms } from '@incultnitollc/cc-bridge'
66+
67+
await sendMessage({ from: 'planner', room: 'team', msg: 'kicking off build' })
68+
69+
const ctrl = listen({ room: 'team', sessionId: 'reviewer' })
70+
for await (const ev of ctrl.iterator) {
71+
if (ev.ok) console.log(ev.line)
72+
}
73+
```
74+
75+
## Security model
76+
77+
`cc-bridge` is a **local-host IPC primitive**. The trust boundary is your user account. Do not place `~/.cc-bridge/` on a shared filesystem, network drive, or cloud-synced directory.
78+
79+
- Files in `~/.cc-bridge/` are created with mode `0700` (dir) / `0600` (files).
80+
- Room names sanitized; path traversal refused.
81+
- Symlinks at room paths refused.
82+
- 64KB per-message cap. 10MB room file soft-warn; 100MB hard-refuse.
83+
- `--pretty` strips ANSI escape sequences to prevent terminal hijack.
84+
- The `from` field is sender-asserted (no signature). v1 explicitly trusts everyone with write access to your `$HOME`.
85+
86+
## Roadmap
87+
88+
- **v1.1**`cc-bridge install-hooks` (auto-wire Claude Code Stop/UserPromptSubmit hooks), DM filtering (`--me`), time-based replay (`--replay 1h`), read receipts, Windows support.
89+
- **v2** — MCP server wrapper, cross-machine backend (Supabase Realtime / Redis), webhook fanout, observer dashboard.
90+
91+
## License
92+
93+
MIT © 2026 Incultnito LLC.

docs/superpowers/specs/2026-06-02-claude-code-live-bridge-design.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ cc-bridge listen
174174

175175
**Offset state:** saved to `~/.cc-bridge/state/<room>-<sessionId>.offset` so `listen` can resume after SIGINT without re-replaying. Stale state for vanished sessions is reaped during `cc-bridge rooms` runs (best-effort, non-fatal).
176176

177+
### 6.3 Watcher Mode — Polling Decision (v0.1.0)
178+
179+
The listen module's chokidar watcher was originally configured with `usePolling: false` (native FSEvents on darwin, inotify on linux). Under chokidar 4.x on macOS, `change` events from FSEvents fire unreliably for our local append-only JSONL workload — tests timed out waiting for events that arrived 5+ seconds late or not at all. Switched to `usePolling: true, interval: 50` for v0.1.0.
180+
181+
**Tradeoff:** ~50ms latency floor on message delivery (negligible for human-driven Claude Code IPC), and constant low background `fs.stat` load (one syscall per room being listened to per 50ms — bounded by number of active sessions, not by message rate).
182+
183+
**Revisit if:** (a) chokidar 4 fixes FSEvents reliability, or (b) profiling shows polling overhead matters at scale. Not a v1.1 priority.
184+
177185
## 7. Security Model
178186

179187
**Threat model:** local-host IPC only. Same user, same machine. Trust boundary = the user account. This is NOT a network protocol. README states this explicitly.

eslint.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import js from '@eslint/js'
2+
import tseslint from 'typescript-eslint'
3+
4+
export default tseslint.config(
5+
js.configs.recommended,
6+
...tseslint.configs.recommended,
7+
{
8+
rules: {
9+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
10+
'@typescript-eslint/explicit-function-return-type': 'off',
11+
'no-console': 'off',
12+
},
13+
},
14+
{
15+
ignores: ['dist/**', 'coverage/**', 'node_modules/**'],
16+
},
17+
)

0 commit comments

Comments
 (0)