Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

## Unreleased

- Config file support (`chorus.json` / `.chorus/config.json` / `~/.config/chorus/config.json` / `/etc/chorus/config.json`) with layered merge, enterprise security locks (`allowSkipApproval`, `requireRepoMatch`), and `/chorus-status` effective-config output
- Security: host approval gate for joiners (`requireApproval` on share, default true; `chorus-approve` / `chorus-deny`)
- Security: display name required on join (reject empty / whitespace)
- Security: when the host share directory has a git `origin`, bind the session to that remote and reject joiners who are not in a matching clone (soft claim check, not provider ACL)
- Host tool: `chorus-kick` for active joiners
- Session access control: host approval, required display names, repo/email/domain gates (`session.policy` / `auth.pending` / `host.approve|deny`)
- Configurable git remote prefixes and host rewrites; layered `chorus.json` org settings
- Hybrid OpenCode security e2e, VS Code adapter e2e, and Cloud Agent install script
- VS Code extension (`packages/vscode`) — share/join/chat; Session sidebar; approve/deny
- Extract `@chorus/client` (`JoinClient` + `RelayServer`) for reuse by OpenCode, VS Code, and future adapters
- Real-time shared transcript for all agents: fan out collaborator prompts to every joiner, abort joiner local LLM, mirror `[Host]`/`[name]`/`[AI]` (prefer web UI for live view)
- Joiner mirrors host prompts and AI replies into its OpenCode session transcript (`[Host]:` / `[AI]:`), including history replay on join (replaces toast-only viewing)
- Fix: prevent host/joiner feedback loop when the same agent both shares and joins (block join while sharing; never mirror while hosting; drop echoed `[AI]/`/`[Host]:` collab lines)
- Fix: prevent host/joiner feedback loop when the same agent both shares and joins (block join while sharing; never mirror while hosting; drop echoed `[AI]`/`[Host]:` collab lines)
- Replace in-process Bun WebSocket relay with Rust `chorus-relay` binary (`crates/chorus-relay`)
- Host control protocol on `/host` (token issue, session events, chat, collab.input fan-in)
- Joiner protocol on `/ws` unchanged
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Collaborative OpenCode session sharing. Pair-program a live AI session from anot

## How it works

