@claudecollab/cli 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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/package.json +36 -0
  4. package/packages/cli/bin/claude-share.js +1319 -0
  5. package/packages/cli/package.json +15 -0
  6. package/packages/cli/src/brain/card.js +157 -0
  7. package/packages/cli/src/brain/commands.js +122 -0
  8. package/packages/cli/src/brain/drafts.js +557 -0
  9. package/packages/cli/src/brain/gate.js +134 -0
  10. package/packages/cli/src/brain/knocks.js +27 -0
  11. package/packages/cli/src/brain/log.js +123 -0
  12. package/packages/cli/src/brain/queue.js +81 -0
  13. package/packages/cli/src/brain/state.js +245 -0
  14. package/packages/cli/src/hooks.js +243 -0
  15. package/packages/cli/src/invite.js +43 -0
  16. package/packages/cli/src/known-relays.js +66 -0
  17. package/packages/cli/src/pty.js +175 -0
  18. package/packages/cli/src/relay-client.js +271 -0
  19. package/packages/cli/src/renderer.js +230 -0
  20. package/packages/cli/src/screen-snapshot.js +139 -0
  21. package/packages/cli/src/term-chatter.js +89 -0
  22. package/packages/relay/auth.js +18 -0
  23. package/packages/relay/bin/serve.js +73 -0
  24. package/packages/relay/names.js +27 -0
  25. package/packages/relay/package.json +13 -0
  26. package/packages/relay/public/client.js +1553 -0
  27. package/packages/relay/public/index.html +110 -0
  28. package/packages/relay/public/style.css +952 -0
  29. package/packages/relay/rooms.js +149 -0
  30. package/packages/relay/server.js +652 -0
  31. package/packages/relay/web.js +334 -0
  32. package/packages/relay/wordlists.js +38 -0
  33. package/packages/shared/package.json +8 -0
  34. package/packages/shared/protocol.js +189 -0
