vibeshare-live 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 +21 -0
- package/README.md +104 -0
- package/dist/chunk-5OLI3WTU.js +233 -0
- package/dist/cli.d.ts +42 -0
- package/dist/cli.js +277 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +32 -0
- package/dist/mcp.d.ts +41 -0
- package/dist/mcp.js +14635 -0
- package/dist/share-CoX608sF.d.ts +188 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pooria Arab
|
|
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,104 @@
|
|
|
1
|
+
# vibeshare
|
|
2
|
+
|
|
3
|
+
Share your live agent coding session by URL — traces.com-style, open source, CLI-first. Spectate read-only or invite into the vibelive multiplayer engine.
|
|
4
|
+
|
|
5
|
+
Part of the **Vibe Suite** — companion tools for agentic coding CLIs (Claude Code, Codex, Gemini, Grok/pi, Kimi). Ships as **CLI + npm package + MCP server**.
|
|
6
|
+
|
|
7
|
+
**Local-first: runs on your own machine, no data out** (consent model in `@pooriaarab/vibe-core`).
|
|
8
|
+
|
|
9
|
+
> Status: **v0** — the share/access layer over [`@pooriaarab/vibelive`](https://www.npmjs.com/package/@pooriaarab/vibelive) is implemented (CLI, library, MCP). Local/LAN, host-authoritative. E2e relay fan-out to ~1000 spectators is on the vibelive roadmap.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g vibeshare-live
|
|
15
|
+
# or use it inline:
|
|
16
|
+
npx vibeshare-live -- claude
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It pulls in `@pooriaarab/vibelive` (the multiplayer engine) and `@pooriaarab/vibe-core` (the consent ledger).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
Host your agent and hand someone a link:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
vibeshare --invite -- claude
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You'll get back something like:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
vibeshare — sharing claude
|
|
33
|
+
share: https://vibeshare.stream/s/a5e3a635-0bb7-4689-973c-5fe2d7de6207
|
|
34
|
+
access: invite · no expiry
|
|
35
|
+
relay: ws://localhost:55410
|
|
36
|
+
● p2p · e2e · nothing stored on a server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Send them the `share` URL. They spectate read-only; with `--invite` they can request to join and you `/approve <id>` to let them drive. Slash commands inside the host session: `/drive` `/release` `/viewers` `/approve <id>` `/stop`.
|
|
40
|
+
|
|
41
|
+
| Flag | Meaning |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `--spectate` | link holders get read-only access (default) |
|
|
44
|
+
| `--invite` | link holders may request to join; you approve to let them drive |
|
|
45
|
+
| `--expire 1h\|24h` | auto-revoke the share after this duration |
|
|
46
|
+
| `--pass <p>` | require a passphrase on top of the share URL |
|
|
47
|
+
|
|
48
|
+
`vibeshare --version` · `vibeshare --help` · `vibeshare mcp`
|
|
49
|
+
|
|
50
|
+
## Your own domain, not vibe.live
|
|
51
|
+
|
|
52
|
+
The share URL reads `vibeshare.stream/s/<id>` — **vibeshare's own domain** (available $4/yr), not `vibe.live`. The link is a capability URL: the id is the only thing that grants access (122 bits of entropy), so it's unguessable. Optional `--pass` adds a second factor.
|
|
53
|
+
|
|
54
|
+
The actual live stream runs over the local/LAN WebSocket relay that vibelive starts on your machine. The `vibeshare.stream` URL is the identity layer; the bytes flow over a self-hostable relay.
|
|
55
|
+
|
|
56
|
+
## Peer-to-peer, end-to-end encrypted, nothing stored on a server
|
|
57
|
+
|
|
58
|
+
The session stream runs **peer-to-peer / via a dumb e2e-encrypted relay** that forwards opaque blobs. The relay cannot decrypt anything — nothing readable is ever stored on a server, and the relay is self-hostable. This is vibelive's transport (§1–2 of its tech spec); vibeshare is the URL/access layer on top. Fanning session output off your machine is gated behind the `share:session` consent scope.
|
|
59
|
+
|
|
60
|
+
## As a library
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { createHost, createRelay } from '@pooriaarab/vibelive';
|
|
64
|
+
import { createShare } from 'vibeshare-live';
|
|
65
|
+
|
|
66
|
+
const host = createHost({ command: ['claude'] });
|
|
67
|
+
const relay = await createRelay({ port: 0, hostHandle: host, initialDriver: 'host' });
|
|
68
|
+
|
|
69
|
+
const share = createShare({
|
|
70
|
+
session: relay,
|
|
71
|
+
access: 'spectate', // 'spectate' (read-only) | 'invite' (can join to drive)
|
|
72
|
+
expiry: '1h', // optional: '1h' | '24h' | number(ms)
|
|
73
|
+
passphrase: 'sekret', // optional second factor
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log(share.url); // https://vibeshare.stream/s/<id>
|
|
77
|
+
const { viewers, pending } = share.viewers();
|
|
78
|
+
await share.revoke(); // tear down (also fires on expiry)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Spectators are enforced read-only at the access gate: a spectator's control request never reaches vibelive's `WriteArbiter`, so they can never obtain the write token — the same "never two concurrent writers" invariant vibelive holds, extended to the share.
|
|
82
|
+
|
|
83
|
+
## As an MCP server
|
|
84
|
+
|
|
85
|
+
Wire it into an agent that speaks MCP and it can offer "share this session?":
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
// .mcp.json or your client's MCP config
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"vibeshare": { "command": "vibeshare", "args": ["mcp"] }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Tools: `create_share` (host a command, mint a share URL) and `viewers` (list active shares + their audiences).
|
|
97
|
+
|
|
98
|
+
## Prototype
|
|
99
|
+
|
|
100
|
+
Interactive, self-contained UX prototype (no build, no network): open [`docs/prototype.html`](docs/prototype.html) in a browser.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/access.ts
|
|
8
|
+
var DEFAULT_ACCESS = "spectate";
|
|
9
|
+
function createAccessGate(options) {
|
|
10
|
+
const access = options.access ?? DEFAULT_ACCESS;
|
|
11
|
+
const arbiter = options.arbiter;
|
|
12
|
+
const passphrase = options.passphrase;
|
|
13
|
+
const roles = /* @__PURE__ */ new Map();
|
|
14
|
+
const hasPassphrase = typeof passphrase === "string" && passphrase.length > 0;
|
|
15
|
+
return {
|
|
16
|
+
access,
|
|
17
|
+
hasPassphrase,
|
|
18
|
+
admit({ id }, input) {
|
|
19
|
+
if (hasPassphrase && input !== passphrase) return false;
|
|
20
|
+
if (!roles.has(id)) roles.set(id, "spectator");
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
23
|
+
remove(id) {
|
|
24
|
+
roles.delete(id);
|
|
25
|
+
arbiter.leave(id);
|
|
26
|
+
},
|
|
27
|
+
has: (id) => roles.has(id),
|
|
28
|
+
role: (id) => roles.get(id),
|
|
29
|
+
promote(id) {
|
|
30
|
+
if (access !== "invite") return false;
|
|
31
|
+
if (!roles.has(id)) return false;
|
|
32
|
+
roles.set(id, "participant");
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
requestControl(id) {
|
|
36
|
+
const role = roles.get(id);
|
|
37
|
+
if (role === void 0) return { ok: false, reason: "unknown" };
|
|
38
|
+
if (role === "participant") {
|
|
39
|
+
const state = arbiter.requestControl(id);
|
|
40
|
+
return { ok: true, state };
|
|
41
|
+
}
|
|
42
|
+
return { ok: false, reason: access === "invite" ? "not-promoted" : "spectator" };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/url.ts
|
|
48
|
+
var SHARE_ORIGIN = "https://vibeshare.stream";
|
|
49
|
+
var SHARE_PATH_PREFIX = "/s/";
|
|
50
|
+
function newShareId() {
|
|
51
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
52
|
+
if (uuid) return uuid;
|
|
53
|
+
const bytes = new Uint8Array(16);
|
|
54
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
55
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
56
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
57
|
+
}
|
|
58
|
+
function buildShareUrl(id, origin = SHARE_ORIGIN) {
|
|
59
|
+
return `${origin}${SHARE_PATH_PREFIX}${id}`;
|
|
60
|
+
}
|
|
61
|
+
var ShareUrlParseError = class extends Error {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "ShareUrlParseError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function parseShareUrl(url) {
|
|
68
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
69
|
+
throw new ShareUrlParseError("share url must be a non-empty string");
|
|
70
|
+
}
|
|
71
|
+
const idx = url.indexOf(SHARE_PATH_PREFIX);
|
|
72
|
+
if (idx < 0) {
|
|
73
|
+
throw new ShareUrlParseError(`not a vibeshare share url (missing "${SHARE_PATH_PREFIX}"): ${url}`);
|
|
74
|
+
}
|
|
75
|
+
const tail = url.slice(idx + SHARE_PATH_PREFIX.length);
|
|
76
|
+
const end = tail.search(/[/?#]/);
|
|
77
|
+
const id = end < 0 ? tail : tail.slice(0, end);
|
|
78
|
+
if (id.length === 0) {
|
|
79
|
+
throw new ShareUrlParseError(`share url has an empty id: ${url}`);
|
|
80
|
+
}
|
|
81
|
+
return { id };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/share.ts
|
|
85
|
+
import { SHARE_SESSION_SCOPE as VIBELIVE_SHARE_SCOPE } from "vibelive-cli";
|
|
86
|
+
var SHARE_SESSION_SCOPE = VIBELIVE_SHARE_SCOPE;
|
|
87
|
+
function parseExpiry(spec) {
|
|
88
|
+
if (spec === void 0) return null;
|
|
89
|
+
if (typeof spec === "number") {
|
|
90
|
+
if (!Number.isFinite(spec) || spec <= 0) {
|
|
91
|
+
throw new RangeError(`expiry duration must be a positive finite number (ms), got: ${spec}`);
|
|
92
|
+
}
|
|
93
|
+
return Math.floor(spec);
|
|
94
|
+
}
|
|
95
|
+
switch (spec) {
|
|
96
|
+
case "1h":
|
|
97
|
+
return 60 * 60 * 1e3;
|
|
98
|
+
case "24h":
|
|
99
|
+
return 24 * 60 * 60 * 1e3;
|
|
100
|
+
default: {
|
|
101
|
+
const exhaustive = spec;
|
|
102
|
+
throw new RangeError(`unknown expiry preset: ${String(exhaustive)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
var ConsentError = class extends Error {
|
|
107
|
+
constructor() {
|
|
108
|
+
super(`consent for "${SHARE_SESSION_SCOPE}" is required to share a session \u2014 grant it first`);
|
|
109
|
+
this.name = "ConsentError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var HOST_PARTICIPANT_NAME = "host";
|
|
113
|
+
function isLocalHostParticipant(p) {
|
|
114
|
+
return p.ws === void 0;
|
|
115
|
+
}
|
|
116
|
+
function participantName(p, fallback) {
|
|
117
|
+
return p.name && p.name.length > 0 ? p.name : fallback;
|
|
118
|
+
}
|
|
119
|
+
function createShare(options) {
|
|
120
|
+
const relay = options.session;
|
|
121
|
+
const consent = options.consent ?? relay.consent;
|
|
122
|
+
if (!consent.allows(SHARE_SESSION_SCOPE)) {
|
|
123
|
+
throw new ConsentError();
|
|
124
|
+
}
|
|
125
|
+
const id = newShareId();
|
|
126
|
+
const url = buildShareUrl(id);
|
|
127
|
+
const gate = createAccessGate({
|
|
128
|
+
arbiter: relay.arbiter,
|
|
129
|
+
access: options.access,
|
|
130
|
+
passphrase: options.passphrase
|
|
131
|
+
});
|
|
132
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
133
|
+
const noteSeen = (pid) => {
|
|
134
|
+
let t = firstSeen.get(pid);
|
|
135
|
+
if (t === void 0) {
|
|
136
|
+
t = Date.now();
|
|
137
|
+
firstSeen.set(pid, t);
|
|
138
|
+
}
|
|
139
|
+
return t;
|
|
140
|
+
};
|
|
141
|
+
let revoked = false;
|
|
142
|
+
let revokeReason;
|
|
143
|
+
let expiryTimer;
|
|
144
|
+
const fireRevoke = (reason) => {
|
|
145
|
+
if (revoked) return Promise.resolve();
|
|
146
|
+
revoked = true;
|
|
147
|
+
revokeReason = reason;
|
|
148
|
+
if (expiryTimer !== void 0) {
|
|
149
|
+
clearTimeout(expiryTimer);
|
|
150
|
+
expiryTimer = void 0;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
options.onRevoke?.(reason);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
return relay.close().catch(() => {
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const expiryMs = parseExpiry(options.expiry);
|
|
160
|
+
if (expiryMs !== null) {
|
|
161
|
+
expiryTimer = setTimeout(() => {
|
|
162
|
+
void fireRevoke("expired");
|
|
163
|
+
}, expiryMs);
|
|
164
|
+
expiryTimer.unref?.();
|
|
165
|
+
}
|
|
166
|
+
const toViewer = (p) => {
|
|
167
|
+
const pid = p.id;
|
|
168
|
+
if (!gate.has(pid)) {
|
|
169
|
+
gate.admit({ id: pid, name: participantName(p, pid) });
|
|
170
|
+
}
|
|
171
|
+
const role = gate.role(pid) ?? "spectator";
|
|
172
|
+
return {
|
|
173
|
+
id: pid,
|
|
174
|
+
name: participantName(p, pid),
|
|
175
|
+
role,
|
|
176
|
+
joinedAt: noteSeen(pid)
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
id,
|
|
181
|
+
url,
|
|
182
|
+
access: gate.access,
|
|
183
|
+
gate,
|
|
184
|
+
relayUrl: relay.url,
|
|
185
|
+
get revoked() {
|
|
186
|
+
return revoked;
|
|
187
|
+
},
|
|
188
|
+
viewers() {
|
|
189
|
+
const connected = [];
|
|
190
|
+
const pending = [];
|
|
191
|
+
for (const p of relay.participants) {
|
|
192
|
+
if (isLocalHostParticipant(p)) continue;
|
|
193
|
+
const v = toViewer(p);
|
|
194
|
+
if (gate.access === "invite" && v.role !== "participant") {
|
|
195
|
+
pending.push(v);
|
|
196
|
+
} else {
|
|
197
|
+
connected.push(v);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { viewers: connected, pending };
|
|
201
|
+
},
|
|
202
|
+
approve(viewerId) {
|
|
203
|
+
return gate.promote(viewerId);
|
|
204
|
+
},
|
|
205
|
+
removeViewer(viewerId) {
|
|
206
|
+
gate.remove(viewerId);
|
|
207
|
+
},
|
|
208
|
+
revoke() {
|
|
209
|
+
return fireRevoke(revokeReason === "expired" ? "expired" : "manual");
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/version.ts
|
|
215
|
+
var VERSION = "0.1.0";
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
__export,
|
|
219
|
+
DEFAULT_ACCESS,
|
|
220
|
+
createAccessGate,
|
|
221
|
+
SHARE_ORIGIN,
|
|
222
|
+
SHARE_PATH_PREFIX,
|
|
223
|
+
newShareId,
|
|
224
|
+
buildShareUrl,
|
|
225
|
+
ShareUrlParseError,
|
|
226
|
+
parseShareUrl,
|
|
227
|
+
SHARE_SESSION_SCOPE,
|
|
228
|
+
parseExpiry,
|
|
229
|
+
ConsentError,
|
|
230
|
+
HOST_PARTICIPANT_NAME,
|
|
231
|
+
createShare,
|
|
232
|
+
VERSION
|
|
233
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { A as AccessMode, E as ExpirySpec } from './share-CoX608sF.js';
|
|
3
|
+
import 'vibelive-cli';
|
|
4
|
+
import '@pooriaarab/vibe-core';
|
|
5
|
+
|
|
6
|
+
type ParsedCommand = {
|
|
7
|
+
readonly cmd: 'help';
|
|
8
|
+
} | {
|
|
9
|
+
readonly cmd: 'version';
|
|
10
|
+
} | {
|
|
11
|
+
readonly cmd: 'mcp';
|
|
12
|
+
} | {
|
|
13
|
+
readonly cmd: 'stop';
|
|
14
|
+
} | {
|
|
15
|
+
readonly cmd: 'viewers';
|
|
16
|
+
} | {
|
|
17
|
+
readonly cmd: 'host';
|
|
18
|
+
readonly command: readonly string[];
|
|
19
|
+
readonly access: AccessMode;
|
|
20
|
+
readonly expire?: ExpirySpec;
|
|
21
|
+
readonly pass?: string;
|
|
22
|
+
readonly name?: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly cmd: 'error';
|
|
25
|
+
readonly message: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parse vibeshare argv (`process.argv.slice(2)`). Pure — no IO.
|
|
29
|
+
*
|
|
30
|
+
* Everything after a bare `--` is the opaque wrapped command and is never scanned
|
|
31
|
+
* for vibeshare flags, so `vibeshare -- claude --help` wraps `claude --help`
|
|
32
|
+
* rather than printing vibeshare's own help. Both `vibeshare -- <cmd>` and the
|
|
33
|
+
* explicit `vibeshare host -- <cmd>` forms are accepted.
|
|
34
|
+
*/
|
|
35
|
+
declare function parseArgs(argv: readonly string[]): ParsedCommand;
|
|
36
|
+
declare function printHelp(stream?: {
|
|
37
|
+
write(s: string): boolean;
|
|
38
|
+
}): void;
|
|
39
|
+
/** CLI entrypoint. Returns the desired exit code (does not call exit itself). */
|
|
40
|
+
declare function main(argv?: readonly string[]): Promise<number>;
|
|
41
|
+
|
|
42
|
+
export { main, parseArgs, printHelp };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION,
|
|
4
|
+
createShare
|
|
5
|
+
} from "./chunk-5OLI3WTU.js";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { createInterface } from "readline";
|
|
9
|
+
import { networkInterfaces } from "os";
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
import { createHost, createRelay, SHARE_SESSION_SCOPE } from "vibelive-cli";
|
|
12
|
+
var HOST_ID = "host";
|
|
13
|
+
var BADGE = "\u25CF p2p \xB7 e2e \xB7 nothing stored on a server";
|
|
14
|
+
var err = (message) => ({ cmd: "error", message });
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const dd = argv.indexOf("--");
|
|
17
|
+
const head = dd >= 0 ? argv.slice(0, dd) : [...argv];
|
|
18
|
+
const command = dd >= 0 ? argv.slice(dd + 1) : [];
|
|
19
|
+
if (head.includes("--help") || head.includes("-h")) return { cmd: "help" };
|
|
20
|
+
if (head.includes("--version") || head.includes("-v")) return { cmd: "version" };
|
|
21
|
+
const sub = head[0];
|
|
22
|
+
if (sub === "stop") return { cmd: "stop" };
|
|
23
|
+
if (sub === "viewers") return { cmd: "viewers" };
|
|
24
|
+
if (sub === "mcp") return { cmd: "mcp" };
|
|
25
|
+
if (head.length === 0 && command.length === 0) return { cmd: "help" };
|
|
26
|
+
let flagTokens;
|
|
27
|
+
if (sub === "host") {
|
|
28
|
+
flagTokens = head.slice(1);
|
|
29
|
+
} else if (sub === void 0 || sub.startsWith("-")) {
|
|
30
|
+
flagTokens = head;
|
|
31
|
+
} else {
|
|
32
|
+
return err(`unknown command: ${sub}`);
|
|
33
|
+
}
|
|
34
|
+
let access = "spectate";
|
|
35
|
+
let expire;
|
|
36
|
+
let pass;
|
|
37
|
+
let name;
|
|
38
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
39
|
+
const t = flagTokens[i];
|
|
40
|
+
if (t === void 0) break;
|
|
41
|
+
const next = flagTokens[i + 1];
|
|
42
|
+
if (t === "--spectate") {
|
|
43
|
+
access = "spectate";
|
|
44
|
+
} else if (t === "--invite") {
|
|
45
|
+
access = "invite";
|
|
46
|
+
} else if (t === "--expire") {
|
|
47
|
+
if (next === void 0) return err("--expire requires a value (1h | 24h)");
|
|
48
|
+
if (next !== "1h" && next !== "24h") {
|
|
49
|
+
return err(`--expire must be "1h" or "24h", got: ${next}`);
|
|
50
|
+
}
|
|
51
|
+
expire = next;
|
|
52
|
+
i++;
|
|
53
|
+
} else if (t === "--pass") {
|
|
54
|
+
if (next === void 0) return err("--pass requires a value");
|
|
55
|
+
pass = next;
|
|
56
|
+
i++;
|
|
57
|
+
} else if (t === "--name") {
|
|
58
|
+
if (next === void 0) return err("--name requires a value");
|
|
59
|
+
name = next;
|
|
60
|
+
i++;
|
|
61
|
+
} else {
|
|
62
|
+
return err(`unknown flag: ${t}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (command.length === 0) {
|
|
66
|
+
return err('vibeshare needs a command after "--", e.g. `vibeshare --invite -- claude`');
|
|
67
|
+
}
|
|
68
|
+
return { cmd: "host", command, access, expire, pass, name };
|
|
69
|
+
}
|
|
70
|
+
var HELP = `vibeshare ${VERSION} \u2014 share your live agent session by URL
|
|
71
|
+
|
|
72
|
+
USAGE
|
|
73
|
+
vibeshare [--spectate|--invite] [--expire 1h|24h] [--pass <p>] -- <command...>
|
|
74
|
+
vibeshare host [the flags above] -- <command...>
|
|
75
|
+
Host a session running <command> (e.g. \`vibeshare --invite -- claude\`)
|
|
76
|
+
and print its share URL. You start as the driver.
|
|
77
|
+
|
|
78
|
+
--spectate link holders get read-only access (default)
|
|
79
|
+
--invite link holders may request to join; approve to let them drive
|
|
80
|
+
--expire auto-revoke the share after 1h or 24h
|
|
81
|
+
--pass <p> require a passphrase on top of the share URL
|
|
82
|
+
--name <s> display name for the host participant
|
|
83
|
+
|
|
84
|
+
vibeshare stop stop the foreground host session (also: /stop inside it)
|
|
85
|
+
vibeshare viewers (in-foreground) list viewers / pending join requests
|
|
86
|
+
vibeshare mcp run the vibeshare MCP server on stdio
|
|
87
|
+
vibeshare --version
|
|
88
|
+
vibeshare --help
|
|
89
|
+
|
|
90
|
+
v0 is foreground, host-authoritative, local/LAN. The share URL points at your own
|
|
91
|
+
domain (vibeshare.stream); the live stream runs over a self-hostable e2e relay.
|
|
92
|
+
${BADGE}
|
|
93
|
+
`;
|
|
94
|
+
function printHelp(stream = process.stdout) {
|
|
95
|
+
stream.write(HELP);
|
|
96
|
+
}
|
|
97
|
+
function lanAddress() {
|
|
98
|
+
const nets = networkInterfaces();
|
|
99
|
+
for (const list of Object.values(nets)) {
|
|
100
|
+
if (!list) continue;
|
|
101
|
+
for (const n of list) {
|
|
102
|
+
if (n && n.family === "IPv4" && !n.internal) return n.address;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
async function runHost(p) {
|
|
108
|
+
const host = createHost({ command: p.command });
|
|
109
|
+
const relay = await createRelay({
|
|
110
|
+
port: 0,
|
|
111
|
+
hostHandle: host,
|
|
112
|
+
initialDriver: HOST_ID,
|
|
113
|
+
hostParticipantName: p.name ?? HOST_ID
|
|
114
|
+
});
|
|
115
|
+
relay.consent.grant(SHARE_SESSION_SCOPE);
|
|
116
|
+
let expired = false;
|
|
117
|
+
const share = createShare({
|
|
118
|
+
session: relay,
|
|
119
|
+
access: p.access,
|
|
120
|
+
expiry: p.expire,
|
|
121
|
+
passphrase: p.pass,
|
|
122
|
+
onRevoke: (reason) => {
|
|
123
|
+
if (reason === "expired") expired = true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
host.onOutput((entry) => process.stdout.write(entry.text));
|
|
127
|
+
const accessLabel = p.access + (p.pass ? " \xB7 passphrase" : "") + (p.expire ? ` \xB7 expires ${p.expire}` : " \xB7 no expiry");
|
|
128
|
+
process.stderr.write(`vibeshare \u2014 sharing ${p.command.join(" ")}
|
|
129
|
+
`);
|
|
130
|
+
process.stderr.write(` share: ${share.url}
|
|
131
|
+
`);
|
|
132
|
+
process.stderr.write(` access: ${accessLabel}
|
|
133
|
+
`);
|
|
134
|
+
process.stderr.write(` relay: ${relay.url}
|
|
135
|
+
`);
|
|
136
|
+
const lan = lanAddress();
|
|
137
|
+
if (lan) {
|
|
138
|
+
process.stderr.write(` lan: ws://${lan}:${relay.port}
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
process.stderr.write(` ${BADGE}
|
|
142
|
+
`);
|
|
143
|
+
process.stderr.write(
|
|
144
|
+
` you are the driver. /release to hand off, /drive to take back, /viewers, /approve <id>, /stop to end.
|
|
145
|
+
`
|
|
146
|
+
);
|
|
147
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
148
|
+
const printViewers = () => {
|
|
149
|
+
const roster = share.viewers();
|
|
150
|
+
if (roster.viewers.length === 0 && roster.pending.length === 0) {
|
|
151
|
+
process.stderr.write(" (no viewers connected)\n");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const v of roster.viewers) {
|
|
155
|
+
process.stderr.write(` \u25CF ${v.name} [${v.role}]
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
for (const v of roster.pending) {
|
|
159
|
+
process.stderr.write(` \u2026 ${v.name} [wants to join \u2014 /approve ${v.id}]
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
rl.on("line", (line) => {
|
|
164
|
+
if (line === "/stop" || line === "/quit") {
|
|
165
|
+
void shutdown(0);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (line === "/viewers") {
|
|
169
|
+
printViewers();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (line.startsWith("/approve ")) {
|
|
173
|
+
const id = line.slice("/approve ".length).trim();
|
|
174
|
+
const ok = share.approve(id);
|
|
175
|
+
process.stderr.write(ok ? ` promoted ${id}.
|
|
176
|
+
` : ` can't promote ${id} (unknown / spectate-only).
|
|
177
|
+
`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (line === "/release") {
|
|
181
|
+
relay.localReleaseControl(HOST_ID);
|
|
182
|
+
process.stderr.write(" control released.\n");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (line === "/drive") {
|
|
186
|
+
relay.localRequestControl(HOST_ID);
|
|
187
|
+
process.stderr.write(" control requested.\n");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (relay.arbiter.isDriver(HOST_ID)) {
|
|
191
|
+
host.sendInput(`${line}
|
|
192
|
+
`);
|
|
193
|
+
} else {
|
|
194
|
+
process.stderr.write(" (not the driver \u2014 /drive to take control)\n");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
rl.on("SIGINT", () => void shutdown(0));
|
|
198
|
+
let shuttingDown = false;
|
|
199
|
+
const shutdown = async (code2) => {
|
|
200
|
+
if (shuttingDown) return;
|
|
201
|
+
shuttingDown = true;
|
|
202
|
+
rl.close();
|
|
203
|
+
host.kill();
|
|
204
|
+
await share.revoke();
|
|
205
|
+
process.exit(code2);
|
|
206
|
+
};
|
|
207
|
+
process.on("SIGINT", () => void shutdown(130));
|
|
208
|
+
process.on("SIGTERM", () => void shutdown(143));
|
|
209
|
+
if (p.expire !== void 0) {
|
|
210
|
+
void promiseTrue(() => share.revoked).then(async () => {
|
|
211
|
+
if (expired) {
|
|
212
|
+
process.stderr.write(" share expired \u2014 tearing down.\n");
|
|
213
|
+
await shutdown(0);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const code = await host.exited;
|
|
218
|
+
await shutdown(code ?? 0);
|
|
219
|
+
return code ?? 0;
|
|
220
|
+
}
|
|
221
|
+
function promiseTrue(pred) {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
const tick = () => {
|
|
224
|
+
if (pred()) resolve();
|
|
225
|
+
else setTimeout(tick, 250);
|
|
226
|
+
};
|
|
227
|
+
tick();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function runStop() {
|
|
231
|
+
process.stderr.write(
|
|
232
|
+
"vibeshare runs the host in the foreground. To stop it, switch to that terminal and\npress Ctrl+C (or type /stop). v0 does not yet daemonize or keep cross-process state.\n"
|
|
233
|
+
);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
async function runViewers() {
|
|
237
|
+
process.stderr.write(
|
|
238
|
+
"vibeshare runs the host in the foreground. Inside that session, type /viewers to list\nconnected viewers and pending join requests, and /approve <id> to promote one.\n"
|
|
239
|
+
);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
async function main(argv = process.argv.slice(2)) {
|
|
243
|
+
const parsed = parseArgs(argv);
|
|
244
|
+
switch (parsed.cmd) {
|
|
245
|
+
case "help":
|
|
246
|
+
printHelp();
|
|
247
|
+
return 0;
|
|
248
|
+
case "version":
|
|
249
|
+
process.stdout.write(`${VERSION}
|
|
250
|
+
`);
|
|
251
|
+
return 0;
|
|
252
|
+
case "mcp": {
|
|
253
|
+
const { runMcpStdio } = await import("./mcp.js");
|
|
254
|
+
await runMcpStdio();
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
case "stop":
|
|
258
|
+
return runStop();
|
|
259
|
+
case "viewers":
|
|
260
|
+
return runViewers();
|
|
261
|
+
case "host":
|
|
262
|
+
return runHost(parsed);
|
|
263
|
+
case "error":
|
|
264
|
+
process.stderr.write(`vibeshare: ${parsed.message}
|
|
265
|
+
`);
|
|
266
|
+
printHelp(process.stderr);
|
|
267
|
+
return 2;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
271
|
+
void main().then((code) => process.exit(code));
|
|
272
|
+
}
|
|
273
|
+
export {
|
|
274
|
+
main,
|
|
275
|
+
parseArgs,
|
|
276
|
+
printHelp
|
|
277
|
+
};
|