@dolusoft/claude-collab 0.1.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,1822 +1,773 @@
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 { EventEmitter } from 'events';
8
+ import { execFile } from 'child_process';
9
+ import { unlinkSync } from 'fs';
10
+ import { tmpdir } from 'os';
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 */;
94
- }
95
- }
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;
16
+ var SERVICE_TYPE = "_claude-collab._tcp.local";
17
+ var MdnsDiscovery = class {
18
+ mdns;
19
+ announced = false;
20
+ port = 0;
21
+ teamName = "";
22
+ memberId = "";
23
+ peersByTeam = /* @__PURE__ */ new Map();
24
+ peersByMemberId = /* @__PURE__ */ new Map();
25
+ onPeerFoundCb;
26
+ onPeerLostCb;
113
27
  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());
172
- }
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");
28
+ this.mdns = multicastDns();
29
+ this.setupHandlers();
198
30
  }
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;
220
- }
221
- };
222
-
223
- // src/application/use-cases/join-team.use-case.ts
224
- var JoinTeamUseCase = class {
225
- constructor(deps) {
226
- this.deps = deps;
31
+ get serviceName() {
32
+ return `${this.memberId}.${SERVICE_TYPE}`;
227
33
  }
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 */
34
+ buildAnswers() {
35
+ return [
36
+ {
37
+ name: SERVICE_TYPE,
38
+ type: "PTR",
39
+ ttl: 300,
40
+ data: this.serviceName
41
+ },
42
+ {
43
+ name: this.serviceName,
44
+ type: "SRV",
45
+ ttl: 300,
46
+ data: {
47
+ priority: 0,
48
+ weight: 0,
49
+ port: this.port,
50
+ target: "localhost"
51
+ }
52
+ },
53
+ {
54
+ name: this.serviceName,
55
+ type: "TXT",
56
+ ttl: 300,
57
+ data: [
58
+ Buffer.from(`team=${this.teamName}`),
59
+ Buffer.from(`memberId=${this.memberId}`),
60
+ Buffer.from("ver=1")
61
+ ]
62
+ }
63
+ ];
64
+ }
65
+ setupHandlers() {
66
+ this.mdns.on("query", (query) => {
67
+ if (!this.announced) return;
68
+ const questions = query.questions ?? [];
69
+ const ptrQuery = questions.find(
70
+ (q) => q.type === "PTR" && q.name === SERVICE_TYPE
71
+ );
72
+ if (!ptrQuery) return;
73
+ this.mdns.respond({ answers: this.buildAnswers() });
246
74
  });
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 */
75
+ this.mdns.on("response", (response) => {
76
+ this.parseResponse(response);
292
77
  });
293
78
  }
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`
79
+ parseResponse(response) {
80
+ const allRecords = [
81
+ ...response.answers ?? [],
82
+ ...response.additionals ?? []
83
+ ];
84
+ const ptrRecords = allRecords.filter(
85
+ (r) => r.type === "PTR" && r.name === SERVICE_TYPE
86
+ );
87
+ for (const ptr of ptrRecords) {
88
+ const instanceName = ptr.data;
89
+ const srv = allRecords.find(
90
+ (r) => r.type === "SRV" && r.name === instanceName
464
91
  );
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
92
+ const txt = allRecords.find(
93
+ (r) => r.type === "TXT" && r.name === instanceName
596
94
  );
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
- });
95
+ if (!srv) continue;
96
+ const port = srv.data.port;
97
+ const host = "localhost";
98
+ let teamName = "";
99
+ let memberId = "";
100
+ if (txt) {
101
+ const txtData = txt.data ?? [];
102
+ for (const entry of txtData) {
103
+ const str = Buffer.isBuffer(entry) ? entry.toString() : String(entry);
104
+ if (str.startsWith("team=")) teamName = str.slice(5);
105
+ if (str.startsWith("memberId=")) memberId = str.slice(9);
106
+ }
107
+ }
108
+ if (!teamName || !memberId) continue;
109
+ if (memberId === this.memberId) continue;
110
+ const ptrTtl = ptr.ttl ?? 300;
111
+ const srvTtl = srv.ttl ?? 300;
112
+ if (ptrTtl === 0 || srvTtl === 0) {
113
+ this.peersByTeam.delete(teamName);
114
+ this.peersByMemberId.delete(memberId);
115
+ this.onPeerLostCb?.(memberId);
116
+ continue;
117
+ }
118
+ const peer = { host, port, teamName, memberId };
119
+ this.peersByTeam.set(teamName, peer);
120
+ this.peersByMemberId.set(memberId, peer);
121
+ this.onPeerFoundCb?.(peer);
642
122
  }
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
123
  }
669
124
  /**
670
- * Creates a new Answer instance
125
+ * Announce this node's service via mDNS.
126
+ * Sends an unsolicited response so existing peers notice immediately.
671
127
  */
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;
128
+ announce(port, teamName, memberId) {
129
+ this.port = port;
130
+ this.teamName = teamName;
131
+ this.memberId = memberId;
132
+ this.announced = true;
133
+ this.mdns.respond({ answers: this.buildAnswers() });
738
134
  }
739
135
  /**
740
- * Executes the use case
136
+ * Send a PTR query to discover existing peers.
741
137
  */
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()
138
+ discover() {
139
+ this.mdns.query({
140
+ questions: [{ name: SERVICE_TYPE, type: "PTR" }]
762
141
  });
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
142
  }
795
- async findByTeamId(teamId) {
796
- return [...this.members.values()].filter((m) => m.teamId === teamId);
143
+ getPeerByTeam(teamName) {
144
+ return this.peersByTeam.get(teamName);
797
145
  }
798
- async findOnlineByTeamId(teamId) {
799
- return [...this.members.values()].filter((m) => m.teamId === teamId && m.isOnline);
146
+ onPeerFound(cb) {
147
+ this.onPeerFoundCb = cb;
800
148
  }
801
- async delete(id) {
802
- return this.members.delete(id);
149
+ onPeerLost(cb) {
150
+ this.onPeerLostCb = cb;
803
151
  }
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;
152
+ destroy() {
153
+ this.mdns.destroy();
821
154
  }
822
155
  };
823
156
 
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
- };
157
+ // src/infrastructure/p2p/p2p-message-protocol.ts
158
+ function serializeP2PMsg(msg) {
159
+ return JSON.stringify(msg);
160
+ }
161
+ function parseP2PMsg(data) {
162
+ return JSON.parse(data);
163
+ }
164
+ var CS_CONINJECT = `
165
+ using System;
166
+ using System.Collections.Generic;
167
+ using System.Runtime.InteropServices;
918
168
 
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
- };
169
+ public class ConInject {
170
+ [DllImport("kernel32.dll")] public static extern bool FreeConsole();
171
+ [DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
172
+ [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
173
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hwnd);
174
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
175
+ public static extern IntPtr CreateFile(
176
+ string lpFileName, uint dwDesiredAccess, uint dwShareMode,
177
+ IntPtr lpSecurityAttributes, uint dwCreationDisposition,
178
+ uint dwFlagsAndAttributes, IntPtr hTemplateFile);
179
+ [DllImport("kernel32.dll")] public static extern bool WriteConsoleInput(
180
+ IntPtr hIn, INPUT_RECORD[] buf, uint len, out uint written);
181
+ [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
971
182
 
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
- }
183
+ [StructLayout(LayoutKind.Explicit, Size=20)]
184
+ public struct INPUT_RECORD {
185
+ [FieldOffset(0)] public ushort EventType;
186
+ [FieldOffset(4)] public int bKeyDown;
187
+ [FieldOffset(8)] public ushort wRepeatCount;
188
+ [FieldOffset(10)] public ushort wVirtualKeyCode;
189
+ [FieldOffset(12)] public ushort wVirtualScanCode;
190
+ [FieldOffset(14)] public ushort UnicodeChar;
191
+ [FieldOffset(16)] public uint dwControlKeyState;
1009
192
  }
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
193
 
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
- }
194
+ const uint LEFT_CTRL = 0x0008;
195
+
196
+ static IntPtr OpenConin(uint pid) {
197
+ FreeConsole();
198
+ if (!AttachConsole(pid)) return new IntPtr(-1);
199
+ return CreateFile("CONIN$", 0xC0000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
1040
200
  }
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
201
 
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
- }
202
+ // Inject only text characters into console input buffer (no Ctrl keys, no Enter)
203
+ public static int InjectText(uint pid, string text) {
204
+ IntPtr hIn = OpenConin(pid);
205
+ if (hIn == new IntPtr(-1)) return -1;
1089
206
 
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();
207
+ var records = new List<INPUT_RECORD>();
208
+ foreach (char c in text) {
209
+ records.Add(new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, UnicodeChar=(ushort)c });
210
+ records.Add(new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, UnicodeChar=(ushort)c });
1203
211
  }
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));
212
+
213
+ var arr = records.ToArray();
214
+ uint written;
215
+ bool ok = WriteConsoleInput(hIn, arr, (uint)arr.Length, out written);
216
+ CloseHandle(hIn);
217
+ return ok ? (int)written : -2;
1524
218
  }
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();
219
+
220
+ // Focus the console window for WScript.Shell.SendKeys
221
+ public static IntPtr FocusConsole(uint pid) {
222
+ FreeConsole();
223
+ if (!AttachConsole(pid)) return IntPtr.Zero;
224
+ IntPtr hwnd = GetConsoleWindow();
225
+ if (hwnd != IntPtr.Zero) SetForegroundWindow(hwnd);
226
+ return hwnd;
227
+ }
228
+ }
229
+ `;
230
+ function buildScript(claudePid, body) {
231
+ const logFile = join(tmpdir(), `cc-inject-${Date.now()}.log`).replace(/\\/g, "/");
232
+ return `
233
+ $log = "${logFile}"
234
+ function Log($msg) { Add-Content -Path $log -Value $msg -Encoding UTF8 }
235
+ $claudePid = ${claudePid}
236
+ try { Add-Type @'${CS_CONINJECT}'@ } catch { }
237
+ ${body}
238
+ `;
239
+ }
240
+ function run(script) {
241
+ return new Promise((resolve) => {
242
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
243
+ execFile(
244
+ "powershell",
245
+ ["-NoProfile", "-WindowStyle", "Hidden", "-EncodedCommand", encoded],
246
+ { windowsHide: true },
247
+ () => {
248
+ const logFile = script.match(/\$log = "([^"]+)"/)?.[1];
249
+ if (logFile) try {
250
+ unlinkSync(logFile);
251
+ } catch {
1533
252
  }
253
+ resolve();
1534
254
  }
1535
- }, config.hub.heartbeatInterval);
1536
- }
1537
- startTimeoutCheck() {
1538
- this.timeoutCheckInterval = setInterval(async () => {
1539
- await this.questionRepository.markTimedOut(config.communication.defaultTimeout);
1540
- }, 5e3);
1541
- }
255
+ );
256
+ });
257
+ }
258
+ async function windowsInject(text) {
259
+ const claudePid = process.ppid;
260
+ const textB64 = Buffer.from(text, "utf16le").toString("base64");
261
+ const script = buildScript(claudePid, `
262
+ $textBytes = [System.Convert]::FromBase64String('${textB64}')
263
+ $text = [System.Text.Encoding]::Unicode.GetString($textBytes)
264
+ $wsh = New-Object -ComObject WScript.Shell
265
+
266
+ # 1. Focus console and send Ctrl+U to save user's current text to kill ring
267
+ $hwnd = [ConInject]::FocusConsole([uint32]$claudePid)
268
+ Start-Sleep -Milliseconds 150
269
+ $wsh.SendKeys('^u')
270
+ Start-Sleep -Milliseconds 150
271
+
272
+ # 2. Write question text into console input buffer
273
+ [ConInject]::InjectText([uint32]$claudePid, $text) | Out-Null
274
+
275
+ # 3. Re-focus (InjectText calls FreeConsole internally, focus may be lost)
276
+ [ConInject]::FocusConsole([uint32]$claudePid) | Out-Null
277
+ Start-Sleep -Milliseconds 150
278
+
279
+ # 4. Send Enter
280
+ $wsh.SendKeys('~')
281
+ `);
282
+ await run(script);
283
+ }
284
+ async function windowsInjectCtrlY() {
285
+ const claudePid = process.ppid;
286
+ const script = buildScript(claudePid, `
287
+ $wsh = New-Object -ComObject WScript.Shell
288
+ $hwnd = [ConInject]::FocusConsole([uint32]$claudePid)
289
+ Start-Sleep -Milliseconds 150
290
+ $wsh.SendKeys('^y')
291
+ `);
292
+ await run(script);
293
+ }
294
+
295
+ // src/infrastructure/terminal-injector/index.ts
296
+ function formatQuestionForInjection(question) {
297
+ const content = question.content.replace(/\s+/g, " ").trim();
298
+ 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.`;
299
+ }
300
+ async function injectQuestionToTerminal(question) {
301
+ if (process.platform !== "win32") return;
302
+ const text = formatQuestionForInjection(question);
303
+ await windowsInject(text);
304
+ }
305
+ async function injectRestoreText() {
306
+ if (process.platform !== "win32") return;
307
+ await windowsInjectCtrlY();
308
+ }
309
+
310
+ // src/infrastructure/terminal-injector/injection-queue.ts
311
+ var REPLY_TIMEOUT_MS = 5 * 60 * 1e3;
312
+ var InjectionQueue = class extends EventEmitter {
313
+ queue = [];
314
+ processing = false;
1542
315
  /**
1543
- * Gets the number of connected clients
316
+ * Add a question to the queue. Starts processing if idle.
1544
317
  */
1545
- get clientCount() {
1546
- return this.clients.size;
318
+ enqueue(question) {
319
+ this.queue.push(question);
320
+ if (!this.processing) void this.processNext();
1547
321
  }
1548
322
  /**
1549
- * Checks if the server is running
323
+ * Called by the reply tool after a reply is successfully sent.
324
+ * Unblocks the queue to process the next question.
1550
325
  */
1551
- get isRunning() {
1552
- return this.wss !== null;
326
+ notifyReplied() {
327
+ this.emit("replied");
1553
328
  }
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
- }
329
+ async processNext() {
330
+ if (this.queue.length === 0) {
331
+ this.processing = false;
332
+ return;
333
+ }
334
+ this.processing = true;
335
+ const question = this.queue.shift();
336
+ await injectQuestionToTerminal(question);
337
+ await new Promise((resolve) => {
338
+ const timer = setTimeout(resolve, REPLY_TIMEOUT_MS);
339
+ this.once("replied", () => {
340
+ clearTimeout(timer);
341
+ resolve();
342
+ });
1599
343
  });
344
+ await injectRestoreText();
345
+ void this.processNext();
1600
346
  }
347
+ };
348
+ var injectionQueue = new InjectionQueue();
349
+
350
+ // src/config/index.ts
351
+ var config = {
1601
352
  /**
1602
- * Disconnects from the Hub server
353
+ * P2P node configuration
1603
354
  */
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();
355
+ p2p: {
356
+ /**
357
+ * Minimum port for the random WS server port range
358
+ */
359
+ portRangeMin: 1e4,
360
+ /**
361
+ * Maximum port for the random WS server port range
362
+ */
363
+ portRangeMax: 19999
364
+ }};
365
+
366
+ // src/infrastructure/p2p/p2p-node.ts
367
+ function getRandomPort(min, max) {
368
+ return new Promise((resolve) => {
369
+ const port = Math.floor(Math.random() * (max - min + 1)) + min;
370
+ const server = createServer();
371
+ server.listen(port, () => {
372
+ server.close(() => resolve(port));
373
+ });
374
+ server.on("error", () => {
375
+ resolve(getRandomPort(min, max));
1616
376
  });
377
+ });
378
+ }
379
+ var P2PNode = class {
380
+ wss = null;
381
+ mdnsDiscovery = null;
382
+ port = 0;
383
+ // Connections indexed by remote team name
384
+ peerConns = /* @__PURE__ */ new Map();
385
+ // Reverse lookup: ws → teamName (for cleanup on incoming connections)
386
+ wsToTeam = /* @__PURE__ */ new Map();
387
+ // Questions we received from remote peers (our inbox)
388
+ incomingQuestions = /* @__PURE__ */ new Map();
389
+ // Answers we received for questions we asked
390
+ receivedAnswers = /* @__PURE__ */ new Map();
391
+ // Maps questionId → remote teamName (so we know who to poll)
392
+ questionToTeam = /* @__PURE__ */ new Map();
393
+ // Pending response handlers (request-response correlation by filter)
394
+ pendingHandlers = /* @__PURE__ */ new Set();
395
+ localMember = null;
396
+ _isStarted = false;
397
+ get isConnected() {
398
+ return this._isStarted;
399
+ }
400
+ get currentTeamId() {
401
+ return this.localMember?.teamName;
1617
402
  }
1618
403
  /**
1619
- * Joins a team
404
+ * Starts the WS server on a random port and initialises mDNS.
405
+ * Called automatically from join() if not yet started.
1620
406
  */
407
+ async start() {
408
+ this.port = await getRandomPort(config.p2p.portRangeMin, config.p2p.portRangeMax);
409
+ this.wss = new WebSocketServer({ port: this.port });
410
+ this.setupWssHandlers();
411
+ this.mdnsDiscovery = new MdnsDiscovery();
412
+ this._isStarted = true;
413
+ console.error(`P2P node started on port ${this.port}`);
414
+ }
1621
415
  async join(teamName, displayName) {
1622
- v4();
1623
- this.send({
1624
- type: "JOIN",
1625
- teamName,
1626
- displayName
416
+ if (!this._isStarted) {
417
+ await this.start();
418
+ }
419
+ const memberId = v4();
420
+ this.localMember = { memberId, teamName, displayName };
421
+ this.mdnsDiscovery.onPeerFound((peer) => {
422
+ if (peer.teamName !== teamName) {
423
+ console.error(`Discovered peer '${peer.teamName}' at ${peer.host}:${peer.port}`);
424
+ this.connectToPeer(peer.teamName, peer.host, peer.port).catch((err) => {
425
+ console.error(`Could not eagerly connect to ${peer.teamName}:`, err);
426
+ });
427
+ }
1627
428
  });
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;
429
+ this.mdnsDiscovery.announce(this.port, teamName, memberId);
430
+ this.mdnsDiscovery.discover();
431
+ return { memberId, teamId: teamName, teamName, displayName, status: "ONLINE" };
1637
432
  }
1638
- /**
1639
- * Asks a question to another team
1640
- */
1641
- async ask(toTeam, content, format = "markdown", timeoutMs = config.communication.defaultTimeout) {
433
+ async ask(toTeam, content, format) {
434
+ const ws = await this.getPeerConnection(toTeam);
435
+ const questionId = v4();
1642
436
  const requestId = v4();
1643
- this.send({
1644
- type: "ASK",
437
+ this.questionToTeam.set(questionId, toTeam);
438
+ const ackPromise = this.waitForResponse(
439
+ (m) => m.type === "P2P_ASK_ACK" && m.requestId === requestId,
440
+ 5e3
441
+ );
442
+ const msg = {
443
+ type: "P2P_ASK",
444
+ questionId,
445
+ fromMemberId: this.localMember.memberId,
446
+ fromTeam: this.localMember.teamName,
1645
447
  toTeam,
1646
448
  content,
1647
449
  format,
1648
450
  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;
451
+ };
452
+ ws.send(serializeP2PMsg(msg));
453
+ await ackPromise;
454
+ return questionId;
1660
455
  }
1661
- /**
1662
- * Gets the inbox (pending questions)
1663
- */
1664
- async getInbox() {
456
+ async checkAnswer(questionId) {
457
+ const cached = this.receivedAnswers.get(questionId);
458
+ if (cached) {
459
+ return {
460
+ questionId,
461
+ from: { displayName: `${cached.fromTeam} Claude`, teamName: cached.fromTeam },
462
+ content: cached.content,
463
+ format: cached.format,
464
+ answeredAt: cached.answeredAt
465
+ };
466
+ }
467
+ const toTeam = this.questionToTeam.get(questionId);
468
+ if (!toTeam) return null;
469
+ const ws = this.peerConns.get(toTeam);
470
+ if (!ws || ws.readyState !== WebSocket.OPEN) return null;
1665
471
  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,
472
+ const responsePromise = this.waitForResponse(
473
+ (m) => m.type === "P2P_ANSWER" && m.questionId === questionId || m.type === "P2P_ANSWER_PENDING" && m.requestId === requestId,
1672
474
  5e3
1673
475
  );
476
+ const getMsg = {
477
+ type: "P2P_GET_ANSWER",
478
+ questionId,
479
+ requestId
480
+ };
481
+ ws.send(serializeP2PMsg(getMsg));
482
+ const response = await responsePromise;
483
+ if (response.type === "P2P_ANSWER_PENDING") return null;
484
+ const answer = response;
485
+ this.receivedAnswers.set(questionId, {
486
+ content: answer.content,
487
+ format: answer.format,
488
+ answeredAt: answer.answeredAt,
489
+ fromTeam: answer.fromTeam,
490
+ fromMemberId: answer.fromMemberId
491
+ });
492
+ return {
493
+ questionId,
494
+ from: { displayName: `${answer.fromTeam} Claude`, teamName: answer.fromTeam },
495
+ content: answer.content,
496
+ format: answer.format,
497
+ answeredAt: answer.answeredAt
498
+ };
1674
499
  }
1675
- /**
1676
- * Replies to a question
1677
- */
1678
- async reply(questionId, content, format = "markdown") {
1679
- this.send({
1680
- type: "REPLY",
500
+ async reply(questionId, content, format) {
501
+ const question = this.incomingQuestions.get(questionId);
502
+ if (!question) throw new Error(`Question ${questionId} not found in inbox`);
503
+ question.answered = true;
504
+ question.answerContent = content;
505
+ question.answerFormat = format;
506
+ const answerMsg = {
507
+ type: "P2P_ANSWER",
1681
508
  questionId,
1682
509
  content,
1683
- format
1684
- });
510
+ format,
511
+ answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
512
+ fromTeam: this.localMember.teamName,
513
+ fromMemberId: this.localMember.memberId
514
+ };
515
+ if (question.ws.readyState === WebSocket.OPEN) {
516
+ question.ws.send(serializeP2PMsg(answerMsg));
517
+ }
1685
518
  }
1686
- /**
1687
- * Checks if connected
1688
- */
1689
- get isConnected() {
1690
- return this.ws !== null && this.ws.readyState === WebSocket2.OPEN;
519
+ async getInbox() {
520
+ const now = Date.now();
521
+ const questions = [...this.incomingQuestions.values()].filter((q) => !q.answered).map((q) => ({
522
+ questionId: q.questionId,
523
+ from: { displayName: `${q.fromTeam} Claude`, teamName: q.fromTeam },
524
+ content: q.content,
525
+ format: q.format,
526
+ status: "PENDING",
527
+ createdAt: q.createdAt.toISOString(),
528
+ ageMs: now - q.createdAt.getTime()
529
+ }));
530
+ return {
531
+ questions,
532
+ totalCount: questions.length,
533
+ pendingCount: questions.length
534
+ };
1691
535
  }
1692
- /**
1693
- * Gets the current member ID
1694
- */
1695
- get currentMemberId() {
1696
- return this.memberId;
536
+ async disconnect() {
537
+ this.mdnsDiscovery?.destroy();
538
+ for (const ws of this.peerConns.values()) {
539
+ ws.close();
540
+ }
541
+ this.peerConns.clear();
542
+ await new Promise((resolve) => {
543
+ if (this.wss) {
544
+ this.wss.close(() => resolve());
545
+ } else {
546
+ resolve();
547
+ }
548
+ });
549
+ this._isStarted = false;
550
+ }
551
+ // ---------------------------------------------------------------------------
552
+ // Private: WebSocket server setup
553
+ // ---------------------------------------------------------------------------
554
+ setupWssHandlers() {
555
+ this.wss.on("connection", (ws) => {
556
+ ws.on("message", (data) => {
557
+ try {
558
+ const msg = parseP2PMsg(data.toString());
559
+ this.handleMessage(ws, msg);
560
+ } catch (err) {
561
+ console.error("Failed to parse incoming P2P message:", err);
562
+ }
563
+ });
564
+ ws.on("close", () => {
565
+ const team = this.wsToTeam.get(ws);
566
+ if (team) {
567
+ if (this.peerConns.get(team) === ws) {
568
+ this.peerConns.delete(team);
569
+ }
570
+ this.wsToTeam.delete(ws);
571
+ }
572
+ });
573
+ });
1697
574
  }
1698
- /**
1699
- * Gets the current team ID
1700
- */
1701
- get currentTeamId() {
1702
- return this.teamId;
575
+ // ---------------------------------------------------------------------------
576
+ // Private: unified message handler (used for both incoming & outgoing sockets)
577
+ // ---------------------------------------------------------------------------
578
+ handleMessage(ws, msg) {
579
+ for (const handler of this.pendingHandlers) {
580
+ handler(msg);
581
+ }
582
+ switch (msg.type) {
583
+ case "P2P_HELLO":
584
+ this.wsToTeam.set(ws, msg.fromTeam);
585
+ this.peerConns.set(msg.fromTeam, ws);
586
+ console.error(`Peer identified: ${msg.fromTeam}`);
587
+ break;
588
+ case "P2P_ASK":
589
+ this.handleIncomingAsk(ws, msg);
590
+ break;
591
+ case "P2P_GET_ANSWER":
592
+ this.handleGetAnswer(ws, msg);
593
+ break;
594
+ case "P2P_ANSWER":
595
+ if (!this.receivedAnswers.has(msg.questionId)) {
596
+ this.receivedAnswers.set(msg.questionId, {
597
+ content: msg.content,
598
+ format: msg.format,
599
+ answeredAt: msg.answeredAt,
600
+ fromTeam: msg.fromTeam,
601
+ fromMemberId: msg.fromMemberId
602
+ });
603
+ }
604
+ break;
605
+ case "P2P_PING":
606
+ ws.send(serializeP2PMsg({ type: "P2P_PONG" }));
607
+ break;
608
+ }
609
+ }
610
+ handleIncomingAsk(ws, msg) {
611
+ this.incomingQuestions.set(msg.questionId, {
612
+ questionId: msg.questionId,
613
+ fromTeam: msg.fromTeam,
614
+ fromMemberId: msg.fromMemberId,
615
+ content: msg.content,
616
+ format: msg.format,
617
+ createdAt: /* @__PURE__ */ new Date(),
618
+ ws,
619
+ answered: false
620
+ });
621
+ injectionQueue.enqueue({
622
+ questionId: msg.questionId,
623
+ from: {
624
+ displayName: `${msg.fromTeam} Claude`,
625
+ teamName: msg.fromTeam
626
+ },
627
+ content: msg.content,
628
+ format: msg.format,
629
+ status: "PENDING",
630
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
631
+ ageMs: 0
632
+ });
633
+ const ack = {
634
+ type: "P2P_ASK_ACK",
635
+ questionId: msg.questionId,
636
+ requestId: msg.requestId
637
+ };
638
+ ws.send(serializeP2PMsg(ack));
1703
639
  }
1704
- send(message) {
1705
- if (this.ws && this.ws.readyState === WebSocket2.OPEN) {
1706
- this.ws.send(serializeMessage(message));
640
+ handleGetAnswer(ws, msg) {
641
+ const question = this.incomingQuestions.get(msg.questionId);
642
+ if (!question?.answered) {
643
+ const pending = {
644
+ type: "P2P_ANSWER_PENDING",
645
+ questionId: msg.questionId,
646
+ requestId: msg.requestId
647
+ };
648
+ ws.send(serializeP2PMsg(pending));
649
+ return;
650
+ }
651
+ const answer = {
652
+ type: "P2P_ANSWER",
653
+ questionId: msg.questionId,
654
+ content: question.answerContent,
655
+ format: question.answerFormat,
656
+ answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
657
+ fromTeam: this.localMember.teamName,
658
+ fromMemberId: this.localMember.memberId,
659
+ requestId: msg.requestId
660
+ };
661
+ ws.send(serializeP2PMsg(answer));
662
+ }
663
+ // ---------------------------------------------------------------------------
664
+ // Private: peer connection management
665
+ // ---------------------------------------------------------------------------
666
+ async getPeerConnection(teamName) {
667
+ const existing = this.peerConns.get(teamName);
668
+ if (existing && existing.readyState === WebSocket.OPEN) {
669
+ return existing;
670
+ }
671
+ let peer = this.mdnsDiscovery?.getPeerByTeam(teamName);
672
+ if (!peer) {
673
+ this.mdnsDiscovery?.discover();
674
+ await this.waitForMdnsPeer(teamName, 1e4);
675
+ peer = this.mdnsDiscovery?.getPeerByTeam(teamName);
676
+ }
677
+ if (!peer) {
678
+ throw new Error(
679
+ `Peer for team '${teamName}' not found via mDNS. Make sure the other terminal has joined with that team name.`
680
+ );
1707
681
  }
682
+ return this.connectToPeer(teamName, peer.host, peer.port);
1708
683
  }
1709
- handleMessage(data) {
1710
- try {
1711
- const message = parseHubMessage(data);
1712
- for (const [requestId, pending] of this.pendingRequests) {
684
+ async connectToPeer(teamName, host, port) {
685
+ const existing = this.peerConns.get(teamName);
686
+ if (existing && existing.readyState === WebSocket.OPEN) {
687
+ return existing;
688
+ }
689
+ const ws = new WebSocket(`ws://${host}:${port}`);
690
+ await new Promise((resolve, reject) => {
691
+ const timeout = setTimeout(
692
+ () => reject(new Error(`Connection timeout to team '${teamName}'`)),
693
+ 5e3
694
+ );
695
+ ws.on("open", () => {
696
+ clearTimeout(timeout);
697
+ const hello = {
698
+ type: "P2P_HELLO",
699
+ fromTeam: this.localMember?.teamName ?? "unknown",
700
+ fromMemberId: this.localMember?.memberId ?? "unknown"
701
+ };
702
+ ws.send(serializeP2PMsg(hello));
703
+ resolve();
704
+ });
705
+ ws.on("error", (err) => {
706
+ clearTimeout(timeout);
707
+ reject(err);
708
+ });
709
+ });
710
+ ws.on("message", (data) => {
711
+ try {
712
+ const msg = parseP2PMsg(data.toString());
713
+ this.handleMessage(ws, msg);
714
+ } catch (err) {
715
+ console.error("Failed to parse P2P message:", err);
1713
716
  }
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;
717
+ });
718
+ ws.on("close", () => {
719
+ if (this.peerConns.get(teamName) === ws) {
720
+ this.peerConns.delete(teamName);
1730
721
  }
1731
- this.resolvePendingRequest(message);
1732
- } catch (error) {
1733
- console.error("Failed to parse message:", error);
1734
- }
722
+ });
723
+ this.peerConns.set(teamName, ws);
724
+ return ws;
1735
725
  }
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
- }
726
+ waitForMdnsPeer(teamName, timeoutMs) {
727
+ return new Promise((resolve, reject) => {
728
+ const deadline = Date.now() + timeoutMs;
729
+ const check = () => {
730
+ if (this.mdnsDiscovery?.getPeerByTeam(teamName)) {
731
+ resolve();
732
+ return;
733
+ }
734
+ if (Date.now() >= deadline) {
735
+ reject(new Error(`mDNS timeout: team '${teamName}' not found`));
736
+ return;
737
+ }
738
+ this.mdnsDiscovery?.discover();
739
+ setTimeout(check, 500);
740
+ };
741
+ check();
742
+ });
1743
743
  }
1744
744
  waitForResponse(filter, timeoutMs) {
1745
745
  return new Promise((resolve, reject) => {
1746
- const requestId = v4();
1747
746
  const timeout = setTimeout(() => {
1748
- this.pendingRequests.delete(requestId);
1749
- reject(new Error("Request timed out"));
747
+ this.pendingHandlers.delete(handler);
748
+ reject(new Error("P2P request timed out"));
1750
749
  }, 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 {
750
+ const handler = (msg) => {
751
+ if (filter(msg)) {
752
+ clearTimeout(timeout);
753
+ this.pendingHandlers.delete(handler);
754
+ resolve(msg);
1772
755
  }
1773
- originalHandler(data);
1774
756
  };
1775
- if (this.ws) {
1776
- this.ws.removeAllListeners("message");
1777
- this.ws.on("message", (data) => checkFilter(data.toString()));
1778
- }
757
+ this.pendingHandlers.add(handler);
1779
758
  });
1780
759
  }
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
760
  };
1807
761
  var joinSchema = {
1808
762
  team: z.string().describe('Team name to join (e.g., "frontend", "backend", "devops")'),
1809
763
  displayName: z.string().optional().describe('Display name for this terminal (default: team + " Claude")')
1810
764
  };
1811
- function registerJoinTool(server, hubClient) {
765
+ function registerJoinTool(server, client) {
1812
766
  server.tool("join", joinSchema, async (args) => {
1813
767
  const teamName = args.team;
1814
768
  const displayName = args.displayName ?? `${teamName} Claude`;
1815
769
  try {
1816
- if (!hubClient.isConnected) {
1817
- await hubClient.connect();
1818
- }
1819
- const member = await hubClient.join(teamName, displayName);
770
+ const member = await client.join(teamName, displayName);
1820
771
  return {
1821
772
  content: [
1822
773
  {
@@ -1845,16 +796,14 @@ Status: ${member.status}`
1845
796
  }
1846
797
  var askSchema = {
1847
798
  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)`)
799
+ question: z.string().describe("The question to ask (supports markdown)")
1850
800
  };
1851
- function registerAskTool(server, hubClient) {
801
+ function registerAskTool(server, client) {
1852
802
  server.tool("ask", askSchema, async (args) => {
1853
803
  const targetTeam = args.team;
1854
804
  const question = args.question;
1855
- const timeoutMs = (args.timeout ?? config.communication.defaultTimeout / 1e3) * 1e3;
1856
805
  try {
1857
- if (!hubClient.currentTeamId) {
806
+ if (!client.currentTeamId) {
1858
807
  return {
1859
808
  content: [
1860
809
  {
@@ -1865,25 +814,76 @@ function registerAskTool(server, hubClient) {
1865
814
  isError: true
1866
815
  };
1867
816
  }
1868
- const answer = await hubClient.ask(targetTeam, question, "markdown", timeoutMs);
817
+ const questionId = await client.ask(targetTeam, question, "markdown");
818
+ const POLL_INTERVAL_MS = 5e3;
819
+ const MAX_WAIT_MS = 5 * 60 * 1e3;
820
+ const deadline = Date.now() + MAX_WAIT_MS;
821
+ while (Date.now() < deadline) {
822
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
823
+ const answer = await client.checkAnswer(questionId);
824
+ if (answer !== null) {
825
+ return {
826
+ content: [
827
+ {
828
+ type: "text",
829
+ text: `**${answer.from.displayName} (${answer.from.teamName}) cevaplad\u0131:**
830
+
831
+ ${answer.content}`
832
+ }
833
+ ]
834
+ };
835
+ }
836
+ }
1869
837
  return {
1870
838
  content: [
1871
839
  {
1872
840
  type: "text",
1873
- text: `**Answer from ${answer.from.displayName} (${answer.from.teamName}):**
841
+ text: `Soru g\xF6nderildi ancak 5 dakika i\xE7inde cevap gelmedi.
842
+ Question ID: \`${questionId}\`
1874
843
 
1875
- ${answer.content}`
844
+ Manuel kontrol i\xE7in "check_answer" tool'unu kullanabilirsin.`
1876
845
  }
1877
846
  ]
1878
847
  };
1879
848
  } catch (error) {
1880
849
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1881
- if (errorMessage.includes("timed out")) {
850
+ return {
851
+ content: [
852
+ {
853
+ type: "text",
854
+ text: `Failed to send question: ${errorMessage}`
855
+ }
856
+ ],
857
+ isError: true
858
+ };
859
+ }
860
+ });
861
+ }
862
+ var checkAnswerSchema = {
863
+ question_id: z.string().describe('The question ID returned by the "ask" tool')
864
+ };
865
+ function registerCheckAnswerTool(server, client) {
866
+ server.tool("check_answer", checkAnswerSchema, async (args) => {
867
+ const questionId = args.question_id;
868
+ try {
869
+ if (!client.currentTeamId) {
870
+ return {
871
+ content: [
872
+ {
873
+ type: "text",
874
+ text: 'You must join a team first. Use the "join" tool to join a team.'
875
+ }
876
+ ],
877
+ isError: true
878
+ };
879
+ }
880
+ const answer = await client.checkAnswer(questionId);
881
+ if (!answer) {
1882
882
  return {
1883
883
  content: [
1884
884
  {
1885
885
  type: "text",
1886
- text: `No response received from team "${targetTeam}" within ${timeoutMs / 1e3} seconds. The question has been delivered but no one answered yet.`
886
+ text: `No answer yet for question \`${questionId}\`. The other team hasn't replied yet. You can continue working and check again later.`
1887
887
  }
1888
888
  ]
1889
889
  };
@@ -1892,7 +892,19 @@ ${answer.content}`
1892
892
  content: [
1893
893
  {
1894
894
  type: "text",
1895
- text: `Failed to ask question: ${errorMessage}`
895
+ text: `**Answer from ${answer.from.displayName} (${answer.from.teamName}):**
896
+
897
+ ${answer.content}`
898
+ }
899
+ ]
900
+ };
901
+ } catch (error) {
902
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
903
+ return {
904
+ content: [
905
+ {
906
+ type: "text",
907
+ text: `Failed to check answer: ${errorMessage}`
1896
908
  }
1897
909
  ],
1898
910
  isError: true
@@ -1903,10 +915,10 @@ ${answer.content}`
1903
915
 
1904
916
  // src/presentation/mcp/tools/inbox.tool.ts
1905
917
  var inboxSchema = {};
1906
- function registerInboxTool(server, hubClient) {
918
+ function registerInboxTool(server, client) {
1907
919
  server.tool("inbox", inboxSchema, async () => {
1908
920
  try {
1909
- if (!hubClient.currentTeamId) {
921
+ if (!client.currentTeamId) {
1910
922
  return {
1911
923
  content: [
1912
924
  {
@@ -1917,7 +929,7 @@ function registerInboxTool(server, hubClient) {
1917
929
  isError: true
1918
930
  };
1919
931
  }
1920
- const inbox = await hubClient.getInbox();
932
+ const inbox = await client.getInbox();
1921
933
  if (inbox.questions.length === 0) {
1922
934
  return {
1923
935
  content: [
@@ -1969,12 +981,12 @@ var replySchema = {
1969
981
  questionId: z.string().describe("The ID of the question to reply to (from inbox)"),
1970
982
  answer: z.string().describe("Your answer to the question (supports markdown)")
1971
983
  };
1972
- function registerReplyTool(server, hubClient) {
984
+ function registerReplyTool(server, client) {
1973
985
  server.tool("reply", replySchema, async (args) => {
1974
986
  const questionId = args.questionId;
1975
987
  const answer = args.answer;
1976
988
  try {
1977
- if (!hubClient.currentTeamId) {
989
+ if (!client.currentTeamId) {
1978
990
  return {
1979
991
  content: [
1980
992
  {
@@ -1985,7 +997,8 @@ function registerReplyTool(server, hubClient) {
1985
997
  isError: true
1986
998
  };
1987
999
  }
1988
- await hubClient.reply(questionId, answer, "markdown");
1000
+ await client.reply(questionId, answer, "markdown");
1001
+ injectionQueue.notifyReplied();
1989
1002
  return {
1990
1003
  content: [
1991
1004
  {
@@ -2011,7 +1024,7 @@ function registerReplyTool(server, hubClient) {
2011
1024
 
2012
1025
  // src/presentation/mcp/server.ts
2013
1026
  function createMcpServer(options) {
2014
- const { hubClient } = options;
1027
+ const { client } = options;
2015
1028
  const server = new McpServer(
2016
1029
  {
2017
1030
  name: "claude-collab",
@@ -2026,26 +1039,18 @@ function createMcpServer(options) {
2026
1039
  }
2027
1040
  }
2028
1041
  );
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") {
1042
+ registerJoinTool(server, client);
1043
+ registerAskTool(server, client);
1044
+ registerCheckAnswerTool(server, client);
1045
+ registerInboxTool(server, client);
1046
+ registerReplyTool(server, client);
1047
+ server.resource(
1048
+ "inbox-questions",
1049
+ "inbox://questions",
1050
+ { description: "Your inbox of pending questions from other teams", mimeType: "application/json" },
1051
+ async () => {
2047
1052
  try {
2048
- const inbox = await hubClient.getInbox();
1053
+ const inbox = await client.getInbox();
2049
1054
  return {
2050
1055
  contents: [
2051
1056
  {
@@ -2060,150 +1065,33 @@ function createMcpServer(options) {
2060
1065
  throw new Error(`Failed to read inbox: ${errorMessage}`);
2061
1066
  }
2062
1067
  }
2063
- throw new Error(`Unknown resource URI: ${request.params.uri}`);
2064
- });
1068
+ );
2065
1069
  return server;
2066
1070
  }
2067
1071
  async function startMcpServer(options) {
2068
- const { hubClient } = options;
2069
1072
  const server = createMcpServer(options);
2070
1073
  const transport = new StdioServerTransport();
2071
1074
  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
1075
  }
2146
1076
 
2147
1077
  // src/cli.ts
2148
1078
  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);
1079
+ program.name("claude-collab").description("Real-time P2P team collaboration between Claude Code terminals").version("0.1.0");
1080
+ 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) => {
1081
+ const p2pNode = new P2PNode();
2162
1082
  try {
2163
- await server.start();
2164
- console.log(`Claude Collab Hub Server running on ${host}:${port}`);
1083
+ await p2pNode.start();
1084
+ if (options.team) {
1085
+ await p2pNode.join(options.team, `${options.team} Claude`);
1086
+ console.error(`Auto-joined team: ${options.team}`);
1087
+ }
2165
1088
  } catch (error) {
2166
- console.error("Failed to start hub server:", error);
1089
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1090
+ console.error(`Failed to start P2P node: ${errorMessage}`);
2167
1091
  process.exit(1);
2168
1092
  }
1093
+ await startMcpServer({ client: p2pNode });
2169
1094
  });
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
1095
  program.parse();
2208
1096
  //# sourceMappingURL=cli.js.map
2209
1097
  //# sourceMappingURL=cli.js.map