@studiomopoke/crosschat 1.6.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 (169) hide show
  1. package/LICENSE +62 -0
  2. package/README.md +279 -0
  3. package/bin/cli.cjs +631 -0
  4. package/crosschat.md +196 -0
  5. package/dashboard/dist/assets/index-Blgmbgo_.css +1 -0
  6. package/dashboard/dist/assets/index-DzcjxzDR.js +49 -0
  7. package/dashboard/dist/index.html +13 -0
  8. package/dist/dashboard/dashboard-listener.d.ts +22 -0
  9. package/dist/dashboard/dashboard-listener.d.ts.map +1 -0
  10. package/dist/dashboard/dashboard-listener.js +124 -0
  11. package/dist/dashboard/dashboard-listener.js.map +1 -0
  12. package/dist/dashboard/http-server.d.ts +25 -0
  13. package/dist/dashboard/http-server.d.ts.map +1 -0
  14. package/dist/dashboard/http-server.js +281 -0
  15. package/dist/dashboard/http-server.js.map +1 -0
  16. package/dist/dashboard/message-bridge.d.ts +19 -0
  17. package/dist/dashboard/message-bridge.d.ts.map +1 -0
  18. package/dist/dashboard/message-bridge.js +48 -0
  19. package/dist/dashboard/message-bridge.js.map +1 -0
  20. package/dist/hub/agent-connection.d.ts +101 -0
  21. package/dist/hub/agent-connection.d.ts.map +1 -0
  22. package/dist/hub/agent-connection.js +383 -0
  23. package/dist/hub/agent-connection.js.map +1 -0
  24. package/dist/hub/hub-main.d.ts +2 -0
  25. package/dist/hub/hub-main.d.ts.map +1 -0
  26. package/dist/hub/hub-main.js +16 -0
  27. package/dist/hub/hub-main.js.map +1 -0
  28. package/dist/hub/hub-server.d.ts +8 -0
  29. package/dist/hub/hub-server.d.ts.map +1 -0
  30. package/dist/hub/hub-server.js +1500 -0
  31. package/dist/hub/hub-server.js.map +1 -0
  32. package/dist/hub/protocol.d.ts +221 -0
  33. package/dist/hub/protocol.d.ts.map +1 -0
  34. package/dist/hub/protocol.js +20 -0
  35. package/dist/hub/protocol.js.map +1 -0
  36. package/dist/hub/task-manager.d.ts +68 -0
  37. package/dist/hub/task-manager.d.ts.map +1 -0
  38. package/dist/hub/task-manager.js +250 -0
  39. package/dist/hub/task-manager.js.map +1 -0
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +7 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/lifecycle.d.ts +2 -0
  45. package/dist/lifecycle.d.ts.map +1 -0
  46. package/dist/lifecycle.js +234 -0
  47. package/dist/lifecycle.js.map +1 -0
  48. package/dist/prompts.d.ts +3 -0
  49. package/dist/prompts.d.ts.map +1 -0
  50. package/dist/prompts.js +48 -0
  51. package/dist/prompts.js.map +1 -0
  52. package/dist/registry/cleanup.d.ts +2 -0
  53. package/dist/registry/cleanup.d.ts.map +1 -0
  54. package/dist/registry/cleanup.js +60 -0
  55. package/dist/registry/cleanup.js.map +1 -0
  56. package/dist/registry/registry.d.ts +11 -0
  57. package/dist/registry/registry.d.ts.map +1 -0
  58. package/dist/registry/registry.js +82 -0
  59. package/dist/registry/registry.js.map +1 -0
  60. package/dist/server.d.ts +9 -0
  61. package/dist/server.d.ts.map +1 -0
  62. package/dist/server.js +91 -0
  63. package/dist/server.js.map +1 -0
  64. package/dist/stores/message-store.d.ts +21 -0
  65. package/dist/stores/message-store.d.ts.map +1 -0
  66. package/dist/stores/message-store.js +83 -0
  67. package/dist/stores/message-store.js.map +1 -0
  68. package/dist/stores/task-store.d.ts +15 -0
  69. package/dist/stores/task-store.d.ts.map +1 -0
  70. package/dist/stores/task-store.js +57 -0
  71. package/dist/stores/task-store.js.map +1 -0
  72. package/dist/tools/accept-claim.d.ts +4 -0
  73. package/dist/tools/accept-claim.d.ts.map +1 -0
  74. package/dist/tools/accept-claim.js +27 -0
  75. package/dist/tools/accept-claim.js.map +1 -0
  76. package/dist/tools/chat-send.d.ts +4 -0
  77. package/dist/tools/chat-send.d.ts.map +1 -0
  78. package/dist/tools/chat-send.js +19 -0
  79. package/dist/tools/chat-send.js.map +1 -0
  80. package/dist/tools/claim-task.d.ts +4 -0
  81. package/dist/tools/claim-task.d.ts.map +1 -0
  82. package/dist/tools/claim-task.js +26 -0
  83. package/dist/tools/claim-task.js.map +1 -0
  84. package/dist/tools/clear-session.d.ts +5 -0
  85. package/dist/tools/clear-session.d.ts.map +1 -0
  86. package/dist/tools/clear-session.js +41 -0
  87. package/dist/tools/clear-session.js.map +1 -0
  88. package/dist/tools/complete-task.d.ts +4 -0
  89. package/dist/tools/complete-task.d.ts.map +1 -0
  90. package/dist/tools/complete-task.js +29 -0
  91. package/dist/tools/complete-task.js.map +1 -0
  92. package/dist/tools/create-room.d.ts +4 -0
  93. package/dist/tools/create-room.d.ts.map +1 -0
  94. package/dist/tools/create-room.js +28 -0
  95. package/dist/tools/create-room.js.map +1 -0
  96. package/dist/tools/delegate-task.d.ts +4 -0
  97. package/dist/tools/delegate-task.d.ts.map +1 -0
  98. package/dist/tools/delegate-task.js +32 -0
  99. package/dist/tools/delegate-task.js.map +1 -0
  100. package/dist/tools/get-messages.d.ts +4 -0
  101. package/dist/tools/get-messages.d.ts.map +1 -0
  102. package/dist/tools/get-messages.js +35 -0
  103. package/dist/tools/get-messages.js.map +1 -0
  104. package/dist/tools/get-room-digest.d.ts +4 -0
  105. package/dist/tools/get-room-digest.d.ts.map +1 -0
  106. package/dist/tools/get-room-digest.js +63 -0
  107. package/dist/tools/get-room-digest.js.map +1 -0
  108. package/dist/tools/get-task-status.d.ts +4 -0
  109. package/dist/tools/get-task-status.d.ts.map +1 -0
  110. package/dist/tools/get-task-status.js +26 -0
  111. package/dist/tools/get-task-status.js.map +1 -0
  112. package/dist/tools/join-room.d.ts +4 -0
  113. package/dist/tools/join-room.d.ts.map +1 -0
  114. package/dist/tools/join-room.js +26 -0
  115. package/dist/tools/join-room.js.map +1 -0
  116. package/dist/tools/list-peers.d.ts +4 -0
  117. package/dist/tools/list-peers.d.ts.map +1 -0
  118. package/dist/tools/list-peers.js +26 -0
  119. package/dist/tools/list-peers.js.map +1 -0
  120. package/dist/tools/list-tasks.d.ts +4 -0
  121. package/dist/tools/list-tasks.d.ts.map +1 -0
  122. package/dist/tools/list-tasks.js +28 -0
  123. package/dist/tools/list-tasks.js.map +1 -0
  124. package/dist/tools/send-message.d.ts +4 -0
  125. package/dist/tools/send-message.d.ts.map +1 -0
  126. package/dist/tools/send-message.js +28 -0
  127. package/dist/tools/send-message.js.map +1 -0
  128. package/dist/tools/set-status.d.ts +4 -0
  129. package/dist/tools/set-status.d.ts.map +1 -0
  130. package/dist/tools/set-status.js +28 -0
  131. package/dist/tools/set-status.js.map +1 -0
  132. package/dist/tools/update-task.d.ts +4 -0
  133. package/dist/tools/update-task.d.ts.map +1 -0
  134. package/dist/tools/update-task.js +28 -0
  135. package/dist/tools/update-task.js.map +1 -0
  136. package/dist/tools/wait-for-messages.d.ts +4 -0
  137. package/dist/tools/wait-for-messages.d.ts.map +1 -0
  138. package/dist/tools/wait-for-messages.js +84 -0
  139. package/dist/tools/wait-for-messages.js.map +1 -0
  140. package/dist/transport/peer-protocol.d.ts +7 -0
  141. package/dist/transport/peer-protocol.d.ts.map +1 -0
  142. package/dist/transport/peer-protocol.js +30 -0
  143. package/dist/transport/peer-protocol.js.map +1 -0
  144. package/dist/transport/uds-client.d.ts +3 -0
  145. package/dist/transport/uds-client.d.ts.map +1 -0
  146. package/dist/transport/uds-client.js +57 -0
  147. package/dist/transport/uds-client.js.map +1 -0
  148. package/dist/transport/uds-server.d.ts +12 -0
  149. package/dist/transport/uds-server.d.ts.map +1 -0
  150. package/dist/transport/uds-server.js +87 -0
  151. package/dist/transport/uds-server.js.map +1 -0
  152. package/dist/types.d.ts +18 -0
  153. package/dist/types.d.ts.map +1 -0
  154. package/dist/types.js +3 -0
  155. package/dist/types.js.map +1 -0
  156. package/dist/util/id.d.ts +2 -0
  157. package/dist/util/id.d.ts.map +1 -0
  158. package/dist/util/id.js +5 -0
  159. package/dist/util/id.js.map +1 -0
  160. package/dist/util/logger.d.ts +3 -0
  161. package/dist/util/logger.d.ts.map +1 -0
  162. package/dist/util/logger.js +13 -0
  163. package/dist/util/logger.js.map +1 -0
  164. package/dist/util/pid.d.ts +2 -0
  165. package/dist/util/pid.d.ts.map +1 -0
  166. package/dist/util/pid.js +10 -0
  167. package/dist/util/pid.js.map +1 -0
  168. package/hooks/permission-hook.sh +116 -0
  169. package/package.json +59 -0