@@ -0,0 +1,149 @@
1
+ // Relay room registry — pure state, no ssh. Hands out unique room codes, tracks
2
+ // guests/bans/cap per room, runs the 10-minute host-drop TTL, and rate-limits
3
+ // knock attempts per ip. The ssh server (Task 2) wires this to real connections.
4
+
5
+ import { adjectives as defaultAdjectives, animals as defaultAnimals } from './wordlists.js';
6
+
7
+ const DEFAULT_TTL_MS = 10 * 60 * 1000; // spec: code survives a 10-min host drop
8
+ const DEFAULT_CAP = 8; // spec: room cap, composer stays legible
9
+ const DEFAULT_KNOCK_LIMIT = 5; // spec: per-room lockout — 5 knock attempts/min per ip
10
+ const DEFAULT_KNOCK_WINDOW_MS = 60 * 1000;
11
+
12
+ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
13
+
14
+ /**
15
+ * Create an isolated room registry. All timing is injectable so tests stay
16
+ * deterministic: `now` for knock windows, node:test fake timers for the TTL.
17
+ *
18
+ * @param {object} [opts]
19
+ * @param {number} [opts.ttlMs] host-drop grace period before close
20
+ * @param {number} [opts.cap] max guests per room
21
+ * @param {number} [opts.knockLimit] knock attempts allowed per window per ip
22
+ * @param {number} [opts.knockWindowMs] sliding window for the knock limit
23
+ * @param {() => number} [opts.now] millisecond clock (defaults to Date.now)
24
+ * @param {{adjectives:string[], animals:string[]}} [opts.wordlists]
25
+ */
26
+ export function createRegistry(opts = {}) {
27
+ const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
28
+ const cap = opts.cap ?? DEFAULT_CAP;
29
+ const knockLimit = opts.knockLimit ?? DEFAULT_KNOCK_LIMIT;
30
+ const knockWindowMs = opts.knockWindowMs ?? DEFAULT_KNOCK_WINDOW_MS;
31
+ const now = opts.now ?? (() => Date.now());
32
+ const adjectives = opts.wordlists?.adjectives ?? defaultAdjectives;
33
+ const animals = opts.wordlists?.animals ?? defaultAnimals;
34
+
35
+ /** @type {Map<string, object>} code -> room */
36
+ const rooms = new Map();
37
+
38
+ function makeCode() {
39
+ for (let i = 0; i < 5; i++) {
40
+ const code = `${pick(adjectives)}-${pick(animals)}`;
41
+ if (!rooms.has(code)) return code;
42
+ }
43
+ // 5 collisions: widen to a third word (spec fallback).
44
+ for (let i = 0; i < 50; i++) {
45
+ const code = `${pick(adjectives)}-${pick(animals)}-${pick(adjectives)}`;
46
+ if (!rooms.has(code)) return code;
47
+ }
48
+ // Pathologically tiny wordlists: guarantee uniqueness with a numeric suffix.
49
+ const base = `${pick(adjectives)}-${pick(animals)}-${pick(adjectives)}`;
50
+ let n = 2;
51
+ while (rooms.has(`${base}-${n}`)) n++;
52
+ return `${base}-${n}`;
53
+ }
54
+
55
+ function create() {
56
+ const code = makeCode();
57
+ const room = {
58
+ code,
59
+ guests: new Map(), // id -> {name, fp, role}
60
+ banned: new Set(), // banned fingerprints
61
+ cap,
62
+ hostPresent: true,
63
+ createdAt: now(),
64
+ _ttlTimer: null, // pending host-drop close timer
65
+ _knocks: new Map(), // ip -> number[] (recent knock timestamps)
66
+ };
67
+ rooms.set(code, room);
68
+ return room;
69
+ }
70
+
71
+ function get(code) {
72
+ return rooms.get(code);
73
+ }
74
+
75
+ function addGuest(code, id, { name, fp, role } = {}) {
76
+ const room = rooms.get(code);
77
+ if (!room) return false;
78
+ if (fp && room.banned.has(fp)) return false;
79
+ if (room.guests.size >= room.cap) return false;
80
+ room.guests.set(id, { name, fp, role: role ?? 'prompter' });
81
+ return true;
82
+ }
83
+
84
+ function removeGuest(code, id) {
85
+ const room = rooms.get(code);
86
+ if (!room) return false;
87
+ return room.guests.delete(id);
88
+ }
89
+
90
+ // Blocklist a fingerprint for the room's lifetime and evict any live guests
91
+ // holding it (spec §host controls: /kick drops + blocklists the pubkey).
92
+ function ban(code, fp) {
93
+ const room = rooms.get(code);
94
+ if (!room) return;
95
+ room.banned.add(fp);
96
+ for (const [id, g] of room.guests) {
97
+ if (g.fp === fp) room.guests.delete(id);
98
+ }
99
+ }
100
+
101
+ // Host connection dropped: start the grace timer that closes the room.
102
+ function hostDropped(code) {
103
+ const room = rooms.get(code);
104
+ if (!room) return;
105
+ room.hostPresent = false;
106
+ if (room._ttlTimer) clearTimeout(room._ttlTimer);
107
+ room._ttlTimer = setTimeout(() => close(code), ttlMs);
108
+ if (typeof room._ttlTimer?.unref === 'function') room._ttlTimer.unref();
109
+ }
110
+
111
+ // Host reconnected in time: cancel the pending close.
112
+ function hostReturned(code) {
113
+ const room = rooms.get(code);
114
+ if (!room) return;
115
+ room.hostPresent = true;
116
+ if (room._ttlTimer) {
117
+ clearTimeout(room._ttlTimer);
118
+ room._ttlTimer = null;
119
+ }
120
+ }
121
+
122
+ function close(code) {
123
+ const room = rooms.get(code);
124
+ if (!room) return;
125
+ if (room._ttlTimer) {
126
+ clearTimeout(room._ttlTimer);
127
+ room._ttlTimer = null;
128
+ }
129
+ rooms.delete(code);
130
+ }
131
+
132
+ // Record + rate-check a knock attempt. Returns false if the room is gone or
133
+ // this ip has used up its allowance inside the sliding window.
134
+ function tryKnock(code, ip) {
135
+ const room = rooms.get(code);
136
+ if (!room) return false;
137
+ const t = now();
138
+ const recent = (room._knocks.get(ip) ?? []).filter((ts) => t - ts < knockWindowMs);
139
+ if (recent.length >= knockLimit) {
140
+ room._knocks.set(ip, recent);
141
+ return false;
142
+ }
143
+ recent.push(t);
144
+ room._knocks.set(ip, recent);
145
+ return true;
146
+ }
147
+
148
+ return { create, get, addGuest, removeGuest, ban, hostDropped, hostReturned, close, tryKnock };
149
+ }