@dolusoft/claude-collab 0.1.3 → 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/hub-main.js DELETED
@@ -1,1586 +0,0 @@
1
- #!/usr/bin/env node
2
- import { WebSocketServer, WebSocket } from 'ws';
3
- import { v4 } from 'uuid';
4
-
5
- // src/domain/entities/member.entity.ts
6
- var Member = class _Member {
7
- _id;
8
- _teamId;
9
- _displayName;
10
- _connectedAt;
11
- _status;
12
- _lastActivityAt;
13
- constructor(props) {
14
- this._id = props.id;
15
- this._teamId = props.teamId;
16
- this._displayName = props.displayName;
17
- this._connectedAt = props.connectedAt;
18
- this._status = props.status;
19
- this._lastActivityAt = props.connectedAt;
20
- }
21
- /**
22
- * Creates a new Member instance
23
- */
24
- static create(props) {
25
- if (!props.displayName.trim()) {
26
- throw new Error("Display name cannot be empty");
27
- }
28
- return new _Member(props);
29
- }
30
- /**
31
- * Reconstitutes a Member from persistence
32
- */
33
- static reconstitute(props) {
34
- const member = new _Member(props);
35
- member._lastActivityAt = props.lastActivityAt;
36
- return member;
37
- }
38
- // Getters
39
- get id() {
40
- return this._id;
41
- }
42
- get teamId() {
43
- return this._teamId;
44
- }
45
- get displayName() {
46
- return this._displayName;
47
- }
48
- get connectedAt() {
49
- return this._connectedAt;
50
- }
51
- get status() {
52
- return this._status;
53
- }
54
- get lastActivityAt() {
55
- return this._lastActivityAt;
56
- }
57
- get isOnline() {
58
- return this._status === "ONLINE" /* ONLINE */ || this._status === "IDLE" /* IDLE */;
59
- }
60
- // Behaviors
61
- /**
62
- * Marks the member as online
63
- */
64
- goOnline() {
65
- this._status = "ONLINE" /* ONLINE */;
66
- this._lastActivityAt = /* @__PURE__ */ new Date();
67
- }
68
- /**
69
- * Marks the member as idle
70
- */
71
- goIdle() {
72
- this._status = "IDLE" /* IDLE */;
73
- }
74
- /**
75
- * Marks the member as offline
76
- */
77
- goOffline() {
78
- this._status = "OFFLINE" /* OFFLINE */;
79
- }
80
- /**
81
- * Records activity from this member
82
- */
83
- recordActivity() {
84
- this._lastActivityAt = /* @__PURE__ */ new Date();
85
- if (this._status === "IDLE" /* IDLE */) {
86
- this._status = "ONLINE" /* ONLINE */;
87
- }
88
- }
89
- /**
90
- * Converts entity to plain object for serialization
91
- */
92
- toJSON() {
93
- return {
94
- id: this._id,
95
- teamId: this._teamId,
96
- displayName: this._displayName,
97
- connectedAt: this._connectedAt,
98
- status: this._status,
99
- lastActivityAt: this._lastActivityAt
100
- };
101
- }
102
- };
103
- var BaseDomainEvent = class {
104
- eventId;
105
- timestamp;
106
- constructor() {
107
- this.eventId = v4();
108
- this.timestamp = /* @__PURE__ */ new Date();
109
- }
110
- /**
111
- * Converts event to JSON
112
- */
113
- toJSON() {
114
- return {
115
- eventId: this.eventId,
116
- eventType: this.eventType,
117
- timestamp: this.timestamp,
118
- payload: this.payload
119
- };
120
- }
121
- };
122
-
123
- // src/domain/events/member-joined.event.ts
124
- var MemberJoinedEvent = class _MemberJoinedEvent extends BaseDomainEvent {
125
- constructor(memberId, teamId, displayName) {
126
- super();
127
- this.memberId = memberId;
128
- this.teamId = teamId;
129
- this.displayName = displayName;
130
- }
131
- static EVENT_TYPE = "MEMBER_JOINED";
132
- get eventType() {
133
- return _MemberJoinedEvent.EVENT_TYPE;
134
- }
135
- get payload() {
136
- return {
137
- memberId: this.memberId,
138
- teamId: this.teamId,
139
- displayName: this.displayName
140
- };
141
- }
142
- };
143
-
144
- // src/shared/types/branded-types.ts
145
- var MemberId = {
146
- create: (id) => id,
147
- isValid: (id) => id.length > 0
148
- };
149
- var TeamId = {
150
- create: (id) => id.toLowerCase().trim(),
151
- isValid: (id) => /^[a-z][a-z0-9-]*$/.test(id.toLowerCase().trim())
152
- };
153
- var QuestionId = {
154
- create: (id) => id,
155
- isValid: (id) => id.length > 0
156
- };
157
- var AnswerId = {
158
- create: (id) => id,
159
- isValid: (id) => id.length > 0
160
- };
161
-
162
- // src/shared/utils/id-generator.ts
163
- function generateMemberId() {
164
- return MemberId.create(v4());
165
- }
166
- function createTeamId(name) {
167
- return TeamId.create(name);
168
- }
169
- function generateQuestionId() {
170
- return QuestionId.create(`q_${v4()}`);
171
- }
172
- function generateAnswerId() {
173
- return AnswerId.create(`a_${v4()}`);
174
- }
175
-
176
- // src/shared/errors/domain-errors.ts
177
- var DomainError = class extends Error {
178
- code;
179
- timestamp;
180
- constructor(message, code) {
181
- super(message);
182
- this.name = this.constructor.name;
183
- this.code = code;
184
- this.timestamp = /* @__PURE__ */ new Date();
185
- Error.captureStackTrace(this, this.constructor);
186
- }
187
- };
188
- var TeamNotFoundError = class extends DomainError {
189
- constructor(teamId) {
190
- super(`Team '${teamId}' not found`, "TEAM_NOT_FOUND");
191
- }
192
- };
193
- var MemberNotFoundError = class extends DomainError {
194
- constructor(memberId) {
195
- super(`Member '${memberId}' not found`, "MEMBER_NOT_FOUND");
196
- }
197
- };
198
- var QuestionNotFoundError = class extends DomainError {
199
- constructor(questionId) {
200
- super(`Question '${questionId}' not found`, "QUESTION_NOT_FOUND");
201
- }
202
- };
203
- var QuestionAlreadyAnsweredError = class extends DomainError {
204
- constructor(questionId) {
205
- super(`Question '${questionId}' has already been answered`, "QUESTION_ALREADY_ANSWERED");
206
- }
207
- };
208
- var ValidationError = class extends DomainError {
209
- field;
210
- constructor(field, message) {
211
- super(message, "VALIDATION_ERROR");
212
- this.field = field;
213
- }
214
- };
215
-
216
- // src/application/use-cases/join-team.use-case.ts
217
- var JoinTeamUseCase = class {
218
- constructor(deps) {
219
- this.deps = deps;
220
- }
221
- /**
222
- * Executes the use case
223
- */
224
- async execute(input) {
225
- if (!input.teamName.trim()) {
226
- throw new ValidationError("teamName", "Team name cannot be empty");
227
- }
228
- if (!input.displayName.trim()) {
229
- throw new ValidationError("displayName", "Display name cannot be empty");
230
- }
231
- const team = await this.deps.teamRepository.getOrCreate(input.teamName);
232
- const memberId = generateMemberId();
233
- const member = Member.create({
234
- id: memberId,
235
- teamId: team.id,
236
- displayName: input.displayName.trim(),
237
- connectedAt: /* @__PURE__ */ new Date(),
238
- status: "ONLINE" /* ONLINE */
239
- });
240
- await this.deps.memberRepository.save(member);
241
- team.addMember(memberId);
242
- await this.deps.teamRepository.save(team);
243
- if (this.deps.onMemberJoined) {
244
- const event = new MemberJoinedEvent(memberId, team.id, member.displayName);
245
- await this.deps.onMemberJoined(event);
246
- }
247
- return {
248
- memberId,
249
- teamId: team.id,
250
- teamName: team.name,
251
- displayName: member.displayName,
252
- status: member.status,
253
- memberCount: team.memberCount
254
- };
255
- }
256
- };
257
-
258
- // src/domain/entities/question.entity.ts
259
- var Question = class _Question {
260
- _id;
261
- _fromMemberId;
262
- _toTeamId;
263
- _content;
264
- _createdAt;
265
- _status;
266
- _answeredAt;
267
- _answeredByMemberId;
268
- constructor(props) {
269
- this._id = props.id;
270
- this._fromMemberId = props.fromMemberId;
271
- this._toTeamId = props.toTeamId;
272
- this._content = props.content;
273
- this._createdAt = props.createdAt;
274
- this._status = props.status;
275
- this._answeredAt = props.answeredAt;
276
- this._answeredByMemberId = props.answeredByMemberId;
277
- }
278
- /**
279
- * Creates a new Question instance
280
- */
281
- static create(props) {
282
- return new _Question({
283
- ...props,
284
- status: "PENDING" /* PENDING */
285
- });
286
- }
287
- /**
288
- * Reconstitutes a Question from persistence
289
- */
290
- static reconstitute(props) {
291
- return new _Question(props);
292
- }
293
- // Getters
294
- get id() {
295
- return this._id;
296
- }
297
- get fromMemberId() {
298
- return this._fromMemberId;
299
- }
300
- get toTeamId() {
301
- return this._toTeamId;
302
- }
303
- get content() {
304
- return this._content;
305
- }
306
- get createdAt() {
307
- return this._createdAt;
308
- }
309
- get status() {
310
- return this._status;
311
- }
312
- get answeredAt() {
313
- return this._answeredAt;
314
- }
315
- get answeredByMemberId() {
316
- return this._answeredByMemberId;
317
- }
318
- get isPending() {
319
- return this._status === "PENDING" /* PENDING */;
320
- }
321
- get isAnswered() {
322
- return this._status === "ANSWERED" /* ANSWERED */;
323
- }
324
- get isTimedOut() {
325
- return this._status === "TIMEOUT" /* TIMEOUT */;
326
- }
327
- get isCancelled() {
328
- return this._status === "CANCELLED" /* CANCELLED */;
329
- }
330
- get canBeAnswered() {
331
- return this._status === "PENDING" /* PENDING */;
332
- }
333
- /**
334
- * Calculates the age of the question in milliseconds
335
- */
336
- get ageMs() {
337
- return Date.now() - this._createdAt.getTime();
338
- }
339
- // Behaviors
340
- /**
341
- * Marks the question as answered
342
- * @throws QuestionAlreadyAnsweredError if already answered
343
- */
344
- markAsAnswered(answeredByMemberId) {
345
- if (!this.canBeAnswered) {
346
- throw new QuestionAlreadyAnsweredError(this._id);
347
- }
348
- this._status = "ANSWERED" /* ANSWERED */;
349
- this._answeredAt = /* @__PURE__ */ new Date();
350
- this._answeredByMemberId = answeredByMemberId;
351
- }
352
- /**
353
- * Marks the question as timed out
354
- */
355
- markAsTimedOut() {
356
- if (this._status === "PENDING" /* PENDING */) {
357
- this._status = "TIMEOUT" /* TIMEOUT */;
358
- }
359
- }
360
- /**
361
- * Marks the question as cancelled
362
- */
363
- markAsCancelled() {
364
- if (this._status === "PENDING" /* PENDING */) {
365
- this._status = "CANCELLED" /* CANCELLED */;
366
- }
367
- }
368
- /**
369
- * Converts entity to plain object for serialization
370
- */
371
- toJSON() {
372
- return {
373
- id: this._id,
374
- fromMemberId: this._fromMemberId,
375
- toTeamId: this._toTeamId,
376
- content: this._content,
377
- createdAt: this._createdAt,
378
- status: this._status,
379
- answeredAt: this._answeredAt,
380
- answeredByMemberId: this._answeredByMemberId
381
- };
382
- }
383
- };
384
-
385
- // src/config/index.ts
386
- var config = {
387
- /**
388
- * WebSocket Hub configuration
389
- */
390
- hub: {
391
- /**
392
- * Default port for the Hub server
393
- */
394
- port: parseInt(process.env["CLAUDE_COLLAB_PORT"] ?? "9999", 10),
395
- /**
396
- * Host to bind the Hub server to
397
- */
398
- host: process.env["CLAUDE_COLLAB_HOST"] ?? "localhost",
399
- /**
400
- * Heartbeat interval in milliseconds
401
- */
402
- heartbeatInterval: 3e4,
403
- /**
404
- * Client timeout in milliseconds (no heartbeat received)
405
- */
406
- clientTimeout: 6e4
407
- },
408
- /**
409
- * Communication configuration
410
- */
411
- communication: {
412
- /**
413
- * Default timeout for waiting for an answer (in milliseconds)
414
- */
415
- defaultTimeout: 3e4,
416
- /**
417
- * Maximum message content length
418
- */
419
- maxMessageLength: 5e4
420
- }};
421
-
422
- // src/domain/value-objects/message-content.vo.ts
423
- var MessageContent = class _MessageContent {
424
- _text;
425
- _format;
426
- constructor(text, format) {
427
- this._text = text;
428
- this._format = format;
429
- }
430
- /**
431
- * Creates a new MessageContent
432
- * @throws ValidationError if content is invalid
433
- */
434
- static create(text, format = "markdown") {
435
- const trimmedText = text.trim();
436
- if (!trimmedText) {
437
- throw new ValidationError("text", "Message content cannot be empty");
438
- }
439
- if (trimmedText.length > config.communication.maxMessageLength) {
440
- throw new ValidationError(
441
- "text",
442
- `Message content exceeds maximum length of ${config.communication.maxMessageLength} characters`
443
- );
444
- }
445
- return new _MessageContent(trimmedText, format);
446
- }
447
- /**
448
- * Creates a plain text message
449
- */
450
- static plain(text) {
451
- return _MessageContent.create(text, "plain");
452
- }
453
- /**
454
- * Creates a markdown message
455
- */
456
- static markdown(text) {
457
- return _MessageContent.create(text, "markdown");
458
- }
459
- /**
460
- * Reconstitutes from persistence
461
- */
462
- static reconstitute(props) {
463
- return new _MessageContent(props.text, props.format);
464
- }
465
- // Getters
466
- get text() {
467
- return this._text;
468
- }
469
- get format() {
470
- return this._format;
471
- }
472
- get length() {
473
- return this._text.length;
474
- }
475
- get isMarkdown() {
476
- return this._format === "markdown";
477
- }
478
- get isPlain() {
479
- return this._format === "plain";
480
- }
481
- /**
482
- * Returns a preview of the content (first 100 chars)
483
- */
484
- get preview() {
485
- if (this._text.length <= 100) {
486
- return this._text;
487
- }
488
- return `${this._text.substring(0, 97)}...`;
489
- }
490
- /**
491
- * Checks equality with another MessageContent
492
- */
493
- equals(other) {
494
- return this._text === other._text && this._format === other._format;
495
- }
496
- /**
497
- * Converts to plain object for serialization
498
- */
499
- toJSON() {
500
- return {
501
- text: this._text,
502
- format: this._format
503
- };
504
- }
505
- /**
506
- * String representation
507
- */
508
- toString() {
509
- return this._text;
510
- }
511
- };
512
-
513
- // src/domain/events/question-asked.event.ts
514
- var QuestionAskedEvent = class _QuestionAskedEvent extends BaseDomainEvent {
515
- constructor(questionId, fromMemberId, toTeamId, contentPreview) {
516
- super();
517
- this.questionId = questionId;
518
- this.fromMemberId = fromMemberId;
519
- this.toTeamId = toTeamId;
520
- this.contentPreview = contentPreview;
521
- }
522
- static EVENT_TYPE = "QUESTION_ASKED";
523
- get eventType() {
524
- return _QuestionAskedEvent.EVENT_TYPE;
525
- }
526
- get payload() {
527
- return {
528
- questionId: this.questionId,
529
- fromMemberId: this.fromMemberId,
530
- toTeamId: this.toTeamId,
531
- contentPreview: this.contentPreview
532
- };
533
- }
534
- };
535
-
536
- // src/application/use-cases/ask-question.use-case.ts
537
- var AskQuestionUseCase = class {
538
- constructor(deps) {
539
- this.deps = deps;
540
- }
541
- /**
542
- * Executes the use case
543
- */
544
- async execute(input) {
545
- const member = await this.deps.memberRepository.findById(input.fromMemberId);
546
- if (!member) {
547
- throw new MemberNotFoundError(input.fromMemberId);
548
- }
549
- const targetTeamId = createTeamId(input.toTeamName);
550
- const targetTeam = await this.deps.teamRepository.findById(targetTeamId);
551
- if (!targetTeam) {
552
- throw new TeamNotFoundError(input.toTeamName);
553
- }
554
- if (member.teamId === targetTeamId) {
555
- throw new ValidationError("toTeamName", "Cannot ask question to your own team");
556
- }
557
- const content = MessageContent.create(input.content, input.format ?? "markdown");
558
- const questionId = generateQuestionId();
559
- const question = Question.create({
560
- id: questionId,
561
- fromMemberId: input.fromMemberId,
562
- toTeamId: targetTeamId,
563
- content,
564
- createdAt: /* @__PURE__ */ new Date()
565
- });
566
- await this.deps.questionRepository.save(question);
567
- member.recordActivity();
568
- await this.deps.memberRepository.save(member);
569
- if (this.deps.onQuestionAsked) {
570
- const event = new QuestionAskedEvent(
571
- questionId,
572
- input.fromMemberId,
573
- targetTeamId,
574
- content.preview
575
- );
576
- await this.deps.onQuestionAsked(event);
577
- }
578
- return {
579
- questionId,
580
- toTeamId: targetTeamId,
581
- status: question.status,
582
- createdAt: question.createdAt
583
- };
584
- }
585
- };
586
-
587
- // src/application/use-cases/get-inbox.use-case.ts
588
- var GetInboxUseCase = class {
589
- constructor(deps) {
590
- this.deps = deps;
591
- }
592
- /**
593
- * Executes the use case
594
- */
595
- async execute(input) {
596
- const member = await this.deps.memberRepository.findById(input.memberId);
597
- if (!member) {
598
- throw new MemberNotFoundError(input.memberId);
599
- }
600
- const team = await this.deps.teamRepository.findById(input.teamId);
601
- if (!team) {
602
- throw new TeamNotFoundError(input.teamId);
603
- }
604
- const allQuestions = await this.deps.questionRepository.findPendingByTeamId(input.teamId);
605
- const questions = input.includeAnswered ? allQuestions : allQuestions.filter((q) => q.isPending);
606
- const questionItems = [];
607
- for (const question of questions) {
608
- const fromMember = await this.deps.memberRepository.findById(question.fromMemberId);
609
- const fromTeam = fromMember ? await this.deps.teamRepository.findById(fromMember.teamId) : null;
610
- questionItems.push({
611
- questionId: question.id,
612
- fromMemberId: question.fromMemberId,
613
- fromDisplayName: fromMember?.displayName ?? "Unknown",
614
- fromTeamName: fromTeam?.name ?? "Unknown",
615
- content: question.content.text,
616
- format: question.content.format,
617
- status: question.status,
618
- createdAt: question.createdAt,
619
- ageMs: question.ageMs
620
- });
621
- }
622
- questionItems.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
623
- const pendingCount = questionItems.filter((q) => q.status === "PENDING" /* PENDING */).length;
624
- return {
625
- teamId: team.id,
626
- teamName: team.name,
627
- questions: questionItems,
628
- totalCount: questionItems.length,
629
- pendingCount
630
- };
631
- }
632
- };
633
-
634
- // src/domain/entities/answer.entity.ts
635
- var Answer = class _Answer {
636
- _id;
637
- _questionId;
638
- _fromMemberId;
639
- _content;
640
- _createdAt;
641
- constructor(props) {
642
- this._id = props.id;
643
- this._questionId = props.questionId;
644
- this._fromMemberId = props.fromMemberId;
645
- this._content = props.content;
646
- this._createdAt = props.createdAt;
647
- }
648
- /**
649
- * Creates a new Answer instance
650
- */
651
- static create(props) {
652
- return new _Answer(props);
653
- }
654
- /**
655
- * Reconstitutes an Answer from persistence
656
- */
657
- static reconstitute(props) {
658
- return new _Answer(props);
659
- }
660
- // Getters
661
- get id() {
662
- return this._id;
663
- }
664
- get questionId() {
665
- return this._questionId;
666
- }
667
- get fromMemberId() {
668
- return this._fromMemberId;
669
- }
670
- get content() {
671
- return this._content;
672
- }
673
- get createdAt() {
674
- return this._createdAt;
675
- }
676
- /**
677
- * Converts entity to plain object for serialization
678
- */
679
- toJSON() {
680
- return {
681
- id: this._id,
682
- questionId: this._questionId,
683
- fromMemberId: this._fromMemberId,
684
- content: this._content,
685
- createdAt: this._createdAt
686
- };
687
- }
688
- };
689
-
690
- // src/domain/events/question-answered.event.ts
691
- var QuestionAnsweredEvent = class _QuestionAnsweredEvent extends BaseDomainEvent {
692
- constructor(questionId, answerId, answeredByMemberId, contentPreview) {
693
- super();
694
- this.questionId = questionId;
695
- this.answerId = answerId;
696
- this.answeredByMemberId = answeredByMemberId;
697
- this.contentPreview = contentPreview;
698
- }
699
- static EVENT_TYPE = "QUESTION_ANSWERED";
700
- get eventType() {
701
- return _QuestionAnsweredEvent.EVENT_TYPE;
702
- }
703
- get payload() {
704
- return {
705
- questionId: this.questionId,
706
- answerId: this.answerId,
707
- answeredByMemberId: this.answeredByMemberId,
708
- contentPreview: this.contentPreview
709
- };
710
- }
711
- };
712
-
713
- // src/application/use-cases/reply-question.use-case.ts
714
- var ReplyQuestionUseCase = class {
715
- constructor(deps) {
716
- this.deps = deps;
717
- }
718
- /**
719
- * Executes the use case
720
- */
721
- async execute(input) {
722
- const member = await this.deps.memberRepository.findById(input.fromMemberId);
723
- if (!member) {
724
- throw new MemberNotFoundError(input.fromMemberId);
725
- }
726
- const question = await this.deps.questionRepository.findById(input.questionId);
727
- if (!question) {
728
- throw new QuestionNotFoundError(input.questionId);
729
- }
730
- if (!question.canBeAnswered) {
731
- throw new QuestionAlreadyAnsweredError(input.questionId);
732
- }
733
- const content = MessageContent.create(input.content, input.format ?? "markdown");
734
- const answerId = generateAnswerId();
735
- const answer = Answer.create({
736
- id: answerId,
737
- questionId: input.questionId,
738
- fromMemberId: input.fromMemberId,
739
- content,
740
- createdAt: /* @__PURE__ */ new Date()
741
- });
742
- question.markAsAnswered(input.fromMemberId);
743
- await this.deps.answerRepository.save(answer);
744
- await this.deps.questionRepository.save(question);
745
- member.recordActivity();
746
- await this.deps.memberRepository.save(member);
747
- if (this.deps.onQuestionAnswered) {
748
- const event = new QuestionAnsweredEvent(
749
- input.questionId,
750
- answerId,
751
- input.fromMemberId,
752
- content.preview
753
- );
754
- await this.deps.onQuestionAnswered(event);
755
- }
756
- return {
757
- answerId,
758
- questionId: input.questionId,
759
- deliveredToMemberId: question.fromMemberId,
760
- createdAt: answer.createdAt
761
- };
762
- }
763
- };
764
-
765
- // src/infrastructure/repositories/in-memory-member.repository.ts
766
- var InMemoryMemberRepository = class {
767
- members = /* @__PURE__ */ new Map();
768
- async save(member) {
769
- this.members.set(member.id, member);
770
- }
771
- async findById(id) {
772
- return this.members.get(id) ?? null;
773
- }
774
- async findByTeamId(teamId) {
775
- return [...this.members.values()].filter((m) => m.teamId === teamId);
776
- }
777
- async findOnlineByTeamId(teamId) {
778
- return [...this.members.values()].filter((m) => m.teamId === teamId && m.isOnline);
779
- }
780
- async delete(id) {
781
- return this.members.delete(id);
782
- }
783
- async exists(id) {
784
- return this.members.has(id);
785
- }
786
- async findAll() {
787
- return [...this.members.values()];
788
- }
789
- /**
790
- * Clears all data (useful for testing)
791
- */
792
- clear() {
793
- this.members.clear();
794
- }
795
- /**
796
- * Gets the count of members
797
- */
798
- get count() {
799
- return this.members.size;
800
- }
801
- };
802
-
803
- // src/domain/entities/team.entity.ts
804
- var Team = class _Team {
805
- _id;
806
- _name;
807
- _createdAt;
808
- _memberIds;
809
- constructor(props) {
810
- this._id = props.id;
811
- this._name = props.name;
812
- this._createdAt = props.createdAt;
813
- this._memberIds = /* @__PURE__ */ new Set();
814
- }
815
- /**
816
- * Creates a new Team instance
817
- */
818
- static create(props) {
819
- if (!props.name.trim()) {
820
- throw new Error("Team name cannot be empty");
821
- }
822
- return new _Team(props);
823
- }
824
- /**
825
- * Reconstitutes a Team from persistence
826
- */
827
- static reconstitute(props) {
828
- const team = new _Team(props);
829
- for (const memberId of props.memberIds) {
830
- team._memberIds.add(memberId);
831
- }
832
- return team;
833
- }
834
- // Getters
835
- get id() {
836
- return this._id;
837
- }
838
- get name() {
839
- return this._name;
840
- }
841
- get createdAt() {
842
- return this._createdAt;
843
- }
844
- get memberIds() {
845
- return this._memberIds;
846
- }
847
- get memberCount() {
848
- return this._memberIds.size;
849
- }
850
- get isEmpty() {
851
- return this._memberIds.size === 0;
852
- }
853
- // Behaviors
854
- /**
855
- * Adds a member to the team
856
- * @returns true if the member was added, false if already present
857
- */
858
- addMember(memberId) {
859
- if (this._memberIds.has(memberId)) {
860
- return false;
861
- }
862
- this._memberIds.add(memberId);
863
- return true;
864
- }
865
- /**
866
- * Removes a member from the team
867
- * @returns true if the member was removed, false if not present
868
- */
869
- removeMember(memberId) {
870
- return this._memberIds.delete(memberId);
871
- }
872
- /**
873
- * Checks if a member is in the team
874
- */
875
- hasMember(memberId) {
876
- return this._memberIds.has(memberId);
877
- }
878
- /**
879
- * Gets all member IDs except the specified one
880
- * Useful for broadcasting to other team members
881
- */
882
- getOtherMemberIds(excludeMemberId) {
883
- return [...this._memberIds].filter((id) => id !== excludeMemberId);
884
- }
885
- /**
886
- * Converts entity to plain object for serialization
887
- */
888
- toJSON() {
889
- return {
890
- id: this._id,
891
- name: this._name,
892
- createdAt: this._createdAt,
893
- memberIds: [...this._memberIds]
894
- };
895
- }
896
- };
897
-
898
- // src/infrastructure/repositories/in-memory-team.repository.ts
899
- var InMemoryTeamRepository = class {
900
- teams = /* @__PURE__ */ new Map();
901
- async save(team) {
902
- this.teams.set(team.id, team);
903
- }
904
- async findById(id) {
905
- return this.teams.get(id) ?? null;
906
- }
907
- async findByName(name) {
908
- const teamId = createTeamId(name);
909
- return this.teams.get(teamId) ?? null;
910
- }
911
- async getOrCreate(name) {
912
- const existing = await this.findByName(name);
913
- if (existing) {
914
- return existing;
915
- }
916
- const teamId = createTeamId(name);
917
- const team = Team.create({
918
- id: teamId,
919
- name: name.trim(),
920
- createdAt: /* @__PURE__ */ new Date()
921
- });
922
- await this.save(team);
923
- return team;
924
- }
925
- async delete(id) {
926
- return this.teams.delete(id);
927
- }
928
- async exists(id) {
929
- return this.teams.has(id);
930
- }
931
- async findAll() {
932
- return [...this.teams.values()];
933
- }
934
- async findNonEmpty() {
935
- return [...this.teams.values()].filter((t) => !t.isEmpty);
936
- }
937
- /**
938
- * Clears all data (useful for testing)
939
- */
940
- clear() {
941
- this.teams.clear();
942
- }
943
- /**
944
- * Gets the count of teams
945
- */
946
- get count() {
947
- return this.teams.size;
948
- }
949
- };
950
-
951
- // src/infrastructure/repositories/in-memory-question.repository.ts
952
- var InMemoryQuestionRepository = class {
953
- questions = /* @__PURE__ */ new Map();
954
- async save(question) {
955
- this.questions.set(question.id, question);
956
- }
957
- async findById(id) {
958
- return this.questions.get(id) ?? null;
959
- }
960
- async findPendingByTeamId(teamId) {
961
- return [...this.questions.values()].filter((q) => q.toTeamId === teamId && q.isPending);
962
- }
963
- async findByFromMemberId(memberId) {
964
- return [...this.questions.values()].filter((q) => q.fromMemberId === memberId);
965
- }
966
- async findPendingByFromMemberId(memberId) {
967
- return [...this.questions.values()].filter(
968
- (q) => q.fromMemberId === memberId && q.isPending
969
- );
970
- }
971
- async delete(id) {
972
- return this.questions.delete(id);
973
- }
974
- async exists(id) {
975
- return this.questions.has(id);
976
- }
977
- async findAll() {
978
- return [...this.questions.values()];
979
- }
980
- async markTimedOut(olderThanMs) {
981
- let count = 0;
982
- const now = Date.now();
983
- for (const question of this.questions.values()) {
984
- if (question.isPending && now - question.createdAt.getTime() > olderThanMs) {
985
- question.markAsTimedOut();
986
- count++;
987
- }
988
- }
989
- return count;
990
- }
991
- /**
992
- * Clears all data (useful for testing)
993
- */
994
- clear() {
995
- this.questions.clear();
996
- }
997
- /**
998
- * Gets the count of questions
999
- */
1000
- get count() {
1001
- return this.questions.size;
1002
- }
1003
- };
1004
-
1005
- // src/infrastructure/repositories/in-memory-answer.repository.ts
1006
- var InMemoryAnswerRepository = class {
1007
- answers = /* @__PURE__ */ new Map();
1008
- async save(answer) {
1009
- this.answers.set(answer.id, answer);
1010
- }
1011
- async findById(id) {
1012
- return this.answers.get(id) ?? null;
1013
- }
1014
- async findByQuestionId(questionId) {
1015
- for (const answer of this.answers.values()) {
1016
- if (answer.questionId === questionId) {
1017
- return answer;
1018
- }
1019
- }
1020
- return null;
1021
- }
1022
- async findAll() {
1023
- return [...this.answers.values()];
1024
- }
1025
- /**
1026
- * Clears all data (useful for testing)
1027
- */
1028
- clear() {
1029
- this.answers.clear();
1030
- }
1031
- /**
1032
- * Gets the count of answers
1033
- */
1034
- get count() {
1035
- return this.answers.size;
1036
- }
1037
- };
1038
-
1039
- // src/infrastructure/websocket/message-protocol.ts
1040
- function serializeMessage(message) {
1041
- return JSON.stringify(message);
1042
- }
1043
- function parseClientMessage(data) {
1044
- const parsed = JSON.parse(data);
1045
- validateClientMessage(parsed);
1046
- return parsed;
1047
- }
1048
- function validateClientMessage(message) {
1049
- if (!message.type) {
1050
- throw new Error("Message must have a type");
1051
- }
1052
- const validTypes = ["JOIN", "LEAVE", "ASK", "REPLY", "PING", "GET_INBOX"];
1053
- if (!validTypes.includes(message.type)) {
1054
- throw new Error(`Invalid message type: ${message.type}`);
1055
- }
1056
- }
1057
- function createErrorMessage(code, message, requestId) {
1058
- return {
1059
- type: "ERROR",
1060
- code,
1061
- message,
1062
- requestId
1063
- };
1064
- }
1065
-
1066
- // src/infrastructure/websocket/hub-server.ts
1067
- function log(level, message, data) {
1068
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1069
- const prefix = `[${timestamp}] [HUB-${level}]`;
1070
- if (data) {
1071
- console.log(`${prefix} ${message}`, JSON.stringify(data, null, 2));
1072
- } else {
1073
- console.log(`${prefix} ${message}`);
1074
- }
1075
- }
1076
- var HubServer = class {
1077
- constructor(options = {}) {
1078
- this.options = options;
1079
- this.initializeUseCases();
1080
- }
1081
- wss = null;
1082
- clients = /* @__PURE__ */ new Map();
1083
- memberToWs = /* @__PURE__ */ new Map();
1084
- // Repositories
1085
- memberRepository = new InMemoryMemberRepository();
1086
- teamRepository = new InMemoryTeamRepository();
1087
- questionRepository = new InMemoryQuestionRepository();
1088
- answerRepository = new InMemoryAnswerRepository();
1089
- // Use cases
1090
- joinTeamUseCase;
1091
- askQuestionUseCase;
1092
- getInboxUseCase;
1093
- replyQuestionUseCase;
1094
- heartbeatInterval = null;
1095
- timeoutCheckInterval = null;
1096
- initializeUseCases() {
1097
- this.joinTeamUseCase = new JoinTeamUseCase({
1098
- memberRepository: this.memberRepository,
1099
- teamRepository: this.teamRepository,
1100
- onMemberJoined: async (event) => {
1101
- await this.broadcastToTeam(event.teamId, event.memberId, {
1102
- type: "MEMBER_JOINED",
1103
- member: await this.getMemberInfo(event.memberId)
1104
- });
1105
- }
1106
- });
1107
- this.askQuestionUseCase = new AskQuestionUseCase({
1108
- memberRepository: this.memberRepository,
1109
- teamRepository: this.teamRepository,
1110
- questionRepository: this.questionRepository,
1111
- onQuestionAsked: async (event) => {
1112
- const question = await this.questionRepository.findById(event.questionId);
1113
- if (question) {
1114
- await this.deliverQuestion(question);
1115
- }
1116
- }
1117
- });
1118
- this.getInboxUseCase = new GetInboxUseCase({
1119
- memberRepository: this.memberRepository,
1120
- teamRepository: this.teamRepository,
1121
- questionRepository: this.questionRepository
1122
- });
1123
- this.replyQuestionUseCase = new ReplyQuestionUseCase({
1124
- memberRepository: this.memberRepository,
1125
- questionRepository: this.questionRepository,
1126
- answerRepository: this.answerRepository,
1127
- onQuestionAnswered: async (event) => {
1128
- const question = await this.questionRepository.findById(event.questionId);
1129
- const answer = await this.answerRepository.findByQuestionId(event.questionId);
1130
- if (question && answer) {
1131
- await this.deliverAnswer(question, answer, event.answeredByMemberId);
1132
- }
1133
- }
1134
- });
1135
- }
1136
- /**
1137
- * Starts the hub server
1138
- */
1139
- async start() {
1140
- const port = this.options.port ?? config.hub.port;
1141
- const host = this.options.host ?? config.hub.host;
1142
- return new Promise((resolve, reject) => {
1143
- try {
1144
- this.wss = new WebSocketServer({ port, host });
1145
- this.wss.on("connection", (ws) => {
1146
- this.handleConnection(ws);
1147
- });
1148
- this.wss.on("error", (error) => {
1149
- log("ERROR", "Hub server error", { error: error.message, stack: error.stack });
1150
- reject(error);
1151
- });
1152
- this.wss.on("listening", () => {
1153
- log("INFO", `Hub server started successfully`, { host, port });
1154
- this.startHeartbeat();
1155
- this.startTimeoutCheck();
1156
- resolve();
1157
- });
1158
- } catch (error) {
1159
- reject(error);
1160
- }
1161
- });
1162
- }
1163
- /**
1164
- * Stops the hub server
1165
- */
1166
- async stop() {
1167
- if (this.heartbeatInterval) {
1168
- clearInterval(this.heartbeatInterval);
1169
- this.heartbeatInterval = null;
1170
- }
1171
- if (this.timeoutCheckInterval) {
1172
- clearInterval(this.timeoutCheckInterval);
1173
- this.timeoutCheckInterval = null;
1174
- }
1175
- return new Promise((resolve) => {
1176
- if (this.wss) {
1177
- for (const [ws] of this.clients) {
1178
- ws.close();
1179
- }
1180
- this.clients.clear();
1181
- this.memberToWs.clear();
1182
- this.wss.close(() => {
1183
- this.wss = null;
1184
- log("INFO", "Hub server stopped gracefully");
1185
- resolve();
1186
- });
1187
- } else {
1188
- resolve();
1189
- }
1190
- });
1191
- }
1192
- handleConnection(ws) {
1193
- const connection = {
1194
- ws,
1195
- lastPing: /* @__PURE__ */ new Date()
1196
- };
1197
- this.clients.set(ws, connection);
1198
- log("INFO", "New client connected", { totalClients: this.clients.size });
1199
- ws.on("message", async (data) => {
1200
- await this.handleMessage(ws, data.toString());
1201
- });
1202
- ws.on("close", async () => {
1203
- await this.handleDisconnect(ws);
1204
- });
1205
- ws.on("error", (error) => {
1206
- log("ERROR", "Client connection error", { error: error.message });
1207
- });
1208
- }
1209
- async handleMessage(ws, data) {
1210
- const connection = this.clients.get(ws);
1211
- if (!connection) return;
1212
- try {
1213
- const message = parseClientMessage(data);
1214
- connection.lastPing = /* @__PURE__ */ new Date();
1215
- log("DEBUG", `Received message from client`, {
1216
- type: message.type,
1217
- memberId: connection.memberId
1218
- });
1219
- switch (message.type) {
1220
- case "JOIN":
1221
- await this.handleJoin(ws, connection, message.teamName, message.displayName);
1222
- break;
1223
- case "LEAVE":
1224
- await this.handleLeave(ws, connection);
1225
- break;
1226
- case "ASK":
1227
- await this.handleAsk(ws, connection, message);
1228
- break;
1229
- case "REPLY":
1230
- await this.handleReply(ws, connection, message);
1231
- break;
1232
- case "GET_INBOX":
1233
- await this.handleGetInbox(ws, connection, message.requestId);
1234
- break;
1235
- case "PING":
1236
- this.send(ws, { type: "PONG", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1237
- break;
1238
- }
1239
- } catch (error) {
1240
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1241
- log("ERROR", "Failed to handle message", {
1242
- error: errorMessage,
1243
- memberId: connection.memberId
1244
- });
1245
- this.send(ws, createErrorMessage("INVALID_MESSAGE", errorMessage));
1246
- }
1247
- }
1248
- async handleJoin(ws, connection, teamName, displayName) {
1249
- try {
1250
- log("INFO", "Member attempting to join", { teamName, displayName });
1251
- const result = await this.joinTeamUseCase.execute({ teamName, displayName });
1252
- connection.memberId = result.memberId;
1253
- connection.teamId = result.teamId;
1254
- this.memberToWs.set(result.memberId, ws);
1255
- const memberInfo = await this.getMemberInfo(result.memberId);
1256
- this.send(ws, {
1257
- type: "JOINED",
1258
- member: memberInfo,
1259
- memberCount: result.memberCount
1260
- });
1261
- log("INFO", "Member joined successfully", {
1262
- memberId: result.memberId,
1263
- teamName,
1264
- displayName,
1265
- memberCount: result.memberCount
1266
- });
1267
- } catch (error) {
1268
- const errorMessage = error instanceof Error ? error.message : "Join failed";
1269
- log("ERROR", "Member join failed", { teamName, displayName, error: errorMessage });
1270
- this.send(ws, createErrorMessage("JOIN_FAILED", errorMessage));
1271
- }
1272
- }
1273
- async handleLeave(ws, connection) {
1274
- if (connection.memberId && connection.teamId) {
1275
- await this.removeMember(connection.memberId, connection.teamId);
1276
- connection.memberId = void 0;
1277
- connection.teamId = void 0;
1278
- }
1279
- this.send(ws, { type: "LEFT", memberId: connection.memberId });
1280
- }
1281
- async handleAsk(ws, connection, message) {
1282
- if (!connection.memberId) {
1283
- log("WARN", "ASK attempt without joining team", { toTeam: message.toTeam });
1284
- this.send(ws, createErrorMessage("NOT_JOINED", "Must join a team first", message.requestId));
1285
- return;
1286
- }
1287
- try {
1288
- log("INFO", "Question asked", {
1289
- fromMemberId: connection.memberId,
1290
- toTeam: message.toTeam,
1291
- contentPreview: message.content.substring(0, 50) + "..."
1292
- });
1293
- const result = await this.askQuestionUseCase.execute({
1294
- fromMemberId: connection.memberId,
1295
- toTeamName: message.toTeam,
1296
- content: message.content,
1297
- format: message.format
1298
- });
1299
- this.send(ws, {
1300
- type: "QUESTION_SENT",
1301
- questionId: result.questionId,
1302
- toTeamId: result.toTeamId,
1303
- status: result.status,
1304
- requestId: message.requestId
1305
- });
1306
- log("INFO", "Question sent successfully", {
1307
- questionId: result.questionId,
1308
- fromMemberId: connection.memberId,
1309
- toTeamId: result.toTeamId
1310
- });
1311
- } catch (error) {
1312
- const errorMessage = error instanceof Error ? error.message : "Ask failed";
1313
- log("ERROR", "Question failed", {
1314
- fromMemberId: connection.memberId,
1315
- toTeam: message.toTeam,
1316
- error: errorMessage
1317
- });
1318
- this.send(ws, createErrorMessage("ASK_FAILED", errorMessage, message.requestId));
1319
- }
1320
- }
1321
- async handleReply(ws, connection, message) {
1322
- if (!connection.memberId) {
1323
- log("WARN", "REPLY attempt without joining team", { questionId: message.questionId });
1324
- this.send(ws, createErrorMessage("NOT_JOINED", "Must join a team first"));
1325
- return;
1326
- }
1327
- try {
1328
- log("INFO", "Reply received", {
1329
- fromMemberId: connection.memberId,
1330
- questionId: message.questionId,
1331
- contentPreview: message.content.substring(0, 50) + "..."
1332
- });
1333
- await this.replyQuestionUseCase.execute({
1334
- fromMemberId: connection.memberId,
1335
- questionId: message.questionId,
1336
- content: message.content,
1337
- format: message.format
1338
- });
1339
- log("INFO", "Reply delivered successfully", {
1340
- fromMemberId: connection.memberId,
1341
- questionId: message.questionId
1342
- });
1343
- } catch (error) {
1344
- const errorMessage = error instanceof Error ? error.message : "Reply failed";
1345
- log("ERROR", "Reply failed", {
1346
- fromMemberId: connection.memberId,
1347
- questionId: message.questionId,
1348
- error: errorMessage
1349
- });
1350
- this.send(ws, createErrorMessage("REPLY_FAILED", errorMessage));
1351
- }
1352
- }
1353
- async handleGetInbox(ws, connection, requestId) {
1354
- if (!connection.memberId || !connection.teamId) {
1355
- this.send(ws, createErrorMessage("NOT_JOINED", "Must join a team first", requestId));
1356
- return;
1357
- }
1358
- try {
1359
- const result = await this.getInboxUseCase.execute({
1360
- memberId: connection.memberId,
1361
- teamId: connection.teamId
1362
- });
1363
- const questions = await Promise.all(
1364
- result.questions.map(async (q) => ({
1365
- questionId: q.questionId,
1366
- from: await this.getMemberInfo(q.fromMemberId),
1367
- content: q.content,
1368
- format: q.format,
1369
- status: q.status,
1370
- createdAt: q.createdAt.toISOString(),
1371
- ageMs: q.ageMs
1372
- }))
1373
- );
1374
- this.send(ws, {
1375
- type: "INBOX",
1376
- questions,
1377
- totalCount: result.totalCount,
1378
- pendingCount: result.pendingCount,
1379
- requestId
1380
- });
1381
- } catch (error) {
1382
- const errorMessage = error instanceof Error ? error.message : "Get inbox failed";
1383
- this.send(ws, createErrorMessage("INBOX_FAILED", errorMessage, requestId));
1384
- }
1385
- }
1386
- async handleDisconnect(ws) {
1387
- const connection = this.clients.get(ws);
1388
- if (connection?.memberId && connection.teamId) {
1389
- log("INFO", "Client disconnecting", {
1390
- memberId: connection.memberId,
1391
- teamId: connection.teamId
1392
- });
1393
- await this.removeMember(connection.memberId, connection.teamId);
1394
- this.memberToWs.delete(connection.memberId);
1395
- }
1396
- this.clients.delete(ws);
1397
- log("INFO", "Client disconnected", { totalClients: this.clients.size });
1398
- }
1399
- async removeMember(memberId, teamId) {
1400
- const member = await this.memberRepository.findById(memberId);
1401
- if (member) {
1402
- member.goOffline();
1403
- await this.memberRepository.save(member);
1404
- }
1405
- const team = await this.teamRepository.findById(teamId);
1406
- if (team) {
1407
- team.removeMember(memberId);
1408
- await this.teamRepository.save(team);
1409
- await this.broadcastToTeam(teamId, memberId, {
1410
- type: "MEMBER_LEFT",
1411
- memberId,
1412
- teamId
1413
- });
1414
- }
1415
- }
1416
- async deliverQuestion(question) {
1417
- const team = await this.teamRepository.findById(question.toTeamId);
1418
- if (!team) {
1419
- log("WARN", "Cannot deliver question - team not found", { toTeamId: question.toTeamId });
1420
- return;
1421
- }
1422
- const fromMember = await this.memberRepository.findById(question.fromMemberId);
1423
- if (!fromMember) {
1424
- log("WARN", "Cannot deliver question - from member not found", {
1425
- fromMemberId: question.fromMemberId
1426
- });
1427
- return;
1428
- }
1429
- const memberInfo = await this.getMemberInfo(question.fromMemberId);
1430
- let deliveredCount = 0;
1431
- for (const memberId of team.memberIds) {
1432
- const ws = this.memberToWs.get(memberId);
1433
- if (ws && ws.readyState === WebSocket.OPEN) {
1434
- this.send(ws, {
1435
- type: "QUESTION",
1436
- questionId: question.id,
1437
- from: memberInfo,
1438
- content: question.content.text,
1439
- format: question.content.format,
1440
- createdAt: question.createdAt.toISOString()
1441
- });
1442
- deliveredCount++;
1443
- }
1444
- }
1445
- log("INFO", "Question delivered to team", {
1446
- questionId: question.id,
1447
- toTeamId: question.toTeamId,
1448
- teamSize: team.memberIds.size,
1449
- deliveredCount
1450
- });
1451
- }
1452
- async deliverAnswer(question, answer, answeredByMemberId) {
1453
- const ws = this.memberToWs.get(question.fromMemberId);
1454
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1455
- log("WARN", "Cannot deliver answer - questioner not connected", {
1456
- questionId: question.id,
1457
- fromMemberId: question.fromMemberId
1458
- });
1459
- return;
1460
- }
1461
- const memberInfo = await this.getMemberInfo(answeredByMemberId);
1462
- this.send(ws, {
1463
- type: "ANSWER",
1464
- questionId: question.id,
1465
- from: memberInfo,
1466
- content: answer.content.text,
1467
- format: answer.content.format,
1468
- answeredAt: answer.createdAt.toISOString()
1469
- });
1470
- log("INFO", "Answer delivered", {
1471
- questionId: question.id,
1472
- answeredBy: answeredByMemberId,
1473
- deliveredTo: question.fromMemberId
1474
- });
1475
- }
1476
- async broadcastToTeam(teamId, excludeMemberId, message) {
1477
- const team = await this.teamRepository.findById(teamId);
1478
- if (!team) return;
1479
- for (const memberId of team.getOtherMemberIds(excludeMemberId)) {
1480
- const ws = this.memberToWs.get(memberId);
1481
- if (ws && ws.readyState === WebSocket.OPEN) {
1482
- this.send(ws, message);
1483
- }
1484
- }
1485
- }
1486
- async getMemberInfo(memberId) {
1487
- const member = await this.memberRepository.findById(memberId);
1488
- const team = member ? await this.teamRepository.findById(member.teamId) : null;
1489
- return {
1490
- memberId,
1491
- teamId: member?.teamId ?? "",
1492
- teamName: team?.name ?? "Unknown",
1493
- displayName: member?.displayName ?? "Unknown",
1494
- status: member?.status ?? "OFFLINE" /* OFFLINE */
1495
- };
1496
- }
1497
- send(ws, message) {
1498
- if (ws.readyState === WebSocket.OPEN) {
1499
- ws.send(serializeMessage(message));
1500
- }
1501
- }
1502
- startHeartbeat() {
1503
- this.heartbeatInterval = setInterval(() => {
1504
- const now = /* @__PURE__ */ new Date();
1505
- for (const [ws, connection] of this.clients) {
1506
- const timeSinceLastPing = now.getTime() - connection.lastPing.getTime();
1507
- if (timeSinceLastPing > config.hub.clientTimeout) {
1508
- ws.terminate();
1509
- }
1510
- }
1511
- }, config.hub.heartbeatInterval);
1512
- }
1513
- startTimeoutCheck() {
1514
- this.timeoutCheckInterval = setInterval(async () => {
1515
- await this.questionRepository.markTimedOut(config.communication.defaultTimeout);
1516
- }, 5e3);
1517
- }
1518
- /**
1519
- * Gets the number of connected clients
1520
- */
1521
- get clientCount() {
1522
- return this.clients.size;
1523
- }
1524
- /**
1525
- * Checks if the server is running
1526
- */
1527
- get isRunning() {
1528
- return this.wss !== null;
1529
- }
1530
- };
1531
-
1532
- // src/hub-main.ts
1533
- var args = process.argv.slice(2);
1534
- function parseArgs() {
1535
- let host = config.hub.host;
1536
- let port = config.hub.port;
1537
- for (let i = 0; i < args.length; i++) {
1538
- const arg = args[i];
1539
- const nextArg = args[i + 1];
1540
- if (arg === "--host" && nextArg) {
1541
- host = nextArg;
1542
- i++;
1543
- } else if (arg === "--port" && nextArg) {
1544
- port = parseInt(nextArg, 10);
1545
- i++;
1546
- } else if (arg === "-h" || arg === "--help") {
1547
- console.log(`
1548
- Claude Collab Hub Server
1549
-
1550
- Usage:
1551
- hub-main [options]
1552
-
1553
- Options:
1554
- --host <host> Host to bind to (default: ${config.hub.host})
1555
- --port <port> Port to listen on (default: ${config.hub.port})
1556
- -h, --help Show this help message
1557
- `);
1558
- process.exit(0);
1559
- }
1560
- }
1561
- return { host, port };
1562
- }
1563
- async function main() {
1564
- const { host, port } = parseArgs();
1565
- const server = new HubServer({ host, port });
1566
- const shutdown = async () => {
1567
- console.log("\nShutting down hub server...");
1568
- await server.stop();
1569
- process.exit(0);
1570
- };
1571
- process.on("SIGINT", shutdown);
1572
- process.on("SIGTERM", shutdown);
1573
- try {
1574
- await server.start();
1575
- console.log(`Claude Collab Hub Server running on ${host}:${port}`);
1576
- } catch (error) {
1577
- console.error("Failed to start hub server:", error);
1578
- process.exit(1);
1579
- }
1580
- }
1581
- main().catch((error) => {
1582
- console.error("Unexpected error:", error);
1583
- process.exit(1);
1584
- });
1585
- //# sourceMappingURL=hub-main.js.map
1586
- //# sourceMappingURL=hub-main.js.map