package/bin/cli.cjs ADDED
@@ -0,0 +1,631 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const pkg = require("../package.json");
8
+
9
+ const CLAUDE_JSON = path.join(os.homedir(), ".claude.json");
10
+ const SETTINGS_JSON = path.join(os.homedir(), ".claude", "settings.json");
11
+ const COMMANDS_DIR = path.join(os.homedir(), ".claude", "commands");
12
+ const COMMAND_SOURCE = path.join(__dirname, "..", "crosschat.md");
13
+ const COMMAND_TARGET = path.join(COMMANDS_DIR, "crosschat.md");
14
+ const HOOK_SOURCE = path.join(__dirname, "..", "hooks", "permission-hook.sh");
15
+ const MCP_KEY = "crosschat";
16
+
17
+ const CROSSCHAT_PERMISSIONS = [
18
+ "mcp__crosschat__wait_for_messages",
19
+ "mcp__crosschat__get_messages",
20
+ "mcp__crosschat__list_peers",
21
+ "mcp__crosschat__send_message",
22
+ "mcp__crosschat__set_status",
23
+ "mcp__crosschat__complete_task",
24
+ "mcp__crosschat__delegate_task",
25
+ "mcp__crosschat__get_task_status",
26
+ "mcp__crosschat__join_room",
27
+ "mcp__crosschat__create_room",
28
+ "mcp__crosschat__claim_task",
29
+ "mcp__crosschat__accept_claim",
30
+ "mcp__crosschat__update_task",
31
+ "mcp__crosschat__list_tasks",
32
+ "mcp__crosschat__clear_session",
33
+ "mcp__crosschat__get_room_digest",
34
+ ];
35
+
36
+ const { spawn } = require("child_process");
37
+
38
+ const command = process.argv[2];
39
+ const subcommand = process.argv[3];
40
+
41
+ function ensureDir(dir) {
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ }
46
+
47
+ function readSettings(filePath) {
48
+ if (!fs.existsSync(filePath)) return {};
49
+ try {
50
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+
56
+ function writeSettings(filePath, settings) {
57
+ const dir = path.dirname(filePath);
58
+ ensureDir(dir);
59
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n", "utf8");
60
+ }
61
+
62
+ function resolveServerEntry() {
63
+ // Try to find the server in common locations
64
+ const candidates = [
65
+ // Global install
66
+ path.join(__dirname, "..", "dist", "index.js"),
67
+ // npm root -g based
68
+ path.join(
69
+ process.env.npm_config_prefix || "/usr/local",
70
+ "lib",
71
+ "node_modules",
72
+ "@studiomopoke",
73
+ "crosschat",
74
+ "dist",
75
+ "index.js"
76
+ ),
77
+ ];
78
+
79
+ for (const candidate of candidates) {
80
+ if (fs.existsSync(candidate)) {
81
+ return candidate;
82
+ }
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ function install() {
89
+ const serverEntry = resolveServerEntry();
90
+
91
+ // 1. Install MCP server config to ~/.claude.json
92
+ const claudeJson = readSettings(CLAUDE_JSON);
93
+
94
+ if (!claudeJson.mcpServers) {
95
+ claudeJson.mcpServers = {};
96
+ }
97
+
98
+ const isUpdate = !!claudeJson.mcpServers[MCP_KEY];
99
+
100
+ if (serverEntry) {
101
+ // Direct path — works for global installs and local dev
102
+ claudeJson.mcpServers[MCP_KEY] = {
103
+ command: "node",
104
+ args: [serverEntry],
105
+ };
106
+ } else {
107
+ // Fallback to npx — works when installed via npx
108
+ claudeJson.mcpServers[MCP_KEY] = {
109
+ command: "npx",
110
+ args: ["-y", "@studiomopoke/crosschat", "serve"],
111
+ };
112
+ }
113
+
114
+ writeSettings(CLAUDE_JSON, claudeJson);
115
+
116
+ // 2. Add CrossChat tool permissions to ~/.claude/settings.json
117
+ const settings = readSettings(SETTINGS_JSON);
118
+ if (!settings.permissions) settings.permissions = {};
119
+ if (!settings.permissions.allow) settings.permissions.allow = [];
120
+ const existingPerms = new Set(settings.permissions.allow);
121
+ let addedPerms = 0;
122
+ for (const perm of CROSSCHAT_PERMISSIONS) {
123
+ if (!existingPerms.has(perm)) {
124
+ settings.permissions.allow.push(perm);
125
+ addedPerms++;
126
+ }
127
+ }
128
+ // 2b. Add permission hook to settings (PreToolUse)
129
+ if (fs.existsSync(HOOK_SOURCE)) {
130
+ if (!settings.hooks) settings.hooks = {};
131
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
132
+
133
+ const hookCommand = HOOK_SOURCE;
134
+ const alreadyInstalled = settings.hooks.PreToolUse.some((entry) =>
135
+ entry.hooks &&
136
+ entry.hooks.some((h) => h.command && h.command.includes("permission-hook.sh"))
137
+ );
138
+
139
+ if (!alreadyInstalled) {
140
+ settings.hooks.PreToolUse.push({
141
+ matcher: "",
142
+ hooks: [
143
+ {
144
+ type: "command",
145
+ command: hookCommand,
146
+ timeout: 300,
147
+ },
148
+ ],
149
+ });
150
+ }
151
+ }
152
+
153
+ if (addedPerms > 0 || fs.existsSync(HOOK_SOURCE)) {
154
+ writeSettings(SETTINGS_JSON, settings);
155
+ }
156
+
157
+ // 3. Install /crosschat command
158
+ ensureDir(COMMANDS_DIR);
159
+ fs.copyFileSync(COMMAND_SOURCE, COMMAND_TARGET);
160
+
161
+ if (isUpdate) {
162
+ console.log("Updated CrossChat in " + CLAUDE_JSON);
163
+ } else {
164
+ console.log("Installed CrossChat MCP server to " + CLAUDE_JSON);
165
+ }
166
+
167
+ console.log("Installed /crosschat command to " + COMMAND_TARGET);
168
+ if (fs.existsSync(HOOK_SOURCE)) {
169
+ console.log("Installed permission hook (PreToolUse → dashboard)");
170
+ }
171
+ console.log("");
172
+ if (serverEntry) {
173
+ console.log(" MCP server: " + serverEntry);
174
+ } else {
175
+ console.log(" MCP server: via npx @studiomopoke/crosschat serve");
176
+ }
177
+ console.log("");
178
+ console.log("Restart Claude Code, then run /crosschat to start collaborating.");
179
+ }
180
+
181
+ function uninstall() {
182
+ let removedAnything = false;
183
+
184
+ // 1. Remove MCP server config from ~/.claude.json
185
+ const claudeJson = readSettings(CLAUDE_JSON);
186
+ if (claudeJson.mcpServers && claudeJson.mcpServers[MCP_KEY]) {
187
+ delete claudeJson.mcpServers[MCP_KEY];
188
+ if (Object.keys(claudeJson.mcpServers).length === 0) {
189
+ delete claudeJson.mcpServers;
190
+ }
191
+ writeSettings(CLAUDE_JSON, claudeJson);
192
+ console.log("Removed CrossChat MCP server from " + CLAUDE_JSON);
193
+ removedAnything = true;
194
+ }
195
+
196
+ // 2. Remove permission hook from settings
197
+ const settings = readSettings(SETTINGS_JSON);
198
+ if (settings.hooks && settings.hooks.PreToolUse) {
199
+ const before = settings.hooks.PreToolUse.length;
200
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) =>
201
+ !(entry.hooks && entry.hooks.some((h) => h.command && h.command.includes("permission-hook.sh")))
202
+ );
203
+ if (settings.hooks.PreToolUse.length < before) {
204
+ if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
205
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
206
+ writeSettings(SETTINGS_JSON, settings);
207
+ console.log("Removed permission hook from " + SETTINGS_JSON);
208
+ removedAnything = true;
209
+ }
210
+ }
211
+
212
+ // 3. Remove /crosschat command
213
+ if (fs.existsSync(COMMAND_TARGET)) {
214
+ fs.unlinkSync(COMMAND_TARGET);
215
+ console.log("Removed /crosschat command");
216
+ removedAnything = true;
217
+ }
218
+
219
+ if (!removedAnything) {
220
+ console.log("CrossChat is not installed — nothing to remove.");
221
+ }
222
+ }
223
+
224
+ function serve() {
225
+ // MCP server entry point — used when config calls `npx crosschat serve`
226
+ const serverPath = path.join(__dirname, "..", "dist", "index.js");
227
+ if (!fs.existsSync(serverPath)) {
228
+ console.error("Error: dist/index.js not found.");
229
+ process.exit(1);
230
+ }
231
+
232
+ // If a hub is already running, stop it first (prevents stale code issues)
233
+ const lock = readLockFile();
234
+ if (lock && lock.pid) {
235
+ let isRunning = false;
236
+ try { process.kill(lock.pid, 0); isRunning = true; } catch {}
237
+
238
+ if (isRunning) {
239
+ const staleVersion = lock.version && lock.version !== pkg.version;
240
+ if (staleVersion) {
241
+ console.log("Stopping stale hub (v" + lock.version + " → v" + pkg.version + ", PID " + lock.pid + ")...");
242
+ } else {
243
+ console.log("Stopping existing hub (PID " + lock.pid + ")...");
244
+ }
245
+ stop();
246
+ } else {
247
+ // Stale lock file — clean it up
248
+ const lockFile = path.join(os.homedir(), ".crosschat", "dashboard.lock");
249
+ try { fs.unlinkSync(lockFile); } catch {}
250
+ }
251
+ }
252
+
253
+ // Dynamic import of ESM module
254
+ import(
255
+ "file://" + serverPath
256
+ ).catch((err) => {
257
+ console.error("Failed to start CrossChat server:", err.message);
258
+ process.exit(1);
259
+ });
260
+ }
261
+
262
+ async function status() {
263
+ const claudeJson = readSettings(CLAUDE_JSON);
264
+ const mcpConfigured = !!(
265
+ claudeJson.mcpServers && claudeJson.mcpServers[MCP_KEY]
266
+ );
267
+ const commandInstalled = fs.existsSync(COMMAND_TARGET);
268
+ const lock = readLockFile();
269
+ const hubPort = getHubPort();
270
+
271
+ console.log("CrossChat v" + pkg.version);
272
+ console.log("");
273
+ console.log("MCP server: " + (mcpConfigured ? "installed" : "not installed"));
274
+ console.log("/crosschat cmd: " + (commandInstalled ? "installed" : "not installed"));
275
+
276
+ if (!hubPort) {
277
+ console.log("Hub: not running");
278
+ return;
279
+ }
280
+
281
+ console.log("Hub: running on port " + hubPort + " (PID " + lock.pid + ")");
282
+ console.log("Dashboard: http://localhost:" + hubPort);
283
+
284
+ try {
285
+ const [peerList, taskList, roomList] = await Promise.all([
286
+ hubGet(hubPort, "/api/peers"),
287
+ hubGet(hubPort, "/api/tasks"),
288
+ hubGet(hubPort, "/api/rooms"),
289
+ ]);
290
+
291
+ const peers = Array.isArray(peerList) ? peerList : [];
292
+ const tasks = Array.isArray(taskList) ? taskList : [];
293
+ const roomArr = Array.isArray(roomList) ? roomList : [];
294
+
295
+ console.log("");
296
+ console.log("Agents: " + peers.length + " connected");
297
+ for (const p of peers) {
298
+ const badge = p.status === "busy" ? "[busy]" : "[avail]";
299
+ console.log(" " + badge + " " + p.name + " — " + (p.cwd || "?"));
300
+ }
301
+
302
+ if (tasks.length > 0) {
303
+ const byStatus = {};
304
+ for (const t of tasks) {
305
+ byStatus[t.status] = (byStatus[t.status] || 0) + 1;
306
+ }
307
+ const summary = Object.entries(byStatus).map(([k, v]) => v + " " + k).join(", ");
308
+ console.log("Tasks: " + tasks.length + " (" + summary + ")");
309
+ }
310
+
311
+ console.log("Rooms: " + roomArr.length);
312
+ } catch {
313
+ console.log("");
314
+ console.log("Hub not responding (port " + hubPort + ").");
315
+ }
316
+ }
317
+
318
+ function readLockFile() {
319
+ const lockFile = path.join(os.homedir(), ".crosschat", "dashboard.lock");
320
+ if (!fs.existsSync(lockFile)) return null;
321
+ try {
322
+ return JSON.parse(fs.readFileSync(lockFile, "utf8"));
323
+ } catch {
324
+ return null;
325
+ }
326
+ }
327
+
328
+ function getHubPort() {
329
+ const lock = readLockFile();
330
+ if (!lock || !lock.port) return null;
331
+ // Verify the process is actually running
332
+ if (lock.pid) {
333
+ try { process.kill(lock.pid, 0); } catch { return null; }
334
+ }
335
+ return lock.port;
336
+ }
337
+
338
+ function hubGet(port, apiPath) {
339
+ const http = require("http");
340
+ return new Promise((resolve, reject) => {
341
+ const req = http.get("http://localhost:" + port + apiPath, (res) => {
342
+ let body = "";
343
+ res.on("data", (chunk) => (body += chunk));
344
+ res.on("end", () => {
345
+ try { resolve(JSON.parse(body)); }
346
+ catch { reject(new Error("Invalid JSON from hub")); }
347
+ });
348
+ });
349
+ req.on("error", reject);
350
+ req.setTimeout(3000, () => { req.destroy(); reject(new Error("Timeout")); });
351
+ });
352
+ }
353
+
354
+ function requireHub() {
355
+ const port = getHubPort();
356
+ if (!port) {
357
+ console.error("Hub is not running. Start it with: crosschat start");
358
+ process.exit(1);
359
+ }
360
+ return port;
361
+ }
362
+
363
+ function timeAgo(isoDate) {
364
+ const seconds = Math.floor((Date.now() - new Date(isoDate).getTime()) / 1000);
365
+ if (seconds < 60) return seconds + "s ago";
366
+ const minutes = Math.floor(seconds / 60);
367
+ if (minutes < 60) return minutes + "m ago";
368
+ const hours = Math.floor(minutes / 60);
369
+ if (hours < 24) return hours + "h ago";
370
+ return Math.floor(hours / 24) + "d ago";
371
+ }
372
+
373
+ async function start() {
374
+ const existingPort = getHubPort();
375
+ if (existingPort) {
376
+ console.log("Hub already running on port " + existingPort);
377
+ console.log("Dashboard: http://localhost:" + existingPort);
378
+ return;
379
+ }
380
+
381
+ // Clean up stale lock file if present
382
+ const lockFile = path.join(os.homedir(), ".crosschat", "dashboard.lock");
383
+ try { fs.unlinkSync(lockFile); } catch {}
384
+
385
+ // Resolve hub-main.js
386
+ const candidates = [
387
+ path.join(__dirname, "..", "dist", "hub", "hub-main.js"),
388
+ path.join(__dirname, "hub", "hub-main.js"),
389
+ ];
390
+ let hubPath = null;
391
+ for (const c of candidates) {
392
+ if (fs.existsSync(c)) { hubPath = c; break; }
393
+ }
394
+ if (!hubPath) {
395
+ console.error("Error: hub-main.js not found. Is crosschat built?");
396
+ process.exit(1);
397
+ }
398
+
399
+ console.log("Starting hub...");
400
+ const child = spawn(process.execPath, [hubPath], {
401
+ detached: true,
402
+ stdio: "ignore",
403
+ env: { ...process.env },
404
+ });
405
+ child.unref();
406
+
407
+ // Wait for lock file to appear (up to 10s)
408
+ const deadline = Date.now() + 10000;
409
+ while (Date.now() < deadline) {
410
+ await new Promise((r) => setTimeout(r, 300));
411
+ const lock = readLockFile();
412
+ if (lock && lock.port) {
413
+ console.log("Hub started on port " + lock.port + " (PID " + lock.pid + ")");
414
+ console.log("Dashboard: http://localhost:" + lock.port);
415
+ return;
416
+ }
417
+ }
418
+ console.error("Hub did not start within 10 seconds. Check ~/.crosschat/ for errors.");
419
+ process.exit(1);
420
+ }
421
+
422
+ async function peers() {
423
+ const port = requireHub();
424
+ const data = await hubGet(port, "/api/peers");
425
+ const peerList = Array.isArray(data) ? data : [];
426
+
427
+ if (peerList.length === 0) {
428
+ console.log("No connected agents.");
429
+ return;
430
+ }
431
+
432
+ console.log("Connected agents (" + peerList.length + "):\n");
433
+ for (const p of peerList) {
434
+ const badge = p.status === "busy" ? "[busy]" : "[available]";
435
+ const detail = p.statusDetail ? " " + p.statusDetail : "";
436
+ console.log(" " + p.name + " " + badge + detail);
437
+ console.log(" ID: " + p.peerId);
438
+ console.log(" Room: " + (p.currentRoom || "—"));
439
+ console.log(" Directory: " + (p.cwd || "—"));
440
+ console.log(" Connected: " + (p.connectedAt ? timeAgo(p.connectedAt) : "—"));
441
+ console.log("");
442
+ }
443
+ }
444
+
445
+ async function tasks() {
446
+ const port = requireHub();
447
+ const filter = subcommand ? "?status=" + subcommand : "";
448
+ const data = await hubGet(port, "/api/tasks" + filter);
449
+ const taskList = Array.isArray(data) ? data : [];
450
+
451
+ if (taskList.length === 0) {
452
+ console.log("No tasks" + (subcommand ? " with status '" + subcommand + "'" : "") + ".");
453
+ return;
454
+ }
455
+
456
+ console.log("Tasks (" + taskList.length + "):\n");
457
+ for (const t of taskList) {
458
+ const statusTag = {
459
+ open: "OPEN",
460
+ claimed: "CLAIMED",
461
+ in_progress: "IN PROGRESS",
462
+ completed: "DONE",
463
+ failed: "FAILED",
464
+ }[t.status] || t.status.toUpperCase();
465
+ console.log(" [" + statusTag + "] " + t.description);
466
+ console.log(" ID: " + t.taskId);
467
+ console.log(" Room: " + (t.roomId || "—"));
468
+ console.log(" Creator: " + (t.creatorName || "—"));
469
+ if (t.claimantName) {
470
+ console.log(" Assignee: " + t.claimantName);
471
+ }
472
+ console.log(" Created: " + (t.createdAt ? timeAgo(t.createdAt) : "—"));
473
+ if (t.result) {
474
+ console.log(" Result: " + t.result.split("\n")[0]);
475
+ }
476
+ console.log("");
477
+ }
478
+ }
479
+
480
+ async function rooms() {
481
+ const port = requireHub();
482
+ const [roomData, peerData] = await Promise.all([
483
+ hubGet(port, "/api/rooms"),
484
+ hubGet(port, "/api/peers"),
485
+ ]);
486
+ const roomList = Array.isArray(roomData) ? roomData : [];
487
+ const peerList = Array.isArray(peerData) ? peerData : [];
488
+
489
+ if (roomList.length === 0) {
490
+ console.log("No active rooms.");
491
+ return;
492
+ }
493
+
494
+ // Count peers per room
495
+ const peersPerRoom = {};
496
+ for (const p of peerList) {
497
+ const room = p.currentRoom || "general";
498
+ if (!peersPerRoom[room]) peersPerRoom[room] = [];
499
+ peersPerRoom[room].push(p.name);
500
+ }
501
+
502
+ console.log("Rooms (" + roomList.length + "):\n");
503
+ for (const r of roomList) {
504
+ const name = r.name || r.id;
505
+ const msgCount = r.messageCount != null ? r.messageCount : "?";
506
+ const roomPeers = peersPerRoom[name] || peersPerRoom[r.id] || [];
507
+ console.log(" " + name);
508
+ console.log(" Messages: " + msgCount + " Agents: " + roomPeers.length);
509
+ if (roomPeers.length > 0) {
510
+ console.log(" " + roomPeers.join(", "));
511
+ }
512
+ console.log("");
513
+ }
514
+ }
515
+
516
+ function stop() {
517
+ const lock = readLockFile();
518
+ if (!lock || !lock.pid) {
519
+ console.log("No running hub found (no dashboard.lock).");
520
+ return false;
521
+ }
522
+
523
+ try {
524
+ // Check if process is still running
525
+ process.kill(lock.pid, 0);
526
+ } catch {
527
+ console.log("Hub process " + lock.pid + " is not running (stale lock file).");
528
+ // Clean up stale lock file
529
+ const lockFile = path.join(os.homedir(), ".crosschat", "dashboard.lock");
530
+ try { fs.unlinkSync(lockFile); } catch {}
531
+ return false;
532
+ }
533
+
534
+ console.log("Stopping hub (PID " + lock.pid + ", port " + lock.port + ")...");
535
+ process.kill(lock.pid, "SIGTERM");
536
+
537
+ // Wait for process to exit (up to 5 seconds)
538
+ const start = Date.now();
539
+ while (Date.now() - start < 5000) {
540
+ try {
541
+ process.kill(lock.pid, 0);
542
+ // Still running, wait a bit
543
+ const waitUntil = Date.now() + 100;
544
+ while (Date.now() < waitUntil) {} // busy-wait (sync)
545
+ } catch {
546
+ console.log("Hub stopped.");
547
+ return true;
548
+ }
549
+ }
550
+
551
+ console.log("Hub did not stop in time — sending SIGKILL...");
552
+ try { process.kill(lock.pid, "SIGKILL"); } catch {}
553
+ console.log("Hub killed.");
554
+ return true;
555
+ }
556
+
557
+ function showHelp() {
558
+ console.log("CrossChat v" + pkg.version);
559
+ console.log("MCP server for inter-instance Claude Code communication");
560
+ console.log("");
561
+ console.log("Setup:");
562
+ console.log(" crosschat install Install MCP server + /crosschat command");
563
+ console.log(" crosschat uninstall Remove everything");
564
+ console.log("");
565
+ console.log("Server:");
566
+ console.log(" crosschat start Start the hub server");
567
+ console.log(" crosschat stop Stop the hub server");
568
+ console.log(" crosschat restart Restart the hub server");
569
+ console.log("");
570
+ console.log("Info:");
571
+ console.log(" crosschat status Overview of hub, agents, tasks, rooms");
572
+ console.log(" crosschat peers List connected agents with details");
573
+ console.log(" crosschat tasks [status] List tasks (optional: open, claimed, completed, failed)");
574
+ console.log(" crosschat rooms List active rooms");
575
+ console.log("");
576
+ console.log(" crosschat --version Show version");
577
+ console.log(" crosschat --help Show this help");
578
+ console.log("");
579
+ console.log("After installing, restart Claude Code and run /crosschat to start.");
580
+ }
581
+
582
+ switch (command) {
583
+ case "install":
584
+ case "update":
585
+ install();
586
+ break;
587
+ case "uninstall":
588
+ case "remove":
589
+ uninstall();
590
+ break;
591
+ case "serve":
592
+ serve();
593
+ break;
594
+ case "start":
595
+ start();
596
+ break;
597
+ case "stop":
598
+ stop();
599
+ break;
600
+ case "restart":
601
+ stop();
602
+ // Give the OS a moment to release the port
603
+ setTimeout(() => start(), 500);
604
+ break;
605
+ case "status":
606
+ status();
607
+ break;
608
+ case "peers":
609
+ case "agents":
610
+ peers();
611
+ break;
612
+ case "tasks":
613
+ tasks();
614
+ break;
615
+ case "rooms":
616
+ rooms();
617
+ break;
618
+ case "--version":
619
+ case "-v":
620
+ console.log(pkg.version);
621
+ break;
622
+ case "--help":
623
+ case "-h":
624
+ case undefined:
625
+ showHelp();
626
+ break;
627
+ default:
628
+ console.error("Unknown command: " + command);
629
+ console.error('Run "crosschat --help" for usage.');
630
+ process.exit(1);
631
+ }