@dolusoft/claude-collab 0.1.5 → 1.3.1

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/dist/cli.js CHANGED
@@ -1,1822 +1,784 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import WebSocket2, { WebSocketServer, WebSocket } from 'ws';
3
+ import { WebSocketServer, WebSocket } from 'ws';
4
+ import { createServer } from 'net';
4
5
  import { v4 } from 'uuid';
6
+ import multicastDns from 'multicast-dns';
7
+ import { networkInterfaces, tmpdir } from 'os';
8
+ import { EventEmitter } from 'events';
9
+ import { execFile } from 'child_process';
10
+ import { unlinkSync } from 'fs';
11
+ import { join } from 'path';
5
12
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
14
  import { z } from 'zod';
9
- import { spawn } from 'child_process';
10
- import { createConnection } from 'net';
11
15
 
12
- // src/domain/entities/member.entity.ts
13
- var Member = class _Member {
14
- _id;
15
- _teamId;
16
- _displayName;
17
- _connectedAt;
18
- _status;
19
- _lastActivityAt;
20
- constructor(props) {
21
- this._id = props.id;
22
- this._teamId = props.teamId;
23
- this._displayName = props.displayName;
24
- this._connectedAt = props.connectedAt;
25
- this._status = props.status;
26
- this._lastActivityAt = props.connectedAt;
27
- }
28
- /**
29
- * Creates a new Member instance
30
- */
31
- static create(props) {
32
- if (!props.displayName.trim()) {
33
- throw new Error("Display name cannot be empty");
34
- }
35
- return new _Member(props);
36
- }
37
- /**
38
- * Reconstitutes a Member from persistence
39
- */
40
- static reconstitute(props) {
41
- const member = new _Member(props);
42
- member._lastActivityAt = props.lastActivityAt;
43
- return member;
44
- }
45
- // Getters
46
- get id() {
47
- return this._id;
48
- }
49
- get teamId() {
50
- return this._teamId;
51
- }
52
- get displayName() {
53
- return this._displayName;
54
- }
55
- get connectedAt() {
56
- return this._connectedAt;
57
- }
58
- get status() {
59
- return this._status;
60
- }
61
- get lastActivityAt() {
62
- return this._lastActivityAt;
63
- }
64
- get isOnline() {
65
- return this._status === "ONLINE" /* ONLINE */ || this._status === "IDLE" /* IDLE */;
66
- }
67
- // Behaviors
68
- /**
69
- * Marks the member as online
70
- */
71
- goOnline() {
72
- this._status = "ONLINE" /* ONLINE */;
73
- this._lastActivityAt = /* @__PURE__ */ new Date();
74
- }
75
- /**
76
- * Marks the member as idle
77
- */
78
- goIdle() {
79
- this._status = "IDLE" /* IDLE */;
80
- }
81
- /**
82
- * Marks the member as offline
83
- */
84
- goOffline() {
85
- this._status = "OFFLINE" /* OFFLINE */;
86
- }
87
- /**
88
- * Records activity from this member
89
- */
90
- recordActivity() {
91
- this._lastActivityAt = /* @__PURE__ */ new Date();
92
- if (this._status === "IDLE" /* IDLE */) {
93
- this._status = "ONLINE" /* ONLINE */;
16
+ function getLocalIp() {
17
+ const nets = networkInterfaces();
18
+ for (const name of Object.keys(nets)) {
19
+ for (const net of nets[name] ?? []) {
20
+ if (net.family === "IPv4" && !net.internal) {
21
+ return net.address;
22
+ }
94
23
  }
95
24
  }
96
- /**
97
- * Converts entity to plain object for serialization
98
- */
99
- toJSON() {
100
- return {
101
- id: this._id,
102
- teamId: this._teamId,
103
- displayName: this._displayName,
104
- connectedAt: this._connectedAt,
105
- status: this._status,
106
- lastActivityAt: this._lastActivityAt
107
- };
108
- }
109
- };
110
- var BaseDomainEvent = class {
111
- eventId;
112
- timestamp;
113
- constructor() {
114
- this.eventId = v4();
115
- this.timestamp = /* @__PURE__ */ new Date();
116
- }
117
- /**
118
- * Converts event to JSON
119
- */
120
- toJSON() {
121
- return {
122
- eventId: this.eventId,
123
- eventType: this.eventType,
124
- timestamp: this.timestamp,
125
- payload: this.payload
126
- };
127
- }
128
- };
129
-
130
- // src/domain/events/member-joined.event.ts
131
- var MemberJoinedEvent = class _MemberJoinedEvent extends BaseDomainEvent {
132
- constructor(memberId, teamId, displayName) {
133
- super();
134
- this.memberId = memberId;
135
- this.teamId = teamId;
136
- this.displayName = displayName;
137
- }
138
- static EVENT_TYPE = "MEMBER_JOINED";
139
- get eventType() {
140
- return _MemberJoinedEvent.EVENT_TYPE;
141
- }
142
- get payload() {
143
- return {
144
- memberId: this.memberId,
145
- teamId: this.teamId,
146
- displayName: this.displayName
147
- };
148
- }
149
- };
150
-
151
- // src/shared/types/branded-types.ts
152
- var MemberId = {
153
- create: (id) => id,
154
- isValid: (id) => id.length > 0
155
- };
156
- var TeamId = {
157
- create: (id) => id.toLowerCase().trim(),
158
- isValid: (id) => /^[a-z][a-z0-9-]*$/.test(id.toLowerCase().trim())
159
- };
160
- var QuestionId = {
161
- create: (id) => id,
162
- isValid: (id) => id.length > 0
163
- };
164
- var AnswerId = {
165
- create: (id) => id,
166
- isValid: (id) => id.length > 0
167
- };
168
-
169
- // src/shared/utils/id-generator.ts
170
- function generateMemberId() {
171
- return MemberId.create(v4());
25
+ return "127.0.0.1";
172
26
  }
173
- function createTeamId(name) {
174
- return TeamId.create(name);
175
- }
176
- function generateQuestionId() {
177
- return QuestionId.create(`q_${v4()}`);
178
- }
179
- function generateAnswerId() {
180
- return AnswerId.create(`a_${v4()}`);
181
- }
182
-
183
- // src/shared/errors/domain-errors.ts
184
- var DomainError = class extends Error {
185
- code;
186
- timestamp;
187
- constructor(message, code) {
188
- super(message);
189
- this.name = this.constructor.name;
190
- this.code = code;
191
- this.timestamp = /* @__PURE__ */ new Date();
192
- Error.captureStackTrace(this, this.constructor);
193
- }
194
- };
195
- var TeamNotFoundError = class extends DomainError {
196
- constructor(teamId) {
197
- super(`Team '${teamId}' not found`, "TEAM_NOT_FOUND");
198
- }
199
- };
200
- var MemberNotFoundError = class extends DomainError {
201
- constructor(memberId) {
202
- super(`Member '${memberId}' not found`, "MEMBER_NOT_FOUND");
203
- }
204
- };
205
- var QuestionNotFoundError = class extends DomainError {
206
- constructor(questionId) {
207
- super(`Question '${questionId}' not found`, "QUESTION_NOT_FOUND");
208
- }
209
- };
210
- var QuestionAlreadyAnsweredError = class extends DomainError {
211
- constructor(questionId) {
212
- super(`Question '${questionId}' has already been answered`, "QUESTION_ALREADY_ANSWERED");
213
- }
214
- };
215
- var ValidationError = class extends DomainError {
216
- field;
217
- constructor(field, message) {
218
- super(message, "VALIDATION_ERROR");
219
- this.field = field;
27
+ var SERVICE_TYPE = "_claude-collab._tcp.local";
28
+ var MdnsDiscovery = class {
29
+ mdns;
30
+ announced = false;
31
+ port = 0;
32
+ teamName = "";
33
+ memberId = "";
34
+ peersByTeam = /* @__PURE__ */ new Map();
35
+ peersByMemberId = /* @__PURE__ */ new Map();
36
+ onPeerFoundCb;
37
+ onPeerLostCb;
38
+ constructor() {
39
+ this.mdns = multicastDns();
40
+ this.setupHandlers();
220
41
  }
221
- };
222
-
223
- // src/application/use-cases/join-team.use-case.ts
224
- var JoinTeamUseCase = class {
225
- constructor(deps) {
226
- this.deps = deps;
42
+ get serviceName() {
43
+ return `${this.memberId}.${SERVICE_TYPE}`;
227
44
  }
228
- /**
229
- * Executes the use case
230
- */
231
- async execute(input) {
232
- if (!input.teamName.trim()) {
233
- throw new ValidationError("teamName", "Team name cannot be empty");
234
- }
235
- if (!input.displayName.trim()) {
236
- throw new ValidationError("displayName", "Display name cannot be empty");
237
- }
238
- const team = await this.deps.teamRepository.getOrCreate(input.teamName);
239
- const memberId = generateMemberId();
240
- const member = Member.create({
241
- id: memberId,
242
- teamId: team.id,
243
- displayName: input.displayName.trim(),
244
- connectedAt: /* @__PURE__ */ new Date(),
245
- status: "ONLINE" /* ONLINE */
45
+ buildAnswers() {
46
+ return [
47
+ {
48
+ name: SERVICE_TYPE,
49
+ type: "PTR",
50
+ ttl: 300,
51
+ data: this.serviceName
52
+ },
53
+ {
54
+ name: this.serviceName,
55
+ type: "SRV",
56
+ ttl: 300,
57
+ data: {
58
+ priority: 0,
59
+ weight: 0,
60
+ port: this.port,
61
+ target: getLocalIp()
62
+ }
63
+ },
64
+ {
65
+ name: this.serviceName,
66
+ type: "TXT",
67
+ ttl: 300,
68
+ data: [
69
+ Buffer.from(`team=${this.teamName}`),
70
+ Buffer.from(`memberId=${this.memberId}`),
71
+ Buffer.from("ver=1")
72
+ ]
73
+ }
74
+ ];
75
+ }
76
+ setupHandlers() {
77
+ this.mdns.on("query", (query) => {
78
+ if (!this.announced) return;
79
+ const questions = query.questions ?? [];
80
+ const ptrQuery = questions.find(
81
+ (q) => q.type === "PTR" && q.name === SERVICE_TYPE
82
+ );
83
+ if (!ptrQuery) return;
84
+ this.mdns.respond({ answers: this.buildAnswers() });
246
85
  });
247
- await this.deps.memberRepository.save(member);
248
- team.addMember(memberId);
249
- await this.deps.teamRepository.save(team);
250
- if (this.deps.onMemberJoined) {
251
- const event = new MemberJoinedEvent(memberId, team.id, member.displayName);
252
- await this.deps.onMemberJoined(event);
253
- }
254
- return {
255
- memberId,
256
- teamId: team.id,
257
- teamName: team.name,
258
- displayName: member.displayName,
259
- status: member.status,
260
- memberCount: team.memberCount
261
- };
262
- }
263
- };
264
-
265
- // src/domain/entities/question.entity.ts
266
- var Question = class _Question {
267
- _id;
268
- _fromMemberId;
269
- _toTeamId;
270
- _content;
271
- _createdAt;
272
- _status;
273
- _answeredAt;
274
- _answeredByMemberId;
275
- constructor(props) {
276
- this._id = props.id;
277
- this._fromMemberId = props.fromMemberId;
278
- this._toTeamId = props.toTeamId;
279
- this._content = props.content;
280
- this._createdAt = props.createdAt;
281
- this._status = props.status;
282
- this._answeredAt = props.answeredAt;
283
- this._answeredByMemberId = props.answeredByMemberId;
284
- }
285
- /**
286
- * Creates a new Question instance
287
- */
288
- static create(props) {
289
- return new _Question({
290
- ...props,
291
- status: "PENDING" /* PENDING */
86
+ this.mdns.on("response", (response) => {
87
+ this.parseResponse(response);
292
88
  });
293
89
  }
294
- /**
295
- * Reconstitutes a Question from persistence
296
- */
297
- static reconstitute(props) {
298
- return new _Question(props);
299
- }
300
- // Getters
301
- get id() {
302
- return this._id;
303
- }
304
- get fromMemberId() {
305
- return this._fromMemberId;
306
- }
307
- get toTeamId() {
308
- return this._toTeamId;
309
- }
310
- get content() {
311
- return this._content;
312
- }
313
- get createdAt() {
314
- return this._createdAt;
315
- }
316
- get status() {
317
- return this._status;
318
- }
319
- get answeredAt() {
320
- return this._answeredAt;
321
- }
322
- get answeredByMemberId() {
323
- return this._answeredByMemberId;
324
- }
325
- get isPending() {
326
- return this._status === "PENDING" /* PENDING */;
327
- }
328
- get isAnswered() {
329
- return this._status === "ANSWERED" /* ANSWERED */;
330
- }
331
- get isTimedOut() {
332
- return this._status === "TIMEOUT" /* TIMEOUT */;
333
- }
334
- get isCancelled() {
335
- return this._status === "CANCELLED" /* CANCELLED */;
336
- }
337
- get canBeAnswered() {
338
- return this._status === "PENDING" /* PENDING */;
339
- }
340
- /**
341
- * Calculates the age of the question in milliseconds
342
- */
343
- get ageMs() {
344
- return Date.now() - this._createdAt.getTime();
345
- }
346
- // Behaviors
347
- /**
348
- * Marks the question as answered
349
- * @throws QuestionAlreadyAnsweredError if already answered
350
- */
351
- markAsAnswered(answeredByMemberId) {
352
- if (!this.canBeAnswered) {
353
- throw new QuestionAlreadyAnsweredError(this._id);
354
- }
355
- this._status = "ANSWERED" /* ANSWERED */;
356
- this._answeredAt = /* @__PURE__ */ new Date();
357
- this._answeredByMemberId = answeredByMemberId;
358
- }
359
- /**
360
- * Marks the question as timed out
361
- */
362
- markAsTimedOut() {
363
- if (this._status === "PENDING" /* PENDING */) {
364
- this._status = "TIMEOUT" /* TIMEOUT */;
365
- }
366
- }
367
- /**
368
- * Marks the question as cancelled
369
- */
370
- markAsCancelled() {
371
- if (this._status === "PENDING" /* PENDING */) {
372
- this._status = "CANCELLED" /* CANCELLED */;
373
- }
374
- }
375
- /**
376
- * Converts entity to plain object for serialization
377
- */
378
- toJSON() {
379
- return {
380
- id: this._id,
381
- fromMemberId: this._fromMemberId,
382
- toTeamId: this._toTeamId,
383
- content: this._content,
384
- createdAt: this._createdAt,
385
- status: this._status,
386
- answeredAt: this._answeredAt,
387
- answeredByMemberId: this._answeredByMemberId
388
- };
389
- }
390
- };
391
-
392
- // src/config/index.ts
393
- var config = {
394
- /**
395
- * WebSocket Hub configuration
396
- */
397
- hub: {
398
- /**
399
- * Default port for the Hub server
400
- */
401
- port: parseInt(process.env["CLAUDE_COLLAB_PORT"] ?? "9999", 10),
402
- /**
403
- * Host to bind the Hub server to
404
- */
405
- host: process.env["CLAUDE_COLLAB_HOST"] ?? "localhost",
406
- /**
407
- * Heartbeat interval in milliseconds
408
- */
409
- heartbeatInterval: 3e4,
410
- /**
411
- * Client timeout in milliseconds (no heartbeat received)
412
- */
413
- clientTimeout: 6e4
414
- },
415
- /**
416
- * Communication configuration
417
- */
418
- communication: {
419
- /**
420
- * Default timeout for waiting for an answer (in milliseconds)
421
- */
422
- defaultTimeout: 3e4,
423
- /**
424
- * Maximum message content length
425
- */
426
- maxMessageLength: 5e4
427
- },
428
- /**
429
- * Auto-start configuration
430
- */
431
- autoStart: {
432
- /**
433
- * Maximum retries when connecting to hub
434
- */
435
- maxRetries: 3,
436
- /**
437
- * Delay between retries in milliseconds
438
- */
439
- retryDelay: 1e3
440
- }
441
- };
442
-
443
- // src/domain/value-objects/message-content.vo.ts
444
- var MessageContent = class _MessageContent {
445
- _text;
446
- _format;
447
- constructor(text, format) {
448
- this._text = text;
449
- this._format = format;
450
- }
451
- /**
452
- * Creates a new MessageContent
453
- * @throws ValidationError if content is invalid
454
- */
455
- static create(text, format = "markdown") {
456
- const trimmedText = text.trim();
457
- if (!trimmedText) {
458
- throw new ValidationError("text", "Message content cannot be empty");
459
- }
460
- if (trimmedText.length > config.communication.maxMessageLength) {
461
- throw new ValidationError(
462
- "text",
463
- `Message content exceeds maximum length of ${config.communication.maxMessageLength} characters`
90
+ parseResponse(response) {
91
+ const allRecords = [
92
+ ...response.answers ?? [],
93
+ ...response.additionals ?? []
94
+ ];
95
+ const ptrRecords = allRecords.filter(
96
+ (r) => r.type === "PTR" && r.name === SERVICE_TYPE
97
+ );
98
+ for (const ptr of ptrRecords) {
99
+ const instanceName = ptr.data;
100
+ const srv = allRecords.find(
101
+ (r) => r.type === "SRV" && r.name === instanceName
464
102
  );
465
- }
466
- return new _MessageContent(trimmedText, format);
467
- }
468
- /**
469
- * Creates a plain text message
470
- */
471
- static plain(text) {
472
- return _MessageContent.create(text, "plain");
473
- }
474
- /**
475
- * Creates a markdown message
476
- */
477
- static markdown(text) {
478
- return _MessageContent.create(text, "markdown");
479
- }
480
- /**
481
- * Reconstitutes from persistence
482
- */
483
- static reconstitute(props) {
484
- return new _MessageContent(props.text, props.format);
485
- }
486
- // Getters
487
- get text() {
488
- return this._text;
489
- }
490
- get format() {
491
- return this._format;
492
- }
493
- get length() {
494
- return this._text.length;
495
- }
496
- get isMarkdown() {
497
- return this._format === "markdown";
498
- }
499
- get isPlain() {
500
- return this._format === "plain";
501
- }
502
- /**
503
- * Returns a preview of the content (first 100 chars)
504
- */
505
- get preview() {
506
- if (this._text.length <= 100) {
507
- return this._text;
508
- }
509
- return `${this._text.substring(0, 97)}...`;
510
- }
511
- /**
512
- * Checks equality with another MessageContent
513
- */
514
- equals(other) {
515
- return this._text === other._text && this._format === other._format;
516
- }
517
- /**
518
- * Converts to plain object for serialization
519
- */
520
- toJSON() {
521
- return {
522
- text: this._text,
523
- format: this._format
524
- };
525
- }
526
- /**
527
- * String representation
528
- */
529
- toString() {
530
- return this._text;
531
- }
532
- };
533
-
534
- // src/domain/events/question-asked.event.ts
535
- var QuestionAskedEvent = class _QuestionAskedEvent extends BaseDomainEvent {
536
- constructor(questionId, fromMemberId, toTeamId, contentPreview) {
537
- super();
538
- this.questionId = questionId;
539
- this.fromMemberId = fromMemberId;
540
- this.toTeamId = toTeamId;
541
- this.contentPreview = contentPreview;
542
- }
543
- static EVENT_TYPE = "QUESTION_ASKED";
544
- get eventType() {
545
- return _QuestionAskedEvent.EVENT_TYPE;
546
- }
547
- get payload() {
548
- return {
549
- questionId: this.questionId,
550
- fromMemberId: this.fromMemberId,
551
- toTeamId: this.toTeamId,
552
- contentPreview: this.contentPreview
553
- };
554
- }
555
- };
556
-
557
- // src/application/use-cases/ask-question.use-case.ts
558
- var AskQuestionUseCase = class {
559
- constructor(deps) {
560
- this.deps = deps;
561
- }
562
- /**
563
- * Executes the use case
564
- */
565
- async execute(input) {
566
- const member = await this.deps.memberRepository.findById(input.fromMemberId);
567
- if (!member) {
568
- throw new MemberNotFoundError(input.fromMemberId);
569
- }
570
- const targetTeamId = createTeamId(input.toTeamName);
571
- const targetTeam = await this.deps.teamRepository.findById(targetTeamId);
572
- if (!targetTeam) {
573
- throw new TeamNotFoundError(input.toTeamName);
574
- }
575
- if (member.teamId === targetTeamId) {
576
- throw new ValidationError("toTeamName", "Cannot ask question to your own team");
577
- }
578
- const content = MessageContent.create(input.content, input.format ?? "markdown");
579
- const questionId = generateQuestionId();
580
- const question = Question.create({
581
- id: questionId,
582
- fromMemberId: input.fromMemberId,
583
- toTeamId: targetTeamId,
584
- content,
585
- createdAt: /* @__PURE__ */ new Date()
586
- });
587
- await this.deps.questionRepository.save(question);
588
- member.recordActivity();
589
- await this.deps.memberRepository.save(member);
590
- if (this.deps.onQuestionAsked) {
591
- const event = new QuestionAskedEvent(
592
- questionId,
593
- input.fromMemberId,
594
- targetTeamId,
595
- content.preview
103
+ const txt = allRecords.find(
104
+ (r) => r.type === "TXT" && r.name === instanceName
596
105
  );
597
- await this.deps.onQuestionAsked(event);
598
- }
599
- return {
600
- questionId,
601
- toTeamId: targetTeamId,
602
- status: question.status,
603
- createdAt: question.createdAt
604
- };
605
- }
606
- };
607
-
608
- // src/application/use-cases/get-inbox.use-case.ts
609
- var GetInboxUseCase = class {
610
- constructor(deps) {
611
- this.deps = deps;
612
- }
613
- /**
614
- * Executes the use case
615
- */
616
- async execute(input) {
617
- const member = await this.deps.memberRepository.findById(input.memberId);
618
- if (!member) {
619
- throw new MemberNotFoundError(input.memberId);
620
- }
621
- const team = await this.deps.teamRepository.findById(input.teamId);
622
- if (!team) {
623
- throw new TeamNotFoundError(input.teamId);
624
- }
625
- const allQuestions = await this.deps.questionRepository.findPendingByTeamId(input.teamId);
626
- const questions = input.includeAnswered ? allQuestions : allQuestions.filter((q) => q.isPending);
627
- const questionItems = [];
628
- for (const question of questions) {
629
- const fromMember = await this.deps.memberRepository.findById(question.fromMemberId);
630
- const fromTeam = fromMember ? await this.deps.teamRepository.findById(fromMember.teamId) : null;
631
- questionItems.push({
632
- questionId: question.id,
633
- fromMemberId: question.fromMemberId,
634
- fromDisplayName: fromMember?.displayName ?? "Unknown",
635
- fromTeamName: fromTeam?.name ?? "Unknown",
636
- content: question.content.text,
637
- format: question.content.format,
638
- status: question.status,
639
- createdAt: question.createdAt,
640
- ageMs: question.ageMs
641
- });
106
+ if (!srv) continue;
107
+ const port = srv.data.port;
108
+ const host = srv.data.target || "127.0.0.1";
109
+ let teamName = "";
110
+ let memberId = "";
111
+ if (txt) {
112
+ const txtData = txt.data ?? [];
113
+ for (const entry of txtData) {
114
+ const str = Buffer.isBuffer(entry) ? entry.toString() : String(entry);
115
+ if (str.startsWith("team=")) teamName = str.slice(5);
116
+ if (str.startsWith("memberId=")) memberId = str.slice(9);
117
+ }
118
+ }
119
+ if (!teamName || !memberId) continue;
120
+ if (memberId === this.memberId) continue;
121
+ const ptrTtl = ptr.ttl ?? 300;
122
+ const srvTtl = srv.ttl ?? 300;
123
+ if (ptrTtl === 0 || srvTtl === 0) {
124
+ this.peersByTeam.delete(teamName);
125
+ this.peersByMemberId.delete(memberId);
126
+ this.onPeerLostCb?.(memberId);
127
+ continue;
128
+ }
129
+ const peer = { host, port, teamName, memberId };
130
+ this.peersByTeam.set(teamName, peer);
131
+ this.peersByMemberId.set(memberId, peer);
132
+ this.onPeerFoundCb?.(peer);
642
133
  }
643
- questionItems.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
644
- const pendingCount = questionItems.filter((q) => q.status === "PENDING" /* PENDING */).length;
645
- return {
646
- teamId: team.id,
647
- teamName: team.name,
648
- questions: questionItems,
649
- totalCount: questionItems.length,
650
- pendingCount
651
- };
652
- }
653
- };
654
-
655
- // src/domain/entities/answer.entity.ts
656
- var Answer = class _Answer {
657
- _id;
658
- _questionId;
659
- _fromMemberId;
660
- _content;
661
- _createdAt;
662
- constructor(props) {
663
- this._id = props.id;
664
- this._questionId = props.questionId;
665
- this._fromMemberId = props.fromMemberId;
666
- this._content = props.content;
667
- this._createdAt = props.createdAt;
668
134
  }
669
135
  /**
670
- * Creates a new Answer instance
136
+ * Announce this node's service via mDNS.
137
+ * Sends an unsolicited response so existing peers notice immediately.
671
138
  */
672
- static create(props) {
673
- return new _Answer(props);
674
- }
675
- /**
676
- * Reconstitutes an Answer from persistence
677
- */
678
- static reconstitute(props) {
679
- return new _Answer(props);
680
- }
681
- // Getters
682
- get id() {
683
- return this._id;
684
- }
685
- get questionId() {
686
- return this._questionId;
687
- }
688
- get fromMemberId() {
689
- return this._fromMemberId;
690
- }
691
- get content() {
692
- return this._content;
693
- }
694
- get createdAt() {
695
- return this._createdAt;
696
- }
697
- /**
698
- * Converts entity to plain object for serialization
699
- */
700
- toJSON() {
701
- return {
702
- id: this._id,
703
- questionId: this._questionId,
704
- fromMemberId: this._fromMemberId,
705
- content: this._content,
706
- createdAt: this._createdAt
707
- };
708
- }
709
- };
710
-
711
- // src/domain/events/question-answered.event.ts
712
- var QuestionAnsweredEvent = class _QuestionAnsweredEvent extends BaseDomainEvent {
713
- constructor(questionId, answerId, answeredByMemberId, contentPreview) {
714
- super();
715
- this.questionId = questionId;
716
- this.answerId = answerId;
717
- this.answeredByMemberId = answeredByMemberId;
718
- this.contentPreview = contentPreview;
719
- }
720
- static EVENT_TYPE = "QUESTION_ANSWERED";
721
- get eventType() {
722
- return _QuestionAnsweredEvent.EVENT_TYPE;
723
- }
724
- get payload() {
725
- return {
726
- questionId: this.questionId,
727
- answerId: this.answerId,
728
- answeredByMemberId: this.answeredByMemberId,
729
- contentPreview: this.contentPreview
730
- };
731
- }
732
- };
733
-
734
- // src/application/use-cases/reply-question.use-case.ts
735
- var ReplyQuestionUseCase = class {
736
- constructor(deps) {
737
- this.deps = deps;
139
+ announce(port, teamName, memberId) {
140
+ this.port = port;
141
+ this.teamName = teamName;
142
+ this.memberId = memberId;
143
+ this.announced = true;
144
+ this.mdns.respond({ answers: this.buildAnswers() });
738
145
  }
739
146
  /**
740
- * Executes the use case
147
+ * Send a PTR query to discover existing peers.
741
148
  */
742
- async execute(input) {
743
- const member = await this.deps.memberRepository.findById(input.fromMemberId);
744
- if (!member) {
745
- throw new MemberNotFoundError(input.fromMemberId);
746
- }
747
- const question = await this.deps.questionRepository.findById(input.questionId);
748
- if (!question) {
749
- throw new QuestionNotFoundError(input.questionId);
750
- }
751
- if (!question.canBeAnswered) {
752
- throw new QuestionAlreadyAnsweredError(input.questionId);
753
- }
754
- const content = MessageContent.create(input.content, input.format ?? "markdown");
755
- const answerId = generateAnswerId();
756
- const answer = Answer.create({
757
- id: answerId,
758
- questionId: input.questionId,
759
- fromMemberId: input.fromMemberId,
760
- content,
761
- createdAt: /* @__PURE__ */ new Date()
149
+ discover() {
150
+ this.mdns.query({
151
+ questions: [{ name: SERVICE_TYPE, type: "PTR" }]
762
152
  });
763
- question.markAsAnswered(input.fromMemberId);
764
- await this.deps.answerRepository.save(answer);
765
- await this.deps.questionRepository.save(question);
766
- member.recordActivity();
767
- await this.deps.memberRepository.save(member);
768
- if (this.deps.onQuestionAnswered) {
769
- const event = new QuestionAnsweredEvent(
770
- input.questionId,
771
- answerId,
772
- input.fromMemberId,
773
- content.preview
774
- );
775
- await this.deps.onQuestionAnswered(event);
776
- }
777
- return {
778
- answerId,
779
- questionId: input.questionId,
780
- deliveredToMemberId: question.fromMemberId,
781
- createdAt: answer.createdAt
782
- };
783
- }
784
- };
785
-
786
- // src/infrastructure/repositories/in-memory-member.repository.ts
787
- var InMemoryMemberRepository = class {
788
- members = /* @__PURE__ */ new Map();
789
- async save(member) {
790
- this.members.set(member.id, member);
791
- }
792
- async findById(id) {
793
- return this.members.get(id) ?? null;
794
153
  }
795
- async findByTeamId(teamId) {
796
- return [...this.members.values()].filter((m) => m.teamId === teamId);
154
+ getPeerByTeam(teamName) {
155
+ return this.peersByTeam.get(teamName);
797
156
  }
798
- async findOnlineByTeamId(teamId) {
799
- return [...this.members.values()].filter((m) => m.teamId === teamId && m.isOnline);
157
+ onPeerFound(cb) {
158
+ this.onPeerFoundCb = cb;
800
159
  }
801
- async delete(id) {
802
- return this.members.delete(id);
160
+ onPeerLost(cb) {
161
+ this.onPeerLostCb = cb;
803
162
  }
804
- async exists(id) {
805
- return this.members.has(id);
806
- }
807
- async findAll() {
808
- return [...this.members.values()];
809
- }
810
- /**
811
- * Clears all data (useful for testing)
812
- */
813
- clear() {
814
- this.members.clear();
815
- }
816
- /**
817
- * Gets the count of members
818
- */
819
- get count() {
820
- return this.members.size;
163
+ destroy() {
164
+ this.mdns.destroy();
821
165
  }
822
166
  };
823
167
 
824
- // src/domain/entities/team.entity.ts
825
- var Team = class _Team {
826
- _id;
827
- _name;
828
- _createdAt;
829
- _memberIds;
830
- constructor(props) {
831
- this._id = props.id;
832
- this._name = props.name;
833
- this._createdAt = props.createdAt;
834
- this._memberIds = /* @__PURE__ */ new Set();
835
- }
836
- /**
837
- * Creates a new Team instance
838
- */
839
- static create(props) {
840
- if (!props.name.trim()) {
841
- throw new Error("Team name cannot be empty");
842
- }
843
- return new _Team(props);
844
- }
845
- /**
846
- * Reconstitutes a Team from persistence
847
- */
848
- static reconstitute(props) {
849
- const team = new _Team(props);
850
- for (const memberId of props.memberIds) {
851
- team._memberIds.add(memberId);
852
- }
853
- return team;
854
- }
855
- // Getters
856
- get id() {
857
- return this._id;
858
- }
859
- get name() {
860
- return this._name;
861
- }
862
- get createdAt() {
863
- return this._createdAt;
864
- }
865
- get memberIds() {
866
- return this._memberIds;
867
- }
868
- get memberCount() {
869
- return this._memberIds.size;
870
- }
871
- get isEmpty() {
872
- return this._memberIds.size === 0;
873
- }
874
- // Behaviors
875
- /**
876
- * Adds a member to the team
877
- * @returns true if the member was added, false if already present
878
- */
879
- addMember(memberId) {
880
- if (this._memberIds.has(memberId)) {
881
- return false;
882
- }
883
- this._memberIds.add(memberId);
884
- return true;
885
- }
886
- /**
887
- * Removes a member from the team
888
- * @returns true if the member was removed, false if not present
889
- */
890
- removeMember(memberId) {
891
- return this._memberIds.delete(memberId);
892
- }
893
- /**
894
- * Checks if a member is in the team
895
- */
896
- hasMember(memberId) {
897
- return this._memberIds.has(memberId);
898
- }
899
- /**
900
- * Gets all member IDs except the specified one
901
- * Useful for broadcasting to other team members
902
- */
903
- getOtherMemberIds(excludeMemberId) {
904
- return [...this._memberIds].filter((id) => id !== excludeMemberId);
905
- }
906
- /**
907
- * Converts entity to plain object for serialization
908
- */
909
- toJSON() {
910
- return {
911
- id: this._id,
912
- name: this._name,
913
- createdAt: this._createdAt,
914
- memberIds: [...this._memberIds]
915
- };
916
- }
917
- };
168
+ // src/infrastructure/p2p/p2p-message-protocol.ts
169
+ function serializeP2PMsg(msg) {
170
+ return JSON.stringify(msg);
171
+ }
172
+ function parseP2PMsg(data) {
173
+ return JSON.parse(data);
174
+ }
175
+ var CS_CONINJECT = `
176
+ using System;
177
+ using System.Collections.Generic;
178
+ using System.Runtime.InteropServices;
918
179
 
919
- // src/infrastructure/repositories/in-memory-team.repository.ts
920
- var InMemoryTeamRepository = class {
921
- teams = /* @__PURE__ */ new Map();
922
- async save(team) {
923
- this.teams.set(team.id, team);
924
- }
925
- async findById(id) {
926
- return this.teams.get(id) ?? null;
927
- }
928
- async findByName(name) {
929
- const teamId = createTeamId(name);
930
- return this.teams.get(teamId) ?? null;
931
- }
932
- async getOrCreate(name) {
933
- const existing = await this.findByName(name);
934
- if (existing) {
935
- return existing;
936
- }
937
- const teamId = createTeamId(name);
938
- const team = Team.create({
939
- id: teamId,
940
- name: name.trim(),
941
- createdAt: /* @__PURE__ */ new Date()
942
- });
943
- await this.save(team);
944
- return team;
945
- }
946
- async delete(id) {
947
- return this.teams.delete(id);
948
- }
949
- async exists(id) {
950
- return this.teams.has(id);
951
- }
952
- async findAll() {
953
- return [...this.teams.values()];
954
- }
955
- async findNonEmpty() {
956
- return [...this.teams.values()].filter((t) => !t.isEmpty);
957
- }
958
- /**
959
- * Clears all data (useful for testing)
960
- */
961
- clear() {
962
- this.teams.clear();
963
- }
964
- /**
965
- * Gets the count of teams
966
- */
967
- get count() {
968
- return this.teams.size;
969
- }
970
- };
180
+ public class ConInject {
181
+ [DllImport("kernel32.dll")] public static extern bool FreeConsole();
182
+ [DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
183
+ [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
184
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hwnd);
185
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
186
+ public static extern IntPtr CreateFile(
187
+ string lpFileName, uint dwDesiredAccess, uint dwShareMode,
188
+ IntPtr lpSecurityAttributes, uint dwCreationDisposition,
189
+ uint dwFlagsAndAttributes, IntPtr hTemplateFile);
190
+ [DllImport("kernel32.dll")] public static extern bool WriteConsoleInput(
191
+ IntPtr hIn, INPUT_RECORD[] buf, uint len, out uint written);
192
+ [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
971
193
 
972
- // src/infrastructure/repositories/in-memory-question.repository.ts
973
- var InMemoryQuestionRepository = class {
974
- questions = /* @__PURE__ */ new Map();
975
- async save(question) {
976
- this.questions.set(question.id, question);
977
- }
978
- async findById(id) {
979
- return this.questions.get(id) ?? null;
980
- }
981
- async findPendingByTeamId(teamId) {
982
- return [...this.questions.values()].filter((q) => q.toTeamId === teamId && q.isPending);
983
- }
984
- async findByFromMemberId(memberId) {
985
- return [...this.questions.values()].filter((q) => q.fromMemberId === memberId);
986
- }
987
- async findPendingByFromMemberId(memberId) {
988
- return [...this.questions.values()].filter(
989
- (q) => q.fromMemberId === memberId && q.isPending
990
- );
991
- }
992
- async delete(id) {
993
- return this.questions.delete(id);
994
- }
995
- async exists(id) {
996
- return this.questions.has(id);
997
- }
998
- async findAll() {
999
- return [...this.questions.values()];
1000
- }
1001
- async markTimedOut(olderThanMs) {
1002
- let count = 0;
1003
- const now = Date.now();
1004
- for (const question of this.questions.values()) {
1005
- if (question.isPending && now - question.createdAt.getTime() > olderThanMs) {
1006
- question.markAsTimedOut();
1007
- count++;
1008
- }
194
+ [StructLayout(LayoutKind.Explicit, Size=20)]
195
+ public struct INPUT_RECORD {
196
+ [FieldOffset(0)] public ushort EventType;
197
+ [FieldOffset(4)] public int bKeyDown;
198
+ [FieldOffset(8)] public ushort wRepeatCount;
199
+ [FieldOffset(10)] public ushort wVirtualKeyCode;
200
+ [FieldOffset(12)] public ushort wVirtualScanCode;
201
+ [FieldOffset(14)] public ushort UnicodeChar;
202
+ [FieldOffset(16)] public uint dwControlKeyState;
1009
203
  }
1010
- return count;
1011
- }
1012
- /**
1013
- * Clears all data (useful for testing)
1014
- */
1015
- clear() {
1016
- this.questions.clear();
1017
- }
1018
- /**
1019
- * Gets the count of questions
1020
- */
1021
- get count() {
1022
- return this.questions.size;
1023
- }
1024
- };
1025
204
 
1026
- // src/infrastructure/repositories/in-memory-answer.repository.ts
1027
- var InMemoryAnswerRepository = class {
1028
- answers = /* @__PURE__ */ new Map();
1029
- async save(answer) {
1030
- this.answers.set(answer.id, answer);
1031
- }
1032
- async findById(id) {
1033
- return this.answers.get(id) ?? null;
1034
- }
1035
- async findByQuestionId(questionId) {
1036
- for (const answer of this.answers.values()) {
1037
- if (answer.questionId === questionId) {
1038
- return answer;
1039
- }
205
+ const uint LEFT_CTRL = 0x0008;
206
+
207
+ static IntPtr OpenConin(uint pid) {
208
+ FreeConsole();
209
+ if (!AttachConsole(pid)) return new IntPtr(-1);
210
+ return CreateFile("CONIN$", 0xC0000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
1040
211
  }
1041
- return null;
1042
- }
1043
- async findAll() {
1044
- return [...this.answers.values()];
1045
- }
1046
- /**
1047
- * Clears all data (useful for testing)
1048
- */
1049
- clear() {
1050
- this.answers.clear();
1051
- }
1052
- /**
1053
- * Gets the count of answers
1054
- */
1055
- get count() {
1056
- return this.answers.size;
1057
- }
1058
- };
1059
212
 
1060
- // src/infrastructure/websocket/message-protocol.ts
1061
- function serializeMessage(message) {
1062
- return JSON.stringify(message);
1063
- }
1064
- function parseClientMessage(data) {
1065
- const parsed = JSON.parse(data);
1066
- validateClientMessage(parsed);
1067
- return parsed;
1068
- }
1069
- function parseHubMessage(data) {
1070
- return JSON.parse(data);
1071
- }
1072
- function validateClientMessage(message) {
1073
- if (!message.type) {
1074
- throw new Error("Message must have a type");
1075
- }
1076
- const validTypes = ["JOIN", "LEAVE", "ASK", "REPLY", "PING", "GET_INBOX"];
1077
- if (!validTypes.includes(message.type)) {
1078
- throw new Error(`Invalid message type: ${message.type}`);
1079
- }
1080
- }
1081
- function createErrorMessage(code, message, requestId) {
1082
- return {
1083
- type: "ERROR",
1084
- code,
1085
- message,
1086
- requestId
1087
- };
1088
- }
213
+ // Inject only text characters into console input buffer (no Ctrl keys, no Enter)
214
+ public static int InjectText(uint pid, string text) {
215
+ IntPtr hIn = OpenConin(pid);
216
+ if (hIn == new IntPtr(-1)) return -1;
1089
217
 
1090
- // src/infrastructure/websocket/hub-server.ts
1091
- function log(level, message, data) {
1092
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1093
- const prefix = `[${timestamp}] [HUB-${level}]`;
1094
- if (data) {
1095
- console.log(`${prefix} ${message}`, JSON.stringify(data, null, 2));
1096
- } else {
1097
- console.log(`${prefix} ${message}`);
1098
- }
1099
- }
1100
- var HubServer = class {
1101
- constructor(options = {}) {
1102
- this.options = options;
1103
- this.initializeUseCases();
1104
- }
1105
- wss = null;
1106
- clients = /* @__PURE__ */ new Map();
1107
- memberToWs = /* @__PURE__ */ new Map();
1108
- // Repositories
1109
- memberRepository = new InMemoryMemberRepository();
1110
- teamRepository = new InMemoryTeamRepository();
1111
- questionRepository = new InMemoryQuestionRepository();
1112
- answerRepository = new InMemoryAnswerRepository();
1113
- // Use cases
1114
- joinTeamUseCase;
1115
- askQuestionUseCase;
1116
- getInboxUseCase;
1117
- replyQuestionUseCase;
1118
- heartbeatInterval = null;
1119
- timeoutCheckInterval = null;
1120
- initializeUseCases() {
1121
- this.joinTeamUseCase = new JoinTeamUseCase({
1122
- memberRepository: this.memberRepository,
1123
- teamRepository: this.teamRepository,
1124
- onMemberJoined: async (event) => {
1125
- await this.broadcastToTeam(event.teamId, event.memberId, {
1126
- type: "MEMBER_JOINED",
1127
- member: await this.getMemberInfo(event.memberId)
1128
- });
1129
- }
1130
- });
1131
- this.askQuestionUseCase = new AskQuestionUseCase({
1132
- memberRepository: this.memberRepository,
1133
- teamRepository: this.teamRepository,
1134
- questionRepository: this.questionRepository,
1135
- onQuestionAsked: async (event) => {
1136
- const question = await this.questionRepository.findById(event.questionId);
1137
- if (question) {
1138
- await this.deliverQuestion(question);
1139
- }
1140
- }
1141
- });
1142
- this.getInboxUseCase = new GetInboxUseCase({
1143
- memberRepository: this.memberRepository,
1144
- teamRepository: this.teamRepository,
1145
- questionRepository: this.questionRepository
1146
- });
1147
- this.replyQuestionUseCase = new ReplyQuestionUseCase({
1148
- memberRepository: this.memberRepository,
1149
- questionRepository: this.questionRepository,
1150
- answerRepository: this.answerRepository,
1151
- onQuestionAnswered: async (event) => {
1152
- const question = await this.questionRepository.findById(event.questionId);
1153
- const answer = await this.answerRepository.findByQuestionId(event.questionId);
1154
- if (question && answer) {
1155
- await this.deliverAnswer(question, answer, event.answeredByMemberId);
1156
- }
1157
- }
1158
- });
1159
- }
1160
- /**
1161
- * Starts the hub server
1162
- */
1163
- async start() {
1164
- const port = this.options.port ?? config.hub.port;
1165
- const host = this.options.host ?? config.hub.host;
1166
- return new Promise((resolve, reject) => {
1167
- try {
1168
- this.wss = new WebSocketServer({ port, host });
1169
- this.wss.on("connection", (ws) => {
1170
- this.handleConnection(ws);
1171
- });
1172
- this.wss.on("error", (error) => {
1173
- log("ERROR", "Hub server error", { error: error.message, stack: error.stack });
1174
- reject(error);
1175
- });
1176
- this.wss.on("listening", () => {
1177
- log("INFO", `Hub server started successfully`, { host, port });
1178
- this.startHeartbeat();
1179
- this.startTimeoutCheck();
1180
- resolve();
1181
- });
1182
- } catch (error) {
1183
- reject(error);
1184
- }
1185
- });
1186
- }
1187
- /**
1188
- * Stops the hub server
1189
- */
1190
- async stop() {
1191
- if (this.heartbeatInterval) {
1192
- clearInterval(this.heartbeatInterval);
1193
- this.heartbeatInterval = null;
1194
- }
1195
- if (this.timeoutCheckInterval) {
1196
- clearInterval(this.timeoutCheckInterval);
1197
- this.timeoutCheckInterval = null;
1198
- }
1199
- return new Promise((resolve) => {
1200
- if (this.wss) {
1201
- for (const [ws] of this.clients) {
1202
- ws.close();
218
+ var records = new List<INPUT_RECORD>();
219
+ foreach (char c in text) {
220
+ records.Add(new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, UnicodeChar=(ushort)c });
221
+ records.Add(new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, UnicodeChar=(ushort)c });
1203
222
  }
1204
- this.clients.clear();
1205
- this.memberToWs.clear();
1206
- this.wss.close(() => {
1207
- this.wss = null;
1208
- log("INFO", "Hub server stopped gracefully");
1209
- resolve();
1210
- });
1211
- } else {
1212
- resolve();
1213
- }
1214
- });
1215
- }
1216
- handleConnection(ws) {
1217
- const connection = {
1218
- ws,
1219
- lastPing: /* @__PURE__ */ new Date()
1220
- };
1221
- this.clients.set(ws, connection);
1222
- log("INFO", "New client connected", { totalClients: this.clients.size });
1223
- ws.on("message", async (data) => {
1224
- await this.handleMessage(ws, data.toString());
1225
- });
1226
- ws.on("close", async () => {
1227
- await this.handleDisconnect(ws);
1228
- });
1229
- ws.on("error", (error) => {
1230
- log("ERROR", "Client connection error", { error: error.message });
1231
- });
1232
- }
1233
- async handleMessage(ws, data) {
1234
- const connection = this.clients.get(ws);
1235
- if (!connection) return;
1236
- try {
1237
- const message = parseClientMessage(data);
1238
- connection.lastPing = /* @__PURE__ */ new Date();
1239
- log("DEBUG", `Received message from client`, {
1240
- type: message.type,
1241
- memberId: connection.memberId
1242
- });
1243
- switch (message.type) {
1244
- case "JOIN":
1245
- await this.handleJoin(ws, connection, message.teamName, message.displayName);
1246
- break;
1247
- case "LEAVE":
1248
- await this.handleLeave(ws, connection);
1249
- break;
1250
- case "ASK":
1251
- await this.handleAsk(ws, connection, message);
1252
- break;
1253
- case "REPLY":
1254
- await this.handleReply(ws, connection, message);
1255
- break;
1256
- case "GET_INBOX":
1257
- await this.handleGetInbox(ws, connection, message.requestId);
1258
- break;
1259
- case "PING":
1260
- this.send(ws, { type: "PONG", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1261
- break;
1262
- }
1263
- } catch (error) {
1264
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1265
- log("ERROR", "Failed to handle message", {
1266
- error: errorMessage,
1267
- memberId: connection.memberId
1268
- });
1269
- this.send(ws, createErrorMessage("INVALID_MESSAGE", errorMessage));
1270
- }
1271
- }
1272
- async handleJoin(ws, connection, teamName, displayName) {
1273
- try {
1274
- log("INFO", "Member attempting to join", { teamName, displayName });
1275
- const result = await this.joinTeamUseCase.execute({ teamName, displayName });
1276
- connection.memberId = result.memberId;
1277
- connection.teamId = result.teamId;
1278
- this.memberToWs.set(result.memberId, ws);
1279
- const memberInfo = await this.getMemberInfo(result.memberId);
1280
- this.send(ws, {
1281
- type: "JOINED",
1282
- member: memberInfo,
1283
- memberCount: result.memberCount
1284
- });
1285
- log("INFO", "Member joined successfully", {
1286
- memberId: result.memberId,
1287
- teamName,
1288
- displayName,
1289
- memberCount: result.memberCount
1290
- });
1291
- } catch (error) {
1292
- const errorMessage = error instanceof Error ? error.message : "Join failed";
1293
- log("ERROR", "Member join failed", { teamName, displayName, error: errorMessage });
1294
- this.send(ws, createErrorMessage("JOIN_FAILED", errorMessage));
1295
- }
1296
- }
1297
- async handleLeave(ws, connection) {
1298
- if (connection.memberId && connection.teamId) {
1299
- await this.removeMember(connection.memberId, connection.teamId);
1300
- connection.memberId = void 0;
1301
- connection.teamId = void 0;
1302
- }
1303
- this.send(ws, { type: "LEFT", memberId: connection.memberId });
1304
- }
1305
- async handleAsk(ws, connection, message) {
1306
- if (!connection.memberId) {
1307
- log("WARN", "ASK attempt without joining team", { toTeam: message.toTeam });
1308
- this.send(ws, createErrorMessage("NOT_JOINED", "Must join a team first", message.requestId));
1309
- return;
1310
- }
1311
- try {
1312
- log("INFO", "Question asked", {
1313
- fromMemberId: connection.memberId,
1314
- toTeam: message.toTeam,
1315
- contentPreview: message.content.substring(0, 50) + "..."
1316
- });
1317
- const result = await this.askQuestionUseCase.execute({
1318
- fromMemberId: connection.memberId,
1319
- toTeamName: message.toTeam,
1320
- content: message.content,
1321
- format: message.format
1322
- });
1323
- this.send(ws, {
1324
- type: "QUESTION_SENT",
1325
- questionId: result.questionId,
1326
- toTeamId: result.toTeamId,
1327
- status: result.status,
1328
- requestId: message.requestId
1329
- });
1330
- log("INFO", "Question sent successfully", {
1331
- questionId: result.questionId,
1332
- fromMemberId: connection.memberId,
1333
- toTeamId: result.toTeamId
1334
- });
1335
- } catch (error) {
1336
- const errorMessage = error instanceof Error ? error.message : "Ask failed";
1337
- log("ERROR", "Question failed", {
1338
- fromMemberId: connection.memberId,
1339
- toTeam: message.toTeam,
1340
- error: errorMessage
1341
- });
1342
- this.send(ws, createErrorMessage("ASK_FAILED", errorMessage, message.requestId));
1343
- }
1344
- }
1345
- async handleReply(ws, connection, message) {
1346
- if (!connection.memberId) {
1347
- log("WARN", "REPLY attempt without joining team", { questionId: message.questionId });
1348
- this.send(ws, createErrorMessage("NOT_JOINED", "Must join a team first"));
1349
- return;
1350
- }
1351
- try {
1352
- log("INFO", "Reply received", {
1353
- fromMemberId: connection.memberId,
1354
- questionId: message.questionId,
1355
- contentPreview: message.content.substring(0, 50) + "..."
1356
- });
1357
- await this.replyQuestionUseCase.execute({
1358
- fromMemberId: connection.memberId,
1359
- questionId: message.questionId,
1360
- content: message.content,
1361
- format: message.format
1362
- });
1363
- log("INFO", "Reply delivered successfully", {
1364
- fromMemberId: connection.memberId,
1365
- questionId: message.questionId
1366
- });
1367
- } catch (error) {
1368
- const errorMessage = error instanceof Error ? error.message : "Reply failed";
1369
- log("ERROR", "Reply failed", {
1370
- fromMemberId: connection.memberId,
1371
- questionId: message.questionId,
1372
- error: errorMessage
1373
- });
1374
- this.send(ws, createErrorMessage("REPLY_FAILED", errorMessage));
1375
- }
1376
- }
1377
- async handleGetInbox(ws, connection, requestId) {
1378
- if (!connection.memberId || !connection.teamId) {
1379
- this.send(ws, createErrorMessage("NOT_JOINED", "Must join a team first", requestId));
1380
- return;
1381
- }
1382
- try {
1383
- const result = await this.getInboxUseCase.execute({
1384
- memberId: connection.memberId,
1385
- teamId: connection.teamId
1386
- });
1387
- const questions = await Promise.all(
1388
- result.questions.map(async (q) => ({
1389
- questionId: q.questionId,
1390
- from: await this.getMemberInfo(q.fromMemberId),
1391
- content: q.content,
1392
- format: q.format,
1393
- status: q.status,
1394
- createdAt: q.createdAt.toISOString(),
1395
- ageMs: q.ageMs
1396
- }))
1397
- );
1398
- this.send(ws, {
1399
- type: "INBOX",
1400
- questions,
1401
- totalCount: result.totalCount,
1402
- pendingCount: result.pendingCount,
1403
- requestId
1404
- });
1405
- } catch (error) {
1406
- const errorMessage = error instanceof Error ? error.message : "Get inbox failed";
1407
- this.send(ws, createErrorMessage("INBOX_FAILED", errorMessage, requestId));
1408
- }
1409
- }
1410
- async handleDisconnect(ws) {
1411
- const connection = this.clients.get(ws);
1412
- if (connection?.memberId && connection.teamId) {
1413
- log("INFO", "Client disconnecting", {
1414
- memberId: connection.memberId,
1415
- teamId: connection.teamId
1416
- });
1417
- await this.removeMember(connection.memberId, connection.teamId);
1418
- this.memberToWs.delete(connection.memberId);
1419
- }
1420
- this.clients.delete(ws);
1421
- log("INFO", "Client disconnected", { totalClients: this.clients.size });
1422
- }
1423
- async removeMember(memberId, teamId) {
1424
- const member = await this.memberRepository.findById(memberId);
1425
- if (member) {
1426
- member.goOffline();
1427
- await this.memberRepository.save(member);
1428
- }
1429
- const team = await this.teamRepository.findById(teamId);
1430
- if (team) {
1431
- team.removeMember(memberId);
1432
- await this.teamRepository.save(team);
1433
- await this.broadcastToTeam(teamId, memberId, {
1434
- type: "MEMBER_LEFT",
1435
- memberId,
1436
- teamId
1437
- });
1438
- }
1439
- }
1440
- async deliverQuestion(question) {
1441
- const team = await this.teamRepository.findById(question.toTeamId);
1442
- if (!team) {
1443
- log("WARN", "Cannot deliver question - team not found", { toTeamId: question.toTeamId });
1444
- return;
1445
- }
1446
- const fromMember = await this.memberRepository.findById(question.fromMemberId);
1447
- if (!fromMember) {
1448
- log("WARN", "Cannot deliver question - from member not found", {
1449
- fromMemberId: question.fromMemberId
1450
- });
1451
- return;
1452
- }
1453
- const memberInfo = await this.getMemberInfo(question.fromMemberId);
1454
- let deliveredCount = 0;
1455
- for (const memberId of team.memberIds) {
1456
- const ws = this.memberToWs.get(memberId);
1457
- if (ws && ws.readyState === WebSocket.OPEN) {
1458
- this.send(ws, {
1459
- type: "QUESTION",
1460
- questionId: question.id,
1461
- from: memberInfo,
1462
- content: question.content.text,
1463
- format: question.content.format,
1464
- createdAt: question.createdAt.toISOString()
1465
- });
1466
- deliveredCount++;
1467
- }
1468
- }
1469
- log("INFO", "Question delivered to team", {
1470
- questionId: question.id,
1471
- toTeamId: question.toTeamId,
1472
- teamSize: team.memberIds.size,
1473
- deliveredCount
1474
- });
1475
- }
1476
- async deliverAnswer(question, answer, answeredByMemberId) {
1477
- const ws = this.memberToWs.get(question.fromMemberId);
1478
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1479
- log("WARN", "Cannot deliver answer - questioner not connected", {
1480
- questionId: question.id,
1481
- fromMemberId: question.fromMemberId
1482
- });
1483
- return;
1484
- }
1485
- const memberInfo = await this.getMemberInfo(answeredByMemberId);
1486
- this.send(ws, {
1487
- type: "ANSWER",
1488
- questionId: question.id,
1489
- from: memberInfo,
1490
- content: answer.content.text,
1491
- format: answer.content.format,
1492
- answeredAt: answer.createdAt.toISOString()
1493
- });
1494
- log("INFO", "Answer delivered", {
1495
- questionId: question.id,
1496
- answeredBy: answeredByMemberId,
1497
- deliveredTo: question.fromMemberId
1498
- });
1499
- }
1500
- async broadcastToTeam(teamId, excludeMemberId, message) {
1501
- const team = await this.teamRepository.findById(teamId);
1502
- if (!team) return;
1503
- for (const memberId of team.getOtherMemberIds(excludeMemberId)) {
1504
- const ws = this.memberToWs.get(memberId);
1505
- if (ws && ws.readyState === WebSocket.OPEN) {
1506
- this.send(ws, message);
1507
- }
1508
- }
1509
- }
1510
- async getMemberInfo(memberId) {
1511
- const member = await this.memberRepository.findById(memberId);
1512
- const team = member ? await this.teamRepository.findById(member.teamId) : null;
1513
- return {
1514
- memberId,
1515
- teamId: member?.teamId ?? "",
1516
- teamName: team?.name ?? "Unknown",
1517
- displayName: member?.displayName ?? "Unknown",
1518
- status: member?.status ?? "OFFLINE" /* OFFLINE */
1519
- };
1520
- }
1521
- send(ws, message) {
1522
- if (ws.readyState === WebSocket.OPEN) {
1523
- ws.send(serializeMessage(message));
223
+
224
+ var arr = records.ToArray();
225
+ uint written;
226
+ bool ok = WriteConsoleInput(hIn, arr, (uint)arr.Length, out written);
227
+ CloseHandle(hIn);
228
+ return ok ? (int)written : -2;
1524
229
  }
1525
- }
1526
- startHeartbeat() {
1527
- this.heartbeatInterval = setInterval(() => {
1528
- const now = /* @__PURE__ */ new Date();
1529
- for (const [ws, connection] of this.clients) {
1530
- const timeSinceLastPing = now.getTime() - connection.lastPing.getTime();
1531
- if (timeSinceLastPing > config.hub.clientTimeout) {
1532
- ws.terminate();
230
+
231
+ // Focus the console window for WScript.Shell.SendKeys
232
+ public static IntPtr FocusConsole(uint pid) {
233
+ FreeConsole();
234
+ if (!AttachConsole(pid)) return IntPtr.Zero;
235
+ IntPtr hwnd = GetConsoleWindow();
236
+ if (hwnd != IntPtr.Zero) SetForegroundWindow(hwnd);
237
+ return hwnd;
238
+ }
239
+ }
240
+ `;
241
+ function buildScript(claudePid, body) {
242
+ const logFile = join(tmpdir(), `cc-inject-${Date.now()}.log`).replace(/\\/g, "/");
243
+ return `
244
+ $log = "${logFile}"
245
+ function Log($msg) { Add-Content -Path $log -Value $msg -Encoding UTF8 }
246
+ $claudePid = ${claudePid}
247
+ try { Add-Type @'${CS_CONINJECT}'@ } catch { }
248
+ ${body}
249
+ `;
250
+ }
251
+ function run(script) {
252
+ return new Promise((resolve) => {
253
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
254
+ execFile(
255
+ "powershell",
256
+ ["-NoProfile", "-WindowStyle", "Hidden", "-EncodedCommand", encoded],
257
+ { windowsHide: true },
258
+ () => {
259
+ const logFile = script.match(/\$log = "([^"]+)"/)?.[1];
260
+ if (logFile) try {
261
+ unlinkSync(logFile);
262
+ } catch {
1533
263
  }
264
+ resolve();
1534
265
  }
1535
- }, config.hub.heartbeatInterval);
1536
- }
1537
- startTimeoutCheck() {
1538
- this.timeoutCheckInterval = setInterval(async () => {
1539
- await this.questionRepository.markTimedOut(config.communication.defaultTimeout);
1540
- }, 5e3);
1541
- }
266
+ );
267
+ });
268
+ }
269
+ async function windowsInject(text) {
270
+ const claudePid = process.ppid;
271
+ const textB64 = Buffer.from(text, "utf16le").toString("base64");
272
+ const script = buildScript(claudePid, `
273
+ $textBytes = [System.Convert]::FromBase64String('${textB64}')
274
+ $text = [System.Text.Encoding]::Unicode.GetString($textBytes)
275
+ $wsh = New-Object -ComObject WScript.Shell
276
+
277
+ # 1. Focus console and send Ctrl+U to save user's current text to kill ring
278
+ $hwnd = [ConInject]::FocusConsole([uint32]$claudePid)
279
+ Start-Sleep -Milliseconds 150
280
+ $wsh.SendKeys('^u')
281
+ Start-Sleep -Milliseconds 150
282
+
283
+ # 2. Write question text into console input buffer
284
+ [ConInject]::InjectText([uint32]$claudePid, $text) | Out-Null
285
+
286
+ # 3. Re-focus (InjectText calls FreeConsole internally, focus may be lost)
287
+ [ConInject]::FocusConsole([uint32]$claudePid) | Out-Null
288
+ Start-Sleep -Milliseconds 150
289
+
290
+ # 4. Send Enter
291
+ $wsh.SendKeys('~')
292
+ `);
293
+ await run(script);
294
+ }
295
+ async function windowsInjectCtrlY() {
296
+ const claudePid = process.ppid;
297
+ const script = buildScript(claudePid, `
298
+ $wsh = New-Object -ComObject WScript.Shell
299
+ $hwnd = [ConInject]::FocusConsole([uint32]$claudePid)
300
+ Start-Sleep -Milliseconds 150
301
+ $wsh.SendKeys('^y')
302
+ `);
303
+ await run(script);
304
+ }
305
+
306
+ // src/infrastructure/terminal-injector/index.ts
307
+ function formatQuestionForInjection(question) {
308
+ const content = question.content.replace(/\s+/g, " ").trim();
309
+ return `[HUB SORUSU] ID:${question.questionId} | ${question.from.displayName} (${question.from.teamName}): ${content} | Cevapla ve reply tool ile "${question.questionId}" ID'sine g\xF6nder, onay bekleme.`;
310
+ }
311
+ async function injectQuestionToTerminal(question) {
312
+ if (process.platform !== "win32") return;
313
+ const text = formatQuestionForInjection(question);
314
+ await windowsInject(text);
315
+ }
316
+ async function injectRestoreText() {
317
+ if (process.platform !== "win32") return;
318
+ await windowsInjectCtrlY();
319
+ }
320
+
321
+ // src/infrastructure/terminal-injector/injection-queue.ts
322
+ var REPLY_TIMEOUT_MS = 5 * 60 * 1e3;
323
+ var InjectionQueue = class extends EventEmitter {
324
+ queue = [];
325
+ processing = false;
1542
326
  /**
1543
- * Gets the number of connected clients
327
+ * Add a question to the queue. Starts processing if idle.
1544
328
  */
1545
- get clientCount() {
1546
- return this.clients.size;
329
+ enqueue(question) {
330
+ this.queue.push(question);
331
+ if (!this.processing) void this.processNext();
1547
332
  }
1548
333
  /**
1549
- * Checks if the server is running
334
+ * Called by the reply tool after a reply is successfully sent.
335
+ * Unblocks the queue to process the next question.
1550
336
  */
1551
- get isRunning() {
1552
- return this.wss !== null;
337
+ notifyReplied() {
338
+ this.emit("replied");
1553
339
  }
1554
- };
1555
- var HubClient = class {
1556
- constructor(options = {}, events = {}) {
1557
- this.options = options;
1558
- this.events = events;
1559
- }
1560
- ws = null;
1561
- pendingRequests = /* @__PURE__ */ new Map();
1562
- reconnectAttempts = 0;
1563
- isClosing = false;
1564
- memberId;
1565
- teamId;
1566
- teamName;
1567
- displayName;
1568
- /**
1569
- * Connects to the Hub server
1570
- */
1571
- async connect() {
1572
- const host = this.options.host ?? config.hub.host;
1573
- const port = this.options.port ?? config.hub.port;
1574
- const url = `ws://${host}:${port}`;
1575
- return new Promise((resolve, reject) => {
1576
- try {
1577
- this.ws = new WebSocket2(url);
1578
- this.ws.on("open", () => {
1579
- this.reconnectAttempts = 0;
1580
- this.startPingInterval();
1581
- this.events.onConnected?.();
1582
- resolve();
1583
- });
1584
- this.ws.on("message", (data) => {
1585
- this.handleMessage(data.toString());
1586
- });
1587
- this.ws.on("close", () => {
1588
- this.handleDisconnect();
1589
- });
1590
- this.ws.on("error", (error) => {
1591
- this.events.onError?.(error);
1592
- if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) {
1593
- reject(error);
1594
- }
1595
- });
1596
- } catch (error) {
1597
- reject(error);
1598
- }
340
+ async processNext() {
341
+ if (this.queue.length === 0) {
342
+ this.processing = false;
343
+ return;
344
+ }
345
+ this.processing = true;
346
+ const question = this.queue.shift();
347
+ await injectQuestionToTerminal(question);
348
+ await new Promise((resolve) => {
349
+ const timer = setTimeout(resolve, REPLY_TIMEOUT_MS);
350
+ this.once("replied", () => {
351
+ clearTimeout(timer);
352
+ resolve();
353
+ });
1599
354
  });
355
+ await injectRestoreText();
356
+ void this.processNext();
1600
357
  }
358
+ };
359
+ var injectionQueue = new InjectionQueue();
360
+
361
+ // src/config/index.ts
362
+ var config = {
1601
363
  /**
1602
- * Disconnects from the Hub server
364
+ * P2P node configuration
1603
365
  */
1604
- async disconnect() {
1605
- this.isClosing = true;
1606
- if (this.memberId) {
1607
- this.send({ type: "LEAVE" });
1608
- }
1609
- return new Promise((resolve) => {
1610
- if (this.ws) {
1611
- this.ws.close();
1612
- this.ws = null;
1613
- }
1614
- this.isClosing = false;
1615
- resolve();
366
+ p2p: {
367
+ /**
368
+ * Minimum port for the random WS server port range
369
+ */
370
+ portRangeMin: 1e4,
371
+ /**
372
+ * Maximum port for the random WS server port range
373
+ */
374
+ portRangeMax: 19999
375
+ }};
376
+
377
+ // src/infrastructure/p2p/p2p-node.ts
378
+ function getRandomPort(min, max) {
379
+ return new Promise((resolve) => {
380
+ const port = Math.floor(Math.random() * (max - min + 1)) + min;
381
+ const server = createServer();
382
+ server.listen(port, () => {
383
+ server.close(() => resolve(port));
384
+ });
385
+ server.on("error", () => {
386
+ resolve(getRandomPort(min, max));
1616
387
  });
388
+ });
389
+ }
390
+ var P2PNode = class {
391
+ wss = null;
392
+ mdnsDiscovery = null;
393
+ port = 0;
394
+ // Connections indexed by remote team name
395
+ peerConns = /* @__PURE__ */ new Map();
396
+ // Reverse lookup: ws → teamName (for cleanup on incoming connections)
397
+ wsToTeam = /* @__PURE__ */ new Map();
398
+ // Questions we received from remote peers (our inbox)
399
+ incomingQuestions = /* @__PURE__ */ new Map();
400
+ // Answers we received for questions we asked
401
+ receivedAnswers = /* @__PURE__ */ new Map();
402
+ // Maps questionId → remote teamName (so we know who to poll)
403
+ questionToTeam = /* @__PURE__ */ new Map();
404
+ // Pending response handlers (request-response correlation by filter)
405
+ pendingHandlers = /* @__PURE__ */ new Set();
406
+ localMember = null;
407
+ _isStarted = false;
408
+ get isConnected() {
409
+ return this._isStarted;
410
+ }
411
+ get currentTeamId() {
412
+ return this.localMember?.teamName;
1617
413
  }
1618
414
  /**
1619
- * Joins a team
415
+ * Starts the WS server on a random port and initialises mDNS.
416
+ * Called automatically from join() if not yet started.
1620
417
  */
418
+ async start() {
419
+ this.port = await getRandomPort(config.p2p.portRangeMin, config.p2p.portRangeMax);
420
+ this.wss = new WebSocketServer({ port: this.port });
421
+ this.setupWssHandlers();
422
+ this.mdnsDiscovery = new MdnsDiscovery();
423
+ this._isStarted = true;
424
+ console.error(`P2P node started on port ${this.port}`);
425
+ }
1621
426
  async join(teamName, displayName) {
1622
- v4();
1623
- this.send({
1624
- type: "JOIN",
1625
- teamName,
1626
- displayName
427
+ if (!this._isStarted) {
428
+ await this.start();
429
+ }
430
+ const memberId = v4();
431
+ this.localMember = { memberId, teamName, displayName };
432
+ this.mdnsDiscovery.onPeerFound((peer) => {
433
+ if (peer.teamName !== teamName) {
434
+ console.error(`Discovered peer '${peer.teamName}' at ${peer.host}:${peer.port}`);
435
+ this.connectToPeer(peer.teamName, peer.host, peer.port).catch((err) => {
436
+ console.error(`Could not eagerly connect to ${peer.teamName}:`, err);
437
+ });
438
+ }
1627
439
  });
1628
- const response = await this.waitForResponse(
1629
- (msg) => msg.type === "JOINED",
1630
- 3e4
1631
- );
1632
- this.memberId = response.member.memberId;
1633
- this.teamId = response.member.teamId;
1634
- this.teamName = teamName;
1635
- this.displayName = displayName;
1636
- return response.member;
440
+ this.mdnsDiscovery.announce(this.port, teamName, memberId);
441
+ this.mdnsDiscovery.discover();
442
+ return { memberId, teamId: teamName, teamName, displayName, status: "ONLINE" };
1637
443
  }
1638
- /**
1639
- * Asks a question to another team
1640
- */
1641
- async ask(toTeam, content, format = "markdown", timeoutMs = config.communication.defaultTimeout) {
444
+ async ask(toTeam, content, format) {
445
+ const ws = await this.getPeerConnection(toTeam);
446
+ const questionId = v4();
1642
447
  const requestId = v4();
1643
- this.send({
1644
- type: "ASK",
448
+ this.questionToTeam.set(questionId, toTeam);
449
+ const ackPromise = this.waitForResponse(
450
+ (m) => m.type === "P2P_ASK_ACK" && m.requestId === requestId,
451
+ 5e3
452
+ );
453
+ const msg = {
454
+ type: "P2P_ASK",
455
+ questionId,
456
+ fromMemberId: this.localMember.memberId,
457
+ fromTeam: this.localMember.teamName,
1645
458
  toTeam,
1646
459
  content,
1647
460
  format,
1648
461
  requestId
1649
- });
1650
- const questionSent = await this.waitForResponse(
1651
- (msg) => msg.type === "QUESTION_SENT" && "requestId" in msg && msg.requestId === requestId,
1652
- 5e3
1653
- );
1654
- const questionId = questionSent.questionId;
1655
- const answer = await this.waitForResponse(
1656
- (msg) => msg.type === "ANSWER" && msg.questionId === questionId,
1657
- timeoutMs
1658
- );
1659
- return answer;
462
+ };
463
+ ws.send(serializeP2PMsg(msg));
464
+ await ackPromise;
465
+ return questionId;
1660
466
  }
1661
- /**
1662
- * Gets the inbox (pending questions)
1663
- */
1664
- async getInbox() {
467
+ async checkAnswer(questionId) {
468
+ const cached = this.receivedAnswers.get(questionId);
469
+ if (cached) {
470
+ return {
471
+ questionId,
472
+ from: { displayName: `${cached.fromTeam} Claude`, teamName: cached.fromTeam },
473
+ content: cached.content,
474
+ format: cached.format,
475
+ answeredAt: cached.answeredAt
476
+ };
477
+ }
478
+ const toTeam = this.questionToTeam.get(questionId);
479
+ if (!toTeam) return null;
480
+ const ws = this.peerConns.get(toTeam);
481
+ if (!ws || ws.readyState !== WebSocket.OPEN) return null;
1665
482
  const requestId = v4();
1666
- this.send({
1667
- type: "GET_INBOX",
1668
- requestId
1669
- });
1670
- return this.waitForResponse(
1671
- (msg) => msg.type === "INBOX" && msg.requestId === requestId,
483
+ const responsePromise = this.waitForResponse(
484
+ (m) => m.type === "P2P_ANSWER" && m.questionId === questionId || m.type === "P2P_ANSWER_PENDING" && m.requestId === requestId,
1672
485
  5e3
1673
486
  );
487
+ const getMsg = {
488
+ type: "P2P_GET_ANSWER",
489
+ questionId,
490
+ requestId
491
+ };
492
+ ws.send(serializeP2PMsg(getMsg));
493
+ const response = await responsePromise;
494
+ if (response.type === "P2P_ANSWER_PENDING") return null;
495
+ const answer = response;
496
+ this.receivedAnswers.set(questionId, {
497
+ content: answer.content,
498
+ format: answer.format,
499
+ answeredAt: answer.answeredAt,
500
+ fromTeam: answer.fromTeam,
501
+ fromMemberId: answer.fromMemberId
502
+ });
503
+ return {
504
+ questionId,
505
+ from: { displayName: `${answer.fromTeam} Claude`, teamName: answer.fromTeam },
506
+ content: answer.content,
507
+ format: answer.format,
508
+ answeredAt: answer.answeredAt
509
+ };
1674
510
  }
1675
- /**
1676
- * Replies to a question
1677
- */
1678
- async reply(questionId, content, format = "markdown") {
1679
- this.send({
1680
- type: "REPLY",
511
+ async reply(questionId, content, format) {
512
+ const question = this.incomingQuestions.get(questionId);
513
+ if (!question) throw new Error(`Question ${questionId} not found in inbox`);
514
+ question.answered = true;
515
+ question.answerContent = content;
516
+ question.answerFormat = format;
517
+ const answerMsg = {
518
+ type: "P2P_ANSWER",
1681
519
  questionId,
1682
520
  content,
1683
- format
1684
- });
521
+ format,
522
+ answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
523
+ fromTeam: this.localMember.teamName,
524
+ fromMemberId: this.localMember.memberId
525
+ };
526
+ if (question.ws.readyState === WebSocket.OPEN) {
527
+ question.ws.send(serializeP2PMsg(answerMsg));
528
+ }
1685
529
  }
1686
- /**
1687
- * Checks if connected
1688
- */
1689
- get isConnected() {
1690
- return this.ws !== null && this.ws.readyState === WebSocket2.OPEN;
530
+ async getInbox() {
531
+ const now = Date.now();
532
+ const questions = [...this.incomingQuestions.values()].filter((q) => !q.answered).map((q) => ({
533
+ questionId: q.questionId,
534
+ from: { displayName: `${q.fromTeam} Claude`, teamName: q.fromTeam },
535
+ content: q.content,
536
+ format: q.format,
537
+ status: "PENDING",
538
+ createdAt: q.createdAt.toISOString(),
539
+ ageMs: now - q.createdAt.getTime()
540
+ }));
541
+ return {
542
+ questions,
543
+ totalCount: questions.length,
544
+ pendingCount: questions.length
545
+ };
1691
546
  }
1692
- /**
1693
- * Gets the current member ID
1694
- */
1695
- get currentMemberId() {
1696
- return this.memberId;
547
+ async disconnect() {
548
+ this.mdnsDiscovery?.destroy();
549
+ for (const ws of this.peerConns.values()) {
550
+ ws.close();
551
+ }
552
+ this.peerConns.clear();
553
+ await new Promise((resolve) => {
554
+ if (this.wss) {
555
+ this.wss.close(() => resolve());
556
+ } else {
557
+ resolve();
558
+ }
559
+ });
560
+ this._isStarted = false;
561
+ }
562
+ // ---------------------------------------------------------------------------
563
+ // Private: WebSocket server setup
564
+ // ---------------------------------------------------------------------------
565
+ setupWssHandlers() {
566
+ this.wss.on("connection", (ws) => {
567
+ ws.on("message", (data) => {
568
+ try {
569
+ const msg = parseP2PMsg(data.toString());
570
+ this.handleMessage(ws, msg);
571
+ } catch (err) {
572
+ console.error("Failed to parse incoming P2P message:", err);
573
+ }
574
+ });
575
+ ws.on("close", () => {
576
+ const team = this.wsToTeam.get(ws);
577
+ if (team) {
578
+ if (this.peerConns.get(team) === ws) {
579
+ this.peerConns.delete(team);
580
+ }
581
+ this.wsToTeam.delete(ws);
582
+ }
583
+ });
584
+ });
1697
585
  }
1698
- /**
1699
- * Gets the current team ID
1700
- */
1701
- get currentTeamId() {
1702
- return this.teamId;
586
+ // ---------------------------------------------------------------------------
587
+ // Private: unified message handler (used for both incoming & outgoing sockets)
588
+ // ---------------------------------------------------------------------------
589
+ handleMessage(ws, msg) {
590
+ for (const handler of this.pendingHandlers) {
591
+ handler(msg);
592
+ }
593
+ switch (msg.type) {
594
+ case "P2P_HELLO":
595
+ this.wsToTeam.set(ws, msg.fromTeam);
596
+ this.peerConns.set(msg.fromTeam, ws);
597
+ console.error(`Peer identified: ${msg.fromTeam}`);
598
+ break;
599
+ case "P2P_ASK":
600
+ this.handleIncomingAsk(ws, msg);
601
+ break;
602
+ case "P2P_GET_ANSWER":
603
+ this.handleGetAnswer(ws, msg);
604
+ break;
605
+ case "P2P_ANSWER":
606
+ if (!this.receivedAnswers.has(msg.questionId)) {
607
+ this.receivedAnswers.set(msg.questionId, {
608
+ content: msg.content,
609
+ format: msg.format,
610
+ answeredAt: msg.answeredAt,
611
+ fromTeam: msg.fromTeam,
612
+ fromMemberId: msg.fromMemberId
613
+ });
614
+ }
615
+ break;
616
+ case "P2P_PING":
617
+ ws.send(serializeP2PMsg({ type: "P2P_PONG" }));
618
+ break;
619
+ }
620
+ }
621
+ handleIncomingAsk(ws, msg) {
622
+ this.incomingQuestions.set(msg.questionId, {
623
+ questionId: msg.questionId,
624
+ fromTeam: msg.fromTeam,
625
+ fromMemberId: msg.fromMemberId,
626
+ content: msg.content,
627
+ format: msg.format,
628
+ createdAt: /* @__PURE__ */ new Date(),
629
+ ws,
630
+ answered: false
631
+ });
632
+ injectionQueue.enqueue({
633
+ questionId: msg.questionId,
634
+ from: {
635
+ displayName: `${msg.fromTeam} Claude`,
636
+ teamName: msg.fromTeam
637
+ },
638
+ content: msg.content,
639
+ format: msg.format,
640
+ status: "PENDING",
641
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
642
+ ageMs: 0
643
+ });
644
+ const ack = {
645
+ type: "P2P_ASK_ACK",
646
+ questionId: msg.questionId,
647
+ requestId: msg.requestId
648
+ };
649
+ ws.send(serializeP2PMsg(ack));
1703
650
  }
1704
- send(message) {
1705
- if (this.ws && this.ws.readyState === WebSocket2.OPEN) {
1706
- this.ws.send(serializeMessage(message));
651
+ handleGetAnswer(ws, msg) {
652
+ const question = this.incomingQuestions.get(msg.questionId);
653
+ if (!question?.answered) {
654
+ const pending = {
655
+ type: "P2P_ANSWER_PENDING",
656
+ questionId: msg.questionId,
657
+ requestId: msg.requestId
658
+ };
659
+ ws.send(serializeP2PMsg(pending));
660
+ return;
661
+ }
662
+ const answer = {
663
+ type: "P2P_ANSWER",
664
+ questionId: msg.questionId,
665
+ content: question.answerContent,
666
+ format: question.answerFormat,
667
+ answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
668
+ fromTeam: this.localMember.teamName,
669
+ fromMemberId: this.localMember.memberId,
670
+ requestId: msg.requestId
671
+ };
672
+ ws.send(serializeP2PMsg(answer));
673
+ }
674
+ // ---------------------------------------------------------------------------
675
+ // Private: peer connection management
676
+ // ---------------------------------------------------------------------------
677
+ async getPeerConnection(teamName) {
678
+ const existing = this.peerConns.get(teamName);
679
+ if (existing && existing.readyState === WebSocket.OPEN) {
680
+ return existing;
681
+ }
682
+ let peer = this.mdnsDiscovery?.getPeerByTeam(teamName);
683
+ if (!peer) {
684
+ this.mdnsDiscovery?.discover();
685
+ await this.waitForMdnsPeer(teamName, 1e4);
686
+ peer = this.mdnsDiscovery?.getPeerByTeam(teamName);
687
+ }
688
+ if (!peer) {
689
+ throw new Error(
690
+ `Peer for team '${teamName}' not found via mDNS. Make sure the other terminal has joined with that team name.`
691
+ );
1707
692
  }
693
+ return this.connectToPeer(teamName, peer.host, peer.port);
1708
694
  }
1709
- handleMessage(data) {
1710
- try {
1711
- const message = parseHubMessage(data);
1712
- for (const [requestId, pending] of this.pendingRequests) {
695
+ async connectToPeer(teamName, host, port) {
696
+ const existing = this.peerConns.get(teamName);
697
+ if (existing && existing.readyState === WebSocket.OPEN) {
698
+ return existing;
699
+ }
700
+ const ws = new WebSocket(`ws://${host}:${port}`);
701
+ await new Promise((resolve, reject) => {
702
+ const timeout = setTimeout(
703
+ () => reject(new Error(`Connection timeout to team '${teamName}'`)),
704
+ 5e3
705
+ );
706
+ ws.on("open", () => {
707
+ clearTimeout(timeout);
708
+ const hello = {
709
+ type: "P2P_HELLO",
710
+ fromTeam: this.localMember?.teamName ?? "unknown",
711
+ fromMemberId: this.localMember?.memberId ?? "unknown"
712
+ };
713
+ ws.send(serializeP2PMsg(hello));
714
+ resolve();
715
+ });
716
+ ws.on("error", (err) => {
717
+ clearTimeout(timeout);
718
+ reject(err);
719
+ });
720
+ });
721
+ ws.on("message", (data) => {
722
+ try {
723
+ const msg = parseP2PMsg(data.toString());
724
+ this.handleMessage(ws, msg);
725
+ } catch (err) {
726
+ console.error("Failed to parse P2P message:", err);
1713
727
  }
1714
- switch (message.type) {
1715
- case "QUESTION":
1716
- this.events.onQuestion?.(message);
1717
- break;
1718
- case "ANSWER":
1719
- this.events.onAnswer?.(message);
1720
- break;
1721
- case "MEMBER_JOINED":
1722
- this.events.onMemberJoined?.(message.member);
1723
- break;
1724
- case "MEMBER_LEFT":
1725
- this.events.onMemberLeft?.(message.memberId, message.teamId);
1726
- break;
1727
- case "ERROR":
1728
- this.events.onError?.(new Error(`${message.code}: ${message.message}`));
1729
- break;
728
+ });
729
+ ws.on("close", () => {
730
+ if (this.peerConns.get(teamName) === ws) {
731
+ this.peerConns.delete(teamName);
1730
732
  }
1731
- this.resolvePendingRequest(message);
1732
- } catch (error) {
1733
- console.error("Failed to parse message:", error);
1734
- }
733
+ });
734
+ this.peerConns.set(teamName, ws);
735
+ return ws;
1735
736
  }
1736
- resolvePendingRequest(message) {
1737
- for (const [requestId, pending] of this.pendingRequests) {
1738
- this.pendingRequests.delete(requestId);
1739
- clearTimeout(pending.timeout);
1740
- pending.resolve(message);
1741
- break;
1742
- }
737
+ waitForMdnsPeer(teamName, timeoutMs) {
738
+ return new Promise((resolve, reject) => {
739
+ const deadline = Date.now() + timeoutMs;
740
+ const check = () => {
741
+ if (this.mdnsDiscovery?.getPeerByTeam(teamName)) {
742
+ resolve();
743
+ return;
744
+ }
745
+ if (Date.now() >= deadline) {
746
+ reject(new Error(`mDNS timeout: team '${teamName}' not found`));
747
+ return;
748
+ }
749
+ this.mdnsDiscovery?.discover();
750
+ setTimeout(check, 500);
751
+ };
752
+ check();
753
+ });
1743
754
  }
1744
755
  waitForResponse(filter, timeoutMs) {
1745
756
  return new Promise((resolve, reject) => {
1746
- const requestId = v4();
1747
757
  const timeout = setTimeout(() => {
1748
- this.pendingRequests.delete(requestId);
1749
- reject(new Error("Request timed out"));
758
+ this.pendingHandlers.delete(handler);
759
+ reject(new Error("P2P request timed out"));
1750
760
  }, timeoutMs);
1751
- const pending = {
1752
- resolve: (msg) => {
1753
- if (filter(msg)) {
1754
- resolve(msg);
1755
- }
1756
- },
1757
- reject,
1758
- timeout,
1759
- filter
1760
- };
1761
- this.pendingRequests.set(requestId, pending);
1762
- const originalHandler = this.handleMessage.bind(this);
1763
- const checkFilter = (data) => {
1764
- try {
1765
- const message = parseHubMessage(data);
1766
- if (filter(message)) {
1767
- this.pendingRequests.delete(requestId);
1768
- clearTimeout(timeout);
1769
- resolve(message);
1770
- }
1771
- } catch {
761
+ const handler = (msg) => {
762
+ if (filter(msg)) {
763
+ clearTimeout(timeout);
764
+ this.pendingHandlers.delete(handler);
765
+ resolve(msg);
1772
766
  }
1773
- originalHandler(data);
1774
767
  };
1775
- if (this.ws) {
1776
- this.ws.removeAllListeners("message");
1777
- this.ws.on("message", (data) => checkFilter(data.toString()));
1778
- }
768
+ this.pendingHandlers.add(handler);
1779
769
  });
1780
770
  }
1781
- handleDisconnect() {
1782
- this.events.onDisconnected?.();
1783
- if (this.isClosing) return;
1784
- const shouldReconnect = this.options.reconnect ?? true;
1785
- const maxAttempts = this.options.maxReconnectAttempts ?? config.autoStart.maxRetries;
1786
- if (shouldReconnect && this.reconnectAttempts < maxAttempts) {
1787
- this.reconnectAttempts++;
1788
- const delay = this.options.reconnectDelay ?? config.autoStart.retryDelay;
1789
- setTimeout(() => {
1790
- this.connect().then(() => {
1791
- if (this.teamName && this.displayName) {
1792
- return this.join(this.teamName, this.displayName);
1793
- }
1794
- }).catch((error) => {
1795
- this.events.onError?.(error);
1796
- });
1797
- }, delay);
1798
- }
1799
- }
1800
- pingInterval = null;
1801
- startPingInterval() {
1802
- this.pingInterval = setInterval(() => {
1803
- this.send({ type: "PING" });
1804
- }, config.hub.heartbeatInterval);
1805
- }
1806
771
  };
1807
772
  var joinSchema = {
1808
773
  team: z.string().describe('Team name to join (e.g., "frontend", "backend", "devops")'),
1809
774
  displayName: z.string().optional().describe('Display name for this terminal (default: team + " Claude")')
1810
775
  };
1811
- function registerJoinTool(server, hubClient) {
776
+ function registerJoinTool(server, client) {
1812
777
  server.tool("join", joinSchema, async (args) => {
1813
778
  const teamName = args.team;
1814
779
  const displayName = args.displayName ?? `${teamName} Claude`;
1815
780
  try {
1816
- if (!hubClient.isConnected) {
1817
- await hubClient.connect();
1818
- }
1819
- const member = await hubClient.join(teamName, displayName);
781
+ const member = await client.join(teamName, displayName);
1820
782
  return {
1821
783
  content: [
1822
784
  {
@@ -1845,16 +807,14 @@ Status: ${member.status}`
1845
807
  }
1846
808
  var askSchema = {
1847
809
  team: z.string().describe('Target team name to ask (e.g., "backend", "frontend")'),
1848
- question: z.string().describe("The question to ask (supports markdown)"),
1849
- timeout: z.number().optional().describe(`Timeout in seconds to wait for answer (default: ${config.communication.defaultTimeout / 1e3}s)`)
810
+ question: z.string().describe("The question to ask (supports markdown)")
1850
811
  };
1851
- function registerAskTool(server, hubClient) {
812
+ function registerAskTool(server, client) {
1852
813
  server.tool("ask", askSchema, async (args) => {
1853
814
  const targetTeam = args.team;
1854
815
  const question = args.question;
1855
- const timeoutMs = (args.timeout ?? config.communication.defaultTimeout / 1e3) * 1e3;
1856
816
  try {
1857
- if (!hubClient.currentTeamId) {
817
+ if (!client.currentTeamId) {
1858
818
  return {
1859
819
  content: [
1860
820
  {
@@ -1865,25 +825,76 @@ function registerAskTool(server, hubClient) {
1865
825
  isError: true
1866
826
  };
1867
827
  }
1868
- const answer = await hubClient.ask(targetTeam, question, "markdown", timeoutMs);
828
+ const questionId = await client.ask(targetTeam, question, "markdown");
829
+ const POLL_INTERVAL_MS = 5e3;
830
+ const MAX_WAIT_MS = 5 * 60 * 1e3;
831
+ const deadline = Date.now() + MAX_WAIT_MS;
832
+ while (Date.now() < deadline) {
833
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
834
+ const answer = await client.checkAnswer(questionId);
835
+ if (answer !== null) {
836
+ return {
837
+ content: [
838
+ {
839
+ type: "text",
840
+ text: `**${answer.from.displayName} (${answer.from.teamName}) cevaplad\u0131:**
841
+
842
+ ${answer.content}`
843
+ }
844
+ ]
845
+ };
846
+ }
847
+ }
1869
848
  return {
1870
849
  content: [
1871
850
  {
1872
851
  type: "text",
1873
- text: `**Answer from ${answer.from.displayName} (${answer.from.teamName}):**
852
+ text: `Soru g\xF6nderildi ancak 5 dakika i\xE7inde cevap gelmedi.
853
+ Question ID: \`${questionId}\`
1874
854
 
1875
- ${answer.content}`
855
+ Manuel kontrol i\xE7in "check_answer" tool'unu kullanabilirsin.`
1876
856
  }
1877
857
  ]
1878
858
  };
1879
859
  } catch (error) {
1880
860
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1881
- if (errorMessage.includes("timed out")) {
861
+ return {
862
+ content: [
863
+ {
864
+ type: "text",
865
+ text: `Failed to send question: ${errorMessage}`
866
+ }
867
+ ],
868
+ isError: true
869
+ };
870
+ }
871
+ });
872
+ }
873
+ var checkAnswerSchema = {
874
+ question_id: z.string().describe('The question ID returned by the "ask" tool')
875
+ };
876
+ function registerCheckAnswerTool(server, client) {
877
+ server.tool("check_answer", checkAnswerSchema, async (args) => {
878
+ const questionId = args.question_id;
879
+ try {
880
+ if (!client.currentTeamId) {
881
+ return {
882
+ content: [
883
+ {
884
+ type: "text",
885
+ text: 'You must join a team first. Use the "join" tool to join a team.'
886
+ }
887
+ ],
888
+ isError: true
889
+ };
890
+ }
891
+ const answer = await client.checkAnswer(questionId);
892
+ if (!answer) {
1882
893
  return {
1883
894
  content: [
1884
895
  {
1885
896
  type: "text",
1886
- text: `No response received from team "${targetTeam}" within ${timeoutMs / 1e3} seconds. The question has been delivered but no one answered yet.`
897
+ text: `No answer yet for question \`${questionId}\`. The other team hasn't replied yet. You can continue working and check again later.`
1887
898
  }
1888
899
  ]
1889
900
  };
@@ -1892,7 +903,19 @@ ${answer.content}`
1892
903
  content: [
1893
904
  {
1894
905
  type: "text",
1895
- text: `Failed to ask question: ${errorMessage}`
906
+ text: `**Answer from ${answer.from.displayName} (${answer.from.teamName}):**
907
+
908
+ ${answer.content}`
909
+ }
910
+ ]
911
+ };
912
+ } catch (error) {
913
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
914
+ return {
915
+ content: [
916
+ {
917
+ type: "text",
918
+ text: `Failed to check answer: ${errorMessage}`
1896
919
  }
1897
920
  ],
1898
921
  isError: true
@@ -1903,10 +926,10 @@ ${answer.content}`
1903
926
 
1904
927
  // src/presentation/mcp/tools/inbox.tool.ts
1905
928
  var inboxSchema = {};
1906
- function registerInboxTool(server, hubClient) {
929
+ function registerInboxTool(server, client) {
1907
930
  server.tool("inbox", inboxSchema, async () => {
1908
931
  try {
1909
- if (!hubClient.currentTeamId) {
932
+ if (!client.currentTeamId) {
1910
933
  return {
1911
934
  content: [
1912
935
  {
@@ -1917,7 +940,7 @@ function registerInboxTool(server, hubClient) {
1917
940
  isError: true
1918
941
  };
1919
942
  }
1920
- const inbox = await hubClient.getInbox();
943
+ const inbox = await client.getInbox();
1921
944
  if (inbox.questions.length === 0) {
1922
945
  return {
1923
946
  content: [
@@ -1969,12 +992,12 @@ var replySchema = {
1969
992
  questionId: z.string().describe("The ID of the question to reply to (from inbox)"),
1970
993
  answer: z.string().describe("Your answer to the question (supports markdown)")
1971
994
  };
1972
- function registerReplyTool(server, hubClient) {
995
+ function registerReplyTool(server, client) {
1973
996
  server.tool("reply", replySchema, async (args) => {
1974
997
  const questionId = args.questionId;
1975
998
  const answer = args.answer;
1976
999
  try {
1977
- if (!hubClient.currentTeamId) {
1000
+ if (!client.currentTeamId) {
1978
1001
  return {
1979
1002
  content: [
1980
1003
  {
@@ -1985,7 +1008,8 @@ function registerReplyTool(server, hubClient) {
1985
1008
  isError: true
1986
1009
  };
1987
1010
  }
1988
- await hubClient.reply(questionId, answer, "markdown");
1011
+ await client.reply(questionId, answer, "markdown");
1012
+ injectionQueue.notifyReplied();
1989
1013
  return {
1990
1014
  content: [
1991
1015
  {
@@ -2011,7 +1035,7 @@ function registerReplyTool(server, hubClient) {
2011
1035
 
2012
1036
  // src/presentation/mcp/server.ts
2013
1037
  function createMcpServer(options) {
2014
- const { hubClient } = options;
1038
+ const { client } = options;
2015
1039
  const server = new McpServer(
2016
1040
  {
2017
1041
  name: "claude-collab",
@@ -2026,26 +1050,18 @@ function createMcpServer(options) {
2026
1050
  }
2027
1051
  }
2028
1052
  );
2029
- registerJoinTool(server, hubClient);
2030
- registerAskTool(server, hubClient);
2031
- registerInboxTool(server, hubClient);
2032
- registerReplyTool(server, hubClient);
2033
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
2034
- return {
2035
- resources: [
2036
- {
2037
- uri: "inbox://questions",
2038
- name: "Pending Questions",
2039
- description: "Your inbox of pending questions from other teams",
2040
- mimeType: "application/json"
2041
- }
2042
- ]
2043
- };
2044
- });
2045
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2046
- if (request.params.uri === "inbox://questions") {
1053
+ registerJoinTool(server, client);
1054
+ registerAskTool(server, client);
1055
+ registerCheckAnswerTool(server, client);
1056
+ registerInboxTool(server, client);
1057
+ registerReplyTool(server, client);
1058
+ server.resource(
1059
+ "inbox-questions",
1060
+ "inbox://questions",
1061
+ { description: "Your inbox of pending questions from other teams", mimeType: "application/json" },
1062
+ async () => {
2047
1063
  try {
2048
- const inbox = await hubClient.getInbox();
1064
+ const inbox = await client.getInbox();
2049
1065
  return {
2050
1066
  contents: [
2051
1067
  {
@@ -2060,150 +1076,33 @@ function createMcpServer(options) {
2060
1076
  throw new Error(`Failed to read inbox: ${errorMessage}`);
2061
1077
  }
2062
1078
  }
2063
- throw new Error(`Unknown resource URI: ${request.params.uri}`);
2064
- });
1079
+ );
2065
1080
  return server;
2066
1081
  }
2067
1082
  async function startMcpServer(options) {
2068
- const { hubClient } = options;
2069
1083
  const server = createMcpServer(options);
2070
1084
  const transport = new StdioServerTransport();
2071
1085
  await server.connect(transport);
2072
- hubClient.events.onQuestion = async (question) => {
2073
- await server.notification({
2074
- method: "notifications/resources/updated",
2075
- params: {
2076
- uri: "inbox://questions"
2077
- }
2078
- });
2079
- console.error(`[\u{1F4EC} New Question] From: ${question.from.displayName}`);
2080
- console.error(`[\u{1F4A1} Tip] Check your inbox with: inbox()`);
2081
- };
2082
- }
2083
- async function isHubRunning(host = config.hub.host, port = config.hub.port) {
2084
- return new Promise((resolve) => {
2085
- const socket = createConnection({ host, port }, () => {
2086
- socket.end();
2087
- resolve(true);
2088
- });
2089
- socket.on("error", () => {
2090
- resolve(false);
2091
- });
2092
- socket.setTimeout(1e3, () => {
2093
- socket.destroy();
2094
- resolve(false);
2095
- });
2096
- });
2097
- }
2098
- async function waitForHub(host = config.hub.host, port = config.hub.port, maxRetries = config.autoStart.maxRetries, retryDelay = config.autoStart.retryDelay) {
2099
- for (let i = 0; i < maxRetries; i++) {
2100
- if (await isHubRunning(host, port)) {
2101
- return true;
2102
- }
2103
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
2104
- }
2105
- return false;
2106
- }
2107
- function startHubProcess(options = {}) {
2108
- const host = options.host ?? config.hub.host;
2109
- const port = options.port ?? config.hub.port;
2110
- const hubProcess = spawn(
2111
- process.execPath,
2112
- [
2113
- "--experimental-specifier-resolution=node",
2114
- new URL("../../hub-main.js", import.meta.url).pathname,
2115
- "--host",
2116
- host,
2117
- "--port",
2118
- port.toString()
2119
- ],
2120
- {
2121
- detached: true,
2122
- stdio: "ignore"
2123
- }
2124
- );
2125
- hubProcess.unref();
2126
- return hubProcess;
2127
- }
2128
- async function ensureHubRunning(options = {}) {
2129
- const host = options.host ?? config.hub.host;
2130
- const port = options.port ?? config.hub.port;
2131
- const maxRetries = options.maxRetries ?? config.autoStart.maxRetries;
2132
- const retryDelay = options.retryDelay ?? config.autoStart.retryDelay;
2133
- if (await isHubRunning(host, port)) {
2134
- return true;
2135
- }
2136
- console.log(`Hub not running. Starting hub on ${host}:${port}...`);
2137
- startHubProcess({ host, port });
2138
- const isRunning = await waitForHub(host, port, maxRetries, retryDelay);
2139
- if (isRunning) {
2140
- console.log("Hub started successfully");
2141
- } else {
2142
- console.error("Failed to start hub");
2143
- }
2144
- return isRunning;
2145
1086
  }
2146
1087
 
2147
1088
  // src/cli.ts
2148
1089
  var program = new Command();
2149
- program.name("claude-collab").description("Real-time team collaboration between Claude Code terminals").version("0.1.0");
2150
- var hubCmd = program.command("hub").description("Hub server commands");
2151
- hubCmd.command("start").description("Start the Hub server").option("-p, --port <port>", "Port to listen on", String(config.hub.port)).option("--host <host>", "Host to bind to", config.hub.host).action(async (options) => {
2152
- const port = parseInt(options.port, 10);
2153
- const host = options.host;
2154
- const server = new HubServer({ host, port });
2155
- const shutdown = async () => {
2156
- console.log("\nShutting down hub server...");
2157
- await server.stop();
2158
- process.exit(0);
2159
- };
2160
- process.on("SIGINT", shutdown);
2161
- process.on("SIGTERM", shutdown);
1090
+ program.name("claude-collab").description("Real-time P2P team collaboration between Claude Code terminals").version("0.1.0");
1091
+ program.command("client").description("Start MCP client (P2P mode, connects to Claude Code)").option("-t, --team <team>", "Team to auto-join (e.g., frontend, backend)").action(async (options) => {
1092
+ const p2pNode = new P2PNode();
2162
1093
  try {
2163
- await server.start();
2164
- console.log(`Claude Collab Hub Server running on ${host}:${port}`);
1094
+ await p2pNode.start();
1095
+ if (options.team) {
1096
+ await p2pNode.join(options.team, `${options.team} Claude`);
1097
+ console.error(`Auto-joined team: ${options.team}`);
1098
+ }
2165
1099
  } catch (error) {
2166
- console.error("Failed to start hub server:", error);
1100
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1101
+ console.error(`Failed to start P2P node: ${errorMessage}`);
2167
1102
  process.exit(1);
2168
1103
  }
1104
+ await startMcpServer({ client: p2pNode });
2169
1105
  });
2170
- program.command("client").description("Start MCP client (connects to Claude Code)").option("-t, --team <team>", "Team to auto-join (e.g., frontend, backend)").option("--auto-hub", "Auto-start hub if not running", false).option("-p, --port <port>", "Hub port to connect to", String(config.hub.port)).option("--host <host>", "Hub host to connect to", config.hub.host).action(
2171
- async (options) => {
2172
- const port = parseInt(options.port, 10);
2173
- const host = options.host;
2174
- if (options.autoHub) {
2175
- const hubRunning = await ensureHubRunning({ host, port });
2176
- if (!hubRunning) {
2177
- console.error("Failed to start hub server. Exiting.");
2178
- process.exit(1);
2179
- }
2180
- }
2181
- const hubClient = new HubClient(
2182
- { host, port, reconnect: true },
2183
- {
2184
- onError: (error) => {
2185
- console.error("Hub client error:", error.message);
2186
- },
2187
- onQuestion: (question) => {
2188
- console.error(`[Question received from ${question.from.displayName}]`);
2189
- }
2190
- }
2191
- );
2192
- try {
2193
- await hubClient.connect();
2194
- if (options.team) {
2195
- await hubClient.join(options.team, `${options.team} Claude`);
2196
- console.error(`Auto-joined team: ${options.team}`);
2197
- }
2198
- } catch (error) {
2199
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
2200
- console.error(`Failed to connect to hub: ${errorMessage}`);
2201
- console.error("Make sure the hub server is running or use --auto-hub flag.");
2202
- process.exit(1);
2203
- }
2204
- await startMcpServer({ hubClient });
2205
- }
2206
- );
2207
1106
  program.parse();
2208
1107
  //# sourceMappingURL=cli.js.map
2209
1108
  //# sourceMappingURL=cli.js.map