Chorus is an [OpenCode](https://github.com/sst/opencode) plugin plus a **Rust WebSocket relay** (`chorus-relay`).
Chorus is an [OpenCode](https://github.com/sst/opencode) plugin (plus a VS Code adapter) and a **Rust WebSocket relay** (`chorus-relay`).

```
Host runs opencode → plugin loads
Expand Down Expand Up @@ -50,6 +50,8 @@ One monorepo, two ecosystems, one wire contract:
| Path | Artifact | Description |
|---|---|---|
| `packages/plugin` | npm `@chorus/plugin` | OpenCode plugin — tools, hooks, spawns/manages relay |
| `packages/client` | npm `@chorus/client` | Shared `JoinClient` + `RelayServer` for host adapters |
| `packages/vscode` | VS Code extension `chorus` | Share/join Chorus sessions from VS Code |
| `packages/shared` | npm `@chorus/shared` | TypeScript types + codecs for joiner and host-control protocols |
| `crates/chorus-relay` | `chorus-relay` binary | Rust WebSocket relay (`/ws` joiners, `/host` control plane) |
| `protocol/` | fixtures (not published) | Canonical JSON examples both TS and Rust must deserialize |
Expand Down
32 changes: 32 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ Recommended shape:
```

- **v1:** OpenCode adapter = this repo’s plugin + local **Rust** `chorus-relay`
- **v1.x:** keep `@chorus/shared` as the stable joiner + host-control contract
- **v2:** optional second adapter (Codex app-server is the best #2) only after OpenCode UX is solid
- **v1.x:** keep `@chorus/shared` as the stable joiner + host-control contract; `@chorus/client` holds `JoinClient` / `RelayServer` for non-OpenCode hosts
- **v1.x adapters in-monorepo:** VS Code (`packages/vscode`) — same wire protocol, host-specific UI only.
- **v2:** optional further adapters (Codex app-server remains a strong candidate) once OpenCode UX is solid; split adapter packages into separate repos only when publish/CI ownership diverges

**Do not** make Claude Code / Amp the primary host — large audiences, but proprietary control planes. Treat them as distribution adapters later, not the core.

Expand Down
8 changes: 8 additions & 0 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Chorus is an **OpenCode↔OpenCode** LAN collaboration stack:
The browser companion (`packages/web`) was intentionally removed. Joiners now mirror the host transcript into their OpenCode session (`[Host]:` / `[AI]:` lines via `noReply` inject); side-channel chat/typing remain toasts.
The in-process Bun relay has been replaced by `crates/chorus-relay`.

**Editor adapters (in progress):** VS Code (`packages/vscode`).

Differentiation vs nearby OpenCode plugins (`opencode-live`, `opencode-sessions`, `opencode-ensemble`, `opencode-relay`): those target **multi-agent / same-DB sync**. Chorus targets **multi-human** pair programming on one live AI session.

## Priority backlog
Expand Down Expand Up @@ -65,8 +67,14 @@ Differentiation vs nearby OpenCode plugins (`opencode-live`, `opencode-sessions`

`scripts/multi-agent.ts` (via `bun run multi-agent`) can spawn N isolated `opencode serve` instances with the Chorus plugin, then automate `/chorus-share` + `/chorus-join` through OpenCode’s `/session/:id/command` API. Also includes `relay-stress` for concurrent protocol joiners without OpenCode.

## Adapter surface (monorepo)

- **OpenCode** (`packages/plugin`) — primary host; full LLM loop + transcript mirror.
- **VS Code** (`packages/vscode`) — share/join via `@chorus/client`; sidebar transcript; joiner `collab.input` works against an OpenCode host. VS Code share does **not** drive OpenCode’s model — publish host lines manually or pair with OpenCode.

## Explicit non-goals (for now)

- Rebuilding a full coding agent harness from scratch.
- Competing with multi-agent orchestration plugins.
- Making the browser app the primary UX again (optional later for non-OpenCode observers).
- Splitting the monorepo until adapter publish cadence actually diverges.
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@
"scripts": {
"build:relay": "cargo build -p chorus-relay --release",
"build:shared": "bun run --filter @chorus/shared build",
"build:client": "bun run --filter @chorus/client build",
"build:plugin": "bun run --filter @chorus/plugin build",
"build:ts": "bun run build:shared && bun run build:plugin",
"build:vscode": "bun run --filter chorus build",
"build:ts": "bun run build:shared && bun run build:client && bun run build:plugin && bun run build:vscode",
"build": "bun run build:relay && bun run build:ts",
"test:relay": "cargo test -p chorus-relay",
"test:ts": "bun run build:shared && bun run --filter @chorus/shared test && bun run --filter @chorus/plugin test",
"test:ts": "bun run build:shared && bun run build:client && bun run --filter @chorus/shared test && bun run --filter @chorus/client test && bun run --filter @chorus/plugin test && bun run --filter chorus test",
"test": "bun run build:relay && bun run test:relay && bun run test:ts",
"typecheck": "bun run build:shared && bun run --filter @chorus/shared typecheck && bun run --filter @chorus/plugin typecheck",
"clean": "bun run --filter @chorus/shared clean && bun run --filter @chorus/plugin clean && cargo clean",
"typecheck": "bun run build:shared && bun run build:client && bun run --filter @chorus/shared typecheck && bun run --filter @chorus/client typecheck && bun run --filter @chorus/plugin typecheck && bun run --filter chorus typecheck",
"clean": "bun run --filter @chorus/shared clean && bun run --filter @chorus/client clean && bun run --filter @chorus/plugin clean && bun run --filter chorus clean && cargo clean",
"multi-agent": "bun scripts/multi-agent.ts",
"test:multi-agent": "bun scripts/multi-agent.ts setup --agents 2 && bun scripts/multi-agent.ts up --agents 2 && bun scripts/multi-agent.ts smoke && bun scripts/multi-agent.ts pair && bun scripts/multi-agent.ts down",
"test:integration": "bun run test && bun run typecheck && bun run test:multi-agent && bun run test:security-e2e && bun run test:relay-stress",
"test:relay-stress": "bun scripts/multi-agent.ts relay-stress --agents 5",
"test:security-e2e": "bun scripts/security-e2e.ts",
"test:vscode-e2e": "bun scripts/vscode-e2e.ts",
"test:plugin-e2e": "bun run test:multi-agent && bun run test:security-e2e",
"test:adapters-e2e": "bun run test:vscode-e2e",
"docker-agents": "bun scripts/docker-agents.ts",
"test:docker-agents": "bun scripts/docker-agents.ts up --agents 2 && bun scripts/docker-agents.ts smoke && bun scripts/docker-agents.ts pair && bun scripts/docker-agents.ts down"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/client/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { JoinClient, type JoinState, type JoinStatus, } from "./join.js";
export { RelayServer, relayOptionsFromEnv, type RelayServerOptions, } from "./relay.js";
//# sourceMappingURL=index.d.ts.map
1 change: 1 addition & 0 deletions packages/client/dist/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/client/dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/client/dist/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions packages/client/dist/join.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { SessionEvent, ConnectedUser } from "@chorus/shared";
export type JoinStatus = "connecting" | "pending" | "connected" | "disconnected" | "error";
export interface JoinState {
status: JoinStatus;
sessionId: string;
userId?: string;
users: ConnectedUser[];
recentEvents: SessionEvent[];
error?: string;
}
export declare class JoinClient {
private readonly relayUrl;
private readonly token;
private readonly displayName;
private readonly repoRemote?;
private readonly email?;
private ws;
private state;
private onEvent?;
private onChatMessage?;
private onTyping?;
private onPending?;
private onApproved?;
constructor(relayUrl: string, token: string, displayName: string, repoRemote?: string | undefined, email?: string | undefined);
connect(): Promise<void>;
sendInput(content: string): void;
sendChat(content: string): void;
setChatHandler(fn: (displayName: string | undefined, content: string) => void): void;
setTypingHandler(fn: (displayName: string | undefined) => void): void;
setEventHandler(fn: (event: SessionEvent) => void): void;
setPendingHandler(fn: (userId: string) => void): void;
setApprovedHandler(fn: () => void): void;
sendTyping(): void;
getState(): Readonly<JoinState>;
disconnect(): void;
}
//# sourceMappingURL=join.d.ts.map
1 change: 1 addition & 0 deletions packages/client/dist/join.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading