team-chat 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +458 -2
  3. package/agents/rules.md +52 -0
  4. package/agents/runtimes/codex.md +14 -0
  5. package/bin/bridge +3 -0
  6. package/bin/runner +3 -0
  7. package/cli/lib/agents.js +97 -0
  8. package/cli/lib/docs.js +151 -0
  9. package/cli/lib/local.js +41 -0
  10. package/cli/lib/server.js +52 -0
  11. package/cli/lib/util.js +107 -0
  12. package/cli/team-chat.js +383 -0
  13. package/host/bridge/attempts.js +43 -0
  14. package/host/bridge/bridge.js +326 -0
  15. package/host/bridge/config.js +105 -0
  16. package/host/bridge/knowledge-outbox.js +96 -0
  17. package/host/bridge/launcher.js +126 -0
  18. package/host/bridge/main.js +87 -0
  19. package/host/bridge/progress.js +74 -0
  20. package/host/bridge/prompt.js +97 -0
  21. package/host/bridge/run-archive.js +200 -0
  22. package/host/bridge/thread.js +44 -0
  23. package/host/bridge/triage.js +97 -0
  24. package/host/bridge/workdir.js +41 -0
  25. package/host/knowledge/card.js +43 -0
  26. package/host/knowledge/errors.js +50 -0
  27. package/host/knowledge/lesson.js +153 -0
  28. package/host/knowledge/process.js +160 -0
  29. package/host/knowledge/scrub.js +29 -0
  30. package/host/knowledge/shell.js +74 -0
  31. package/host/knowledge/stack.js +229 -0
  32. package/host/knowledge/transcript.js +82 -0
  33. package/host/lib/chat-api.js +120 -0
  34. package/host/lib/log.js +6 -0
  35. package/host/lib/pid-lock.js +65 -0
  36. package/host/lib/shell-words.js +45 -0
  37. package/host/runner/bridge-process.js +82 -0
  38. package/host/runner/config.js +81 -0
  39. package/host/runner/main.js +69 -0
  40. package/host/runner/retention.js +40 -0
  41. package/host/runner/runner.js +161 -0
  42. package/package.json +61 -5
  43. package/server/dist/src/app.js +25 -0
  44. package/server/dist/src/app.module.js +50 -0
  45. package/server/dist/src/attachments/attachment-store.js +111 -0
  46. package/server/dist/src/attachments/attachments.controller.js +67 -0
  47. package/server/dist/src/attachments/mime.js +84 -0
  48. package/server/dist/src/attachments/uploads.interceptor.js +72 -0
  49. package/server/dist/src/auth/participant.guard.js +53 -0
  50. package/server/dist/src/bridges/bridge-restart.service.js +80 -0
  51. package/server/dist/src/bridges/bridges.controller.js +47 -0
  52. package/server/dist/src/chats/chats.controller.js +83 -0
  53. package/server/dist/src/chats/chats.service.js +128 -0
  54. package/server/dist/src/collector/collector.controller.js +71 -0
  55. package/server/dist/src/collector/collector.module.js +23 -0
  56. package/server/dist/src/collector/collector.service.js +111 -0
  57. package/server/dist/src/common/clock.js +20 -0
  58. package/server/dist/src/common/errors.js +37 -0
  59. package/server/dist/src/common/format.js +14 -0
  60. package/server/dist/src/common/http.js +17 -0
  61. package/server/dist/src/common/slug.js +28 -0
  62. package/server/dist/src/common/tokens.js +19 -0
  63. package/server/dist/src/common/validation.js +41 -0
  64. package/server/dist/src/config/config.js +39 -0
  65. package/server/dist/src/conversation.module.js +41 -0
  66. package/server/dist/src/db/database.js +73 -0
  67. package/server/dist/src/db/database.module.js +51 -0
  68. package/server/dist/src/db/schema.js +212 -0
  69. package/server/dist/src/decisions/decisions.controller.js +55 -0
  70. package/server/dist/src/decisions/decisions.service.js +109 -0
  71. package/server/dist/src/documents/documents.controller.js +87 -0
  72. package/server/dist/src/documents/documents.service.js +189 -0
  73. package/server/dist/src/health/health.controller.js +29 -0
  74. package/server/dist/src/knowledge/knowledge.controller.js +83 -0
  75. package/server/dist/src/knowledge/knowledge.module.js +25 -0
  76. package/server/dist/src/knowledge/knowledge.service.js +189 -0
  77. package/server/dist/src/local/docs-sync.service.js +97 -0
  78. package/server/dist/src/local/local.controller.js +197 -0
  79. package/server/dist/src/local/local.guard.js +55 -0
  80. package/server/dist/src/local/local.module.js +30 -0
  81. package/server/dist/src/local/setup.service.js +154 -0
  82. package/server/dist/src/local/transfer.service.js +151 -0
  83. package/server/dist/src/main.js +33 -0
  84. package/server/dist/src/mcp/chat-tools.js +282 -0
  85. package/server/dist/src/mcp/mcp.controller.js +58 -0
  86. package/server/dist/src/mcp/mcp.module.js +24 -0
  87. package/server/dist/src/mentions/mention-state.js +80 -0
  88. package/server/dist/src/mentions/mentions.controller.js +100 -0
  89. package/server/dist/src/mentions/mentions.service.js +203 -0
  90. package/server/dist/src/messages/message.presenter.js +102 -0
  91. package/server/dist/src/messages/messages.service.js +149 -0
  92. package/server/dist/src/messages/thread.controller.js +179 -0
  93. package/server/dist/src/participants/directory.service.js +68 -0
  94. package/server/dist/src/participants/participants.module.js +24 -0
  95. package/server/dist/src/participants/participants.service.js +84 -0
  96. package/server/dist/src/settings/settings.service.js +42 -0
  97. package/server/dist/src/sharing/consent.js +16 -0
  98. package/server/dist/src/sharing/sharing.module.js +22 -0
  99. package/server/dist/src/sharing/sharing.service.js +175 -0
  100. package/server/dist/src/team/runner.service.js +75 -0
  101. package/server/dist/src/team/team.controller.js +178 -0
  102. package/server/dist/src/team/team.module.js +25 -0
  103. package/server/dist/src/team/team.service.js +280 -0
  104. package/server/dist/src/web/template.js +57 -0
  105. package/server/dist/src/web/web-login.service.js +74 -0
  106. package/server/dist/src/web/web.controller.js +203 -0
  107. package/server/dist/src/web/web.module.js +25 -0
  108. package/server/drizzle/0000_init.sql +169 -0
  109. package/server/drizzle/0001_seed_roles.sql +25 -0
  110. package/server/drizzle/0002_knowledge.sql +17 -0
  111. package/server/drizzle/0003_sharing.sql +30 -0
  112. package/server/drizzle/meta/0000_snapshot.json +1224 -0
  113. package/server/drizzle/meta/0001_snapshot.json +1224 -0
  114. package/server/drizzle/meta/0002_snapshot.json +1359 -0
  115. package/server/drizzle/meta/0003_snapshot.json +1545 -0
  116. package/server/drizzle/meta/_journal.json +34 -0
  117. package/server/package.json +1 -0
  118. package/server/views/chat.html +865 -0
  119. package/server/views/documents.html +204 -0
  120. package/server/views/login.html +31 -0
  121. package/server/views/setup.html +60 -0
  122. package/server/views/team.html +369 -0
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.RunnerController = exports.TeamController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const participant_guard_1 = require("../auth/participant.guard");
18
+ const chats_service_1 = require("../chats/chats.service");
19
+ const runner_service_1 = require("./runner.service");
20
+ const team_service_1 = require("./team.service");
21
+ /** Команда: агенты, роли, машины. Читают все, меняет владелец. */
22
+ let TeamController = class TeamController {
23
+ team;
24
+ chats;
25
+ constructor(team, chats) {
26
+ this.team = team;
27
+ this.chats = chats;
28
+ }
29
+ index() {
30
+ return this.team.overview();
31
+ }
32
+ addAgent(me, body) {
33
+ return this.team.addAgent(me, body);
34
+ }
35
+ updateAgent(me, handle, body) {
36
+ return this.team.updateAgent(me, handle, body);
37
+ }
38
+ addRole(me, body) {
39
+ return this.team.addRole(me, body);
40
+ }
41
+ updateRole(me, slug, body) {
42
+ return this.team.updateRole(me, slug, body);
43
+ }
44
+ removeRole(me, slug) {
45
+ return this.team.removeRole(me, slug);
46
+ }
47
+ async chatRole(me, slug, body) {
48
+ return this.team.setChatRole(me, await this.chats.bySlug(slug), body);
49
+ }
50
+ addRunner(me, body, request) {
51
+ return this.team.addRunner(me, body, origin(request));
52
+ }
53
+ rotateRunner(me, handle, request) {
54
+ return this.team.rotateRunner(me, handle, origin(request));
55
+ }
56
+ };
57
+ exports.TeamController = TeamController;
58
+ __decorate([
59
+ (0, common_1.Get)('team'),
60
+ __metadata("design:type", Function),
61
+ __metadata("design:paramtypes", []),
62
+ __metadata("design:returntype", void 0)
63
+ ], TeamController.prototype, "index", null);
64
+ __decorate([
65
+ (0, common_1.Post)('agents'),
66
+ __param(0, (0, participant_guard_1.Me)()),
67
+ __param(1, (0, common_1.Body)()),
68
+ __metadata("design:type", Function),
69
+ __metadata("design:paramtypes", [Object, Object]),
70
+ __metadata("design:returntype", void 0)
71
+ ], TeamController.prototype, "addAgent", null);
72
+ __decorate([
73
+ (0, common_1.Patch)('agents/:handle'),
74
+ __param(0, (0, participant_guard_1.Me)()),
75
+ __param(1, (0, common_1.Param)('handle')),
76
+ __param(2, (0, common_1.Body)()),
77
+ __metadata("design:type", Function),
78
+ __metadata("design:paramtypes", [Object, String, Object]),
79
+ __metadata("design:returntype", void 0)
80
+ ], TeamController.prototype, "updateAgent", null);
81
+ __decorate([
82
+ (0, common_1.Post)('roles'),
83
+ __param(0, (0, participant_guard_1.Me)()),
84
+ __param(1, (0, common_1.Body)()),
85
+ __metadata("design:type", Function),
86
+ __metadata("design:paramtypes", [Object, Object]),
87
+ __metadata("design:returntype", void 0)
88
+ ], TeamController.prototype, "addRole", null);
89
+ __decorate([
90
+ (0, common_1.Patch)('roles/:slug'),
91
+ __param(0, (0, participant_guard_1.Me)()),
92
+ __param(1, (0, common_1.Param)('slug')),
93
+ __param(2, (0, common_1.Body)()),
94
+ __metadata("design:type", Function),
95
+ __metadata("design:paramtypes", [Object, String, Object]),
96
+ __metadata("design:returntype", void 0)
97
+ ], TeamController.prototype, "updateRole", null);
98
+ __decorate([
99
+ (0, common_1.Delete)('roles/:slug'),
100
+ __param(0, (0, participant_guard_1.Me)()),
101
+ __param(1, (0, common_1.Param)('slug')),
102
+ __metadata("design:type", Function),
103
+ __metadata("design:paramtypes", [Object, String]),
104
+ __metadata("design:returntype", void 0)
105
+ ], TeamController.prototype, "removeRole", null);
106
+ __decorate([
107
+ (0, common_1.Put)('chats/:chat/roles'),
108
+ __param(0, (0, participant_guard_1.Me)()),
109
+ __param(1, (0, common_1.Param)('chat')),
110
+ __param(2, (0, common_1.Body)()),
111
+ __metadata("design:type", Function),
112
+ __metadata("design:paramtypes", [Object, String, Object]),
113
+ __metadata("design:returntype", Promise)
114
+ ], TeamController.prototype, "chatRole", null);
115
+ __decorate([
116
+ (0, common_1.Post)('runners'),
117
+ __param(0, (0, participant_guard_1.Me)()),
118
+ __param(1, (0, common_1.Body)()),
119
+ __param(2, (0, common_1.Req)()),
120
+ __metadata("design:type", Function),
121
+ __metadata("design:paramtypes", [Object, Object, Object]),
122
+ __metadata("design:returntype", void 0)
123
+ ], TeamController.prototype, "addRunner", null);
124
+ __decorate([
125
+ (0, common_1.Post)('runners/:handle/token'),
126
+ (0, common_1.HttpCode)(200),
127
+ __param(0, (0, participant_guard_1.Me)()),
128
+ __param(1, (0, common_1.Param)('handle')),
129
+ __param(2, (0, common_1.Req)()),
130
+ __metadata("design:type", Function),
131
+ __metadata("design:paramtypes", [Object, String, Object]),
132
+ __metadata("design:returntype", void 0)
133
+ ], TeamController.prototype, "rotateRunner", null);
134
+ exports.TeamController = TeamController = __decorate([
135
+ (0, common_1.Controller)('api'),
136
+ (0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
137
+ __metadata("design:paramtypes", [team_service_1.TeamService, chats_service_1.ChatsService])
138
+ ], TeamController);
139
+ /** Раннер на машине с агентами — только участник host. */
140
+ let RunnerController = class RunnerController {
141
+ runner;
142
+ constructor(runner) {
143
+ this.runner = runner;
144
+ }
145
+ agents(me, runtimes) {
146
+ return this.runner.agents(me, runtimes);
147
+ }
148
+ token(me, handle) {
149
+ return this.runner.token(me, handle);
150
+ }
151
+ };
152
+ exports.RunnerController = RunnerController;
153
+ __decorate([
154
+ (0, common_1.Get)('agents'),
155
+ __param(0, (0, participant_guard_1.Me)()),
156
+ __param(1, (0, common_1.Query)('runtimes')),
157
+ __metadata("design:type", Function),
158
+ __metadata("design:paramtypes", [Object, String]),
159
+ __metadata("design:returntype", void 0)
160
+ ], RunnerController.prototype, "agents", null);
161
+ __decorate([
162
+ (0, common_1.Post)('agents/:handle/token'),
163
+ (0, common_1.HttpCode)(200),
164
+ __param(0, (0, participant_guard_1.Me)()),
165
+ __param(1, (0, common_1.Param)('handle')),
166
+ __metadata("design:type", Function),
167
+ __metadata("design:paramtypes", [Object, String]),
168
+ __metadata("design:returntype", void 0)
169
+ ], RunnerController.prototype, "token", null);
170
+ exports.RunnerController = RunnerController = __decorate([
171
+ (0, common_1.Controller)('api/runner'),
172
+ (0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
173
+ __metadata("design:paramtypes", [runner_service_1.RunnerService])
174
+ ], RunnerController);
175
+ /** Адрес, по которому открыли страницу: по нему чат виден и с другой машины. */
176
+ function origin(request) {
177
+ return `${request.protocol}://${request.get('host')}`;
178
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.TeamModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const conversation_module_1 = require("../conversation.module");
12
+ const runner_service_1 = require("./runner.service");
13
+ const team_controller_1 = require("./team.controller");
14
+ const team_service_1 = require("./team.service");
15
+ /** Команда и раннеры: кто есть, в каких ролях и на каких машинах. */
16
+ let TeamModule = class TeamModule {
17
+ };
18
+ exports.TeamModule = TeamModule;
19
+ exports.TeamModule = TeamModule = __decorate([
20
+ (0, common_1.Module)({
21
+ imports: [conversation_module_1.ConversationModule],
22
+ providers: [team_service_1.TeamService, runner_service_1.RunnerService],
23
+ controllers: [team_controller_1.TeamController, team_controller_1.RunnerController],
24
+ })
25
+ ], TeamModule);
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.TeamService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const drizzle_orm_1 = require("drizzle-orm");
18
+ const zod_1 = require("zod");
19
+ const clock_1 = require("../common/clock");
20
+ const errors_1 = require("../common/errors");
21
+ const format_1 = require("../common/format");
22
+ const slug_1 = require("../common/slug");
23
+ const validation_1 = require("../common/validation");
24
+ const database_module_1 = require("../db/database.module");
25
+ const schema_1 = require("../db/schema");
26
+ const directory_service_1 = require("../participants/directory.service");
27
+ const participants_service_1 = require("../participants/participants.service");
28
+ const ROLE_SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
29
+ const RUNTIME = /^[a-z0-9_-]{1,40}$/;
30
+ const ACCENT = /^#[0-9a-fA-F]{6}$/;
31
+ /** Слаги служебных участников: ролью агента они быть не могут. */
32
+ const RESERVED_ROLES = [participants_service_1.ROLE_OWNER, participants_service_1.ROLE_HOST];
33
+ const agentSchema = zod_1.z.object({
34
+ handle: zod_1.z.string().regex(participants_service_1.HANDLE_PATTERN, { error: 'Имя агента — латиница, цифры, дефис и подчёркивание, от 2 до 40 знаков.' })
35
+ // @all раскрывается на всех — агент с таким именем перехватил бы обращение.
36
+ .refine(h => h !== 'all', { error: 'Имя all занято обращением ко всем.' }),
37
+ name: zod_1.z.string().max(60),
38
+ role: zod_1.z.string(),
39
+ runtime: zod_1.z.string().regex(RUNTIME),
40
+ accent: zod_1.z.string().regex(ACCENT).nullish(),
41
+ });
42
+ const agentUpdateSchema = zod_1.z.object({
43
+ name: zod_1.z.string().max(60).optional(),
44
+ role: zod_1.z.string().optional(),
45
+ runtime: zod_1.z.string().regex(RUNTIME).optional(),
46
+ accent: zod_1.z.string().regex(ACCENT).optional(),
47
+ disabled: (0, validation_1.flag)().optional(),
48
+ // На какой машине работает: handle раннера; null — «свободен, заберёт первый».
49
+ host: zod_1.z.string().nullish(),
50
+ });
51
+ const roleSchema = zod_1.z.object({
52
+ slug: zod_1.z.string().regex(ROLE_SLUG, { error: 'Адрес роли — латиница, цифры и дефис.' })
53
+ .refine(s => !RESERVED_ROLES.includes(s), { error: 'Это имя занято служебной ролью.' }),
54
+ name: zod_1.z.string().max(60),
55
+ instructions: zod_1.z.string().max(20000),
56
+ });
57
+ const roleUpdateSchema = zod_1.z.object({
58
+ name: zod_1.z.string().max(60).optional(),
59
+ instructions: zod_1.z.string().max(20000).optional(),
60
+ // На слаг ссылаются агенты и замены в чатах.
61
+ slug: zod_1.z.undefined({ error: 'Адрес роли не меняется.' }).optional(),
62
+ });
63
+ const chatRoleSchema = zod_1.z.object({ handle: zod_1.z.string(), role: zod_1.z.string().nullable() });
64
+ /**
65
+ * Команда: агенты, роли, машины. Видят все, меняет только владелец — роль это инструкция
66
+ * агенту, а агент работает на машине владельца.
67
+ */
68
+ let TeamService = class TeamService {
69
+ db;
70
+ participants;
71
+ directories;
72
+ constructor(db, participants, directories) {
73
+ this.db = db;
74
+ this.participants = participants;
75
+ this.directories = directories;
76
+ }
77
+ async overview() {
78
+ const [agents, allRoles, runners, overrides, directory] = await Promise.all([
79
+ this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.eq)(schema_1.participants.kind, 'ai')).orderBy((0, drizzle_orm_1.asc)(schema_1.participants.id)),
80
+ this.db.select().from(schema_1.roles).orderBy((0, drizzle_orm_1.asc)(schema_1.roles.id)),
81
+ this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.eq)(schema_1.participants.kind, participants_service_1.KIND_HOST)).orderBy((0, drizzle_orm_1.asc)(schema_1.participants.id)),
82
+ this.chatRoles(),
83
+ this.directories.load(),
84
+ ]);
85
+ const usedRoles = new Set([...agents.map(a => a.role), ...overrides.map(o => o.role)]);
86
+ const runnerById = new Map(runners.map(r => [r.id, r]));
87
+ return {
88
+ agents: agents.map(a => this.agentApi(a, directory, runnerById, overrides)),
89
+ roles: allRoles.map(r => ({ ...roleApi(r), in_use: usedRoles.has(r.slug) })),
90
+ runners: runners.map(r => ({
91
+ handle: r.handle,
92
+ name: r.name,
93
+ seen_at: (0, format_1.iso)(r.lastSeenAt),
94
+ // Движки сообщает каждый раннер сам — какие есть на его машине.
95
+ runtimes: r.runtimes ?? [],
96
+ agents: agents.filter(a => a.hostId === r.id).map(a => a.handle),
97
+ })),
98
+ runtimes: [...new Set(runners.flatMap(r => r.runtimes ?? []))],
99
+ };
100
+ }
101
+ /** Ещё одна машина с агентами: участник-раннер и его токен — вместе с командой для той машины. */
102
+ async addRunner(me, input, origin) {
103
+ ownerOnly(me);
104
+ const { name } = (0, validation_1.validate)(zod_1.z.object({ name: zod_1.z.string().max(60) }), input);
105
+ const base = `host-${(0, slug_1.slugify)(name) || 'machine'}`;
106
+ const taken = new Set((await this.db.select({ handle: schema_1.participants.handle }).from(schema_1.participants)
107
+ .where((0, drizzle_orm_1.like)(schema_1.participants.handle, `${base}%`))).map(p => p.handle));
108
+ let handle = base;
109
+ for (let i = 2; taken.has(handle); i++)
110
+ handle = `${base}-${i}`;
111
+ const [runner] = await this.db.insert(schema_1.participants).values({ handle, name, kind: participants_service_1.KIND_HOST, role: participants_service_1.ROLE_HOST }).returning();
112
+ return this.runnerToken(runner, origin);
113
+ }
114
+ /** Новый токен машины — старый перестаёт работать (утёк, машину сменили). */
115
+ async rotateRunner(me, handle, origin) {
116
+ ownerOnly(me);
117
+ return this.runnerToken(await this.byHandle(handle, participants_service_1.KIND_HOST), origin);
118
+ }
119
+ /** Агента запускает раннер, и токен он берёт себе сам — здесь токен не выдаётся. */
120
+ async addAgent(me, input) {
121
+ ownerOnly(me);
122
+ const data = (0, validation_1.validate)(agentSchema, input);
123
+ if (await this.handleTaken(data.handle))
124
+ throw errors_1.ValidationFailed.field('handle', 'Такое имя уже занято.');
125
+ await this.requireRole(data.role);
126
+ const [agent] = await this.db.insert(schema_1.participants).values({
127
+ handle: data.handle, name: data.name, kind: 'ai', role: data.role, runtime: data.runtime, accent: data.accent ?? '#6d28d9',
128
+ }).returning();
129
+ return { agent: await this.agent(agent) };
130
+ }
131
+ async updateAgent(me, handle, input) {
132
+ ownerOnly(me);
133
+ const agent = await this.byHandle(handle, 'ai');
134
+ const data = (0, validation_1.validate)(agentUpdateSchema, input);
135
+ if (data.role !== undefined)
136
+ await this.requireRole(data.role);
137
+ const changes = {};
138
+ for (const key of ['name', 'role', 'runtime', 'accent'])
139
+ if (data[key] !== undefined)
140
+ changes[key] = data[key];
141
+ if (data.host !== undefined) {
142
+ changes.hostId = data.host === null ? null : (await this.hostByHandleOr422(data.host)).id;
143
+ }
144
+ else if (data.runtime !== undefined && data.runtime !== agent.runtime) {
145
+ // Сменился движок — машина могла его не уметь: пусть заберёт та, что умеет.
146
+ changes.hostId = null;
147
+ }
148
+ if (data.disabled !== undefined)
149
+ changes.disabledAt = data.disabled ? agent.disabledAt ?? (0, clock_1.now)() : null;
150
+ const [updated] = Object.keys(changes).length
151
+ ? await this.db.update(schema_1.participants).set(changes).where((0, drizzle_orm_1.eq)(schema_1.participants.id, agent.id)).returning()
152
+ : [agent];
153
+ return { agent: await this.agent(updated) };
154
+ }
155
+ async addRole(me, input) {
156
+ ownerOnly(me);
157
+ const raw = (input ?? {});
158
+ const data = (0, validation_1.validate)(roleSchema, { ...raw, slug: raw.slug || (0, slug_1.slugify)(String(raw.name ?? '')) });
159
+ const [exists] = await this.db.select({ id: schema_1.roles.id }).from(schema_1.roles).where((0, drizzle_orm_1.eq)(schema_1.roles.slug, data.slug));
160
+ if (exists)
161
+ throw errors_1.ValidationFailed.field('slug', 'Роль с таким адресом уже есть.');
162
+ const [role] = await this.db.insert(schema_1.roles).values(data).returning();
163
+ return { role: roleApi(role) };
164
+ }
165
+ async updateRole(me, slug, input) {
166
+ ownerOnly(me);
167
+ const role = await this.role(slug);
168
+ const data = (0, validation_1.validate)(roleUpdateSchema, input);
169
+ const changes = Object.fromEntries(Object.entries({ name: data.name, instructions: data.instructions }).filter(([, v]) => v !== undefined));
170
+ const [updated] = Object.keys(changes).length
171
+ ? await this.db.update(schema_1.roles).set(changes).where((0, drizzle_orm_1.eq)(schema_1.roles.id, role.id)).returning()
172
+ : [role];
173
+ return { role: roleApi(updated) };
174
+ }
175
+ async removeRole(me, slug) {
176
+ ownerOnly(me);
177
+ const role = await this.role(slug);
178
+ const [agent] = await this.db.select({ id: schema_1.participants.id }).from(schema_1.participants).where((0, drizzle_orm_1.eq)(schema_1.participants.role, slug)).limit(1);
179
+ const [override] = await this.db.select({ id: schema_1.chatAgentRoles.id }).from(schema_1.chatAgentRoles).where((0, drizzle_orm_1.eq)(schema_1.chatAgentRoles.role, slug)).limit(1);
180
+ if (agent || override)
181
+ throw (0, errors_1.conflict)('in_use', 'Роль назначена агентам — сначала смените им роль.');
182
+ await this.db.delete(schema_1.roles).where((0, drizzle_orm_1.eq)(schema_1.roles.id, role.id));
183
+ return { deleted: slug };
184
+ }
185
+ /** Роль агента в этом чате. role = null или основная — снять замену. */
186
+ async setChatRole(me, chat, input) {
187
+ ownerOnly(me);
188
+ const data = (0, validation_1.validate)(chatRoleSchema, input);
189
+ const [agent] = await this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participants.handle, data.handle), (0, drizzle_orm_1.eq)(schema_1.participants.kind, 'ai')));
190
+ if (!agent)
191
+ throw errors_1.ValidationFailed.field('handle', 'Такого агента нет.');
192
+ if (data.role !== null)
193
+ await this.requireRole(data.role);
194
+ const key = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.chatAgentRoles.chatId, chat.id), (0, drizzle_orm_1.eq)(schema_1.chatAgentRoles.participantId, agent.id));
195
+ if (data.role === null || data.role === agent.role) {
196
+ await this.db.delete(schema_1.chatAgentRoles).where(key);
197
+ }
198
+ else {
199
+ await this.db.insert(schema_1.chatAgentRoles).values({ chatId: chat.id, participantId: agent.id, role: data.role })
200
+ .onConflictDoUpdate({ target: [schema_1.chatAgentRoles.chatId, schema_1.chatAgentRoles.participantId], set: { role: data.role } });
201
+ }
202
+ return { handle: agent.handle, role: (await this.directories.load()).roleIn(agent, chat.id) };
203
+ }
204
+ async runnerToken(runner, origin) {
205
+ const token = await this.participants.issueToken(runner.id);
206
+ return {
207
+ runner: { handle: runner.handle, name: runner.name },
208
+ token,
209
+ // Адрес — тот, по которому владелец открыл страницу: с другой машины чат виден по нему же.
210
+ connect: `team-chat connect ${origin} --token ${token}`,
211
+ };
212
+ }
213
+ async agent(agent) {
214
+ const [directory, overrides, runners] = await Promise.all([
215
+ this.directories.load(),
216
+ this.chatRoles([agent.id]),
217
+ this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.eq)(schema_1.participants.kind, participants_service_1.KIND_HOST)),
218
+ ]);
219
+ return this.agentApi(agent, directory, new Map(runners.map(r => [r.id, r])), overrides);
220
+ }
221
+ agentApi(p, directory, runners, overrides) {
222
+ const host = p.hostId === null ? undefined : runners.get(p.hostId);
223
+ return {
224
+ handle: p.handle,
225
+ name: p.name,
226
+ accent: p.accent,
227
+ role: p.role,
228
+ role_name: directory.roleName(p.role),
229
+ runtime: p.runtime,
230
+ host: host ? { handle: host.handle, name: host.name } : null,
231
+ disabled: p.disabledAt !== null,
232
+ seen_at: (0, format_1.iso)(p.lastSeenAt),
233
+ chat_roles: Object.fromEntries(overrides.filter(o => o.participantId === p.id).map(o => [o.chat, o.role])),
234
+ };
235
+ }
236
+ chatRoles(participantIds) {
237
+ return this.db.select({ participantId: schema_1.chatAgentRoles.participantId, role: schema_1.chatAgentRoles.role, chat: schema_1.chats.slug })
238
+ .from(schema_1.chatAgentRoles).innerJoin(schema_1.chats, (0, drizzle_orm_1.eq)(schema_1.chats.id, schema_1.chatAgentRoles.chatId))
239
+ .where(participantIds ? (0, drizzle_orm_1.inArray)(schema_1.chatAgentRoles.participantId, participantIds) : undefined);
240
+ }
241
+ async byHandle(handle, kind) {
242
+ const [p] = await this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participants.handle, handle), (0, drizzle_orm_1.eq)(schema_1.participants.kind, kind)));
243
+ if (!p)
244
+ throw (0, errors_1.notFound)();
245
+ return p;
246
+ }
247
+ async hostByHandleOr422(handle) {
248
+ const [p] = await this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participants.handle, handle), (0, drizzle_orm_1.eq)(schema_1.participants.kind, participants_service_1.KIND_HOST)));
249
+ if (!p)
250
+ throw errors_1.ValidationFailed.field('host', 'Такой машины нет.');
251
+ return p;
252
+ }
253
+ async role(slug) {
254
+ const [role] = await this.db.select().from(schema_1.roles).where((0, drizzle_orm_1.eq)(schema_1.roles.slug, slug));
255
+ if (!role)
256
+ throw (0, errors_1.notFound)();
257
+ return role;
258
+ }
259
+ async requireRole(slug) {
260
+ const [role] = await this.db.select({ id: schema_1.roles.id }).from(schema_1.roles).where((0, drizzle_orm_1.eq)(schema_1.roles.slug, slug));
261
+ if (!role)
262
+ throw errors_1.ValidationFailed.field('role', 'Такой роли нет.');
263
+ }
264
+ async handleTaken(handle) {
265
+ const [p] = await this.db.select({ id: schema_1.participants.id }).from(schema_1.participants).where((0, drizzle_orm_1.eq)(schema_1.participants.handle, handle));
266
+ return Boolean(p);
267
+ }
268
+ };
269
+ exports.TeamService = TeamService;
270
+ exports.TeamService = TeamService = __decorate([
271
+ (0, common_1.Injectable)(),
272
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
273
+ __metadata("design:paramtypes", [Object, participants_service_1.ParticipantsService,
274
+ directory_service_1.DirectoryService])
275
+ ], TeamService);
276
+ const roleApi = (r) => ({ slug: r.slug, name: r.name, instructions: r.instructions });
277
+ function ownerOnly(me) {
278
+ if (!(0, participants_service_1.isOwner)(me))
279
+ throw (0, errors_1.forbidden)('Агентов и роли настраивает только владелец.');
280
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ // Страницы — HTML-файлы из server/views с подстановками. Почти вся страница — клиентский
3
+ // JS, который ходит в API тем же токеном, что и агенты; серверу остаётся вставить
4
+ // несколько значений:
5
+ //
6
+ // {{ chat.name }} значение, экранированное для HTML
7
+ // {{json token}} значение как JSON — безопасно внутри <script>
8
+ // {{#owner}}…{{/owner}} блок, если значение истинно
9
+ // {{^owner}}…{{/owner}} блок, если ложно
10
+ //
11
+ // Неизвестное имя — ошибка, а не пустая строка: опечатка в шаблоне должна быть видна сразу.
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.render = render;
17
+ exports.renderString = renderString;
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ const VIEWS = node_path_1.default.join(__dirname, '..', '..', '..', 'views');
21
+ const cache = new Map();
22
+ function render(view, vars) {
23
+ return renderString(load(view), vars, view);
24
+ }
25
+ function renderString(template, vars, name = 'шаблон') {
26
+ const value = (key) => {
27
+ let current = vars;
28
+ for (const part of key.split('.')) {
29
+ if (current === null || typeof current !== 'object' || !(part in current)) {
30
+ throw new Error(`${name}: нет значения «${key}»`);
31
+ }
32
+ current = current[part];
33
+ }
34
+ return current;
35
+ };
36
+ return template
37
+ .replace(/\{\{([#^])([\w.]+)\}\}([\s\S]*?)\{\{\/\2\}\}/g, (_, kind, key, body) => (Boolean(value(key)) === (kind === '#') ? body : ''))
38
+ .replace(/\{\{json ([\w.]+)\}\}/g, (_, key) => scriptJson(value(key)))
39
+ .replace(/\{\{ ([\w.]+) \}\}/g, (_, key) => escapeHtml(String(value(key))));
40
+ }
41
+ function load(view) {
42
+ let template = cache.get(view);
43
+ if (template === undefined) {
44
+ template = node_fs_1.default.readFileSync(node_path_1.default.join(VIEWS, `${view}.html`), 'utf8');
45
+ cache.set(view, template);
46
+ }
47
+ return template;
48
+ }
49
+ function escapeHtml(text) {
50
+ return text.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
51
+ }
52
+ /** JSON внутри <script>: «</script>» в данных не должен закрыть тег. */
53
+ function scriptJson(data) {
54
+ return JSON.stringify(data ?? null)
55
+ .replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026')
56
+ .replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
57
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.WebLoginService = exports.COOKIE_MAX_AGE_MS = exports.COOKIE = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const drizzle_orm_1 = require("drizzle-orm");
18
+ const clock_1 = require("../common/clock");
19
+ const tokens_1 = require("../common/tokens");
20
+ const config_1 = require("../config/config");
21
+ const database_module_1 = require("../db/database.module");
22
+ const schema_1 = require("../db/schema");
23
+ const participants_service_1 = require("../participants/participants.service");
24
+ /** Cookie со входом браузера. Имя — как у PHP-сервера. */
25
+ exports.COOKIE = 'chat_token';
26
+ /** Сколько живёт ссылка, если ею не воспользовались. */
27
+ const LINK_TTL_MS = 15 * 60_000;
28
+ /** Сколько живёт вход в браузере: год — это рабочий инструмент, а не банк. */
29
+ exports.COOKIE_MAX_AGE_MS = 365 * 24 * 60 * 60_000;
30
+ /**
31
+ * Вход в страницы по одноразовой ссылке. Ссылку печатает консоль (team-chat login): кто
32
+ * может запустить команду на машине, тот и владелец. По ссылке браузер получает свой
33
+ * токен — отдельный на каждый вход, и выход отзывает только его.
34
+ */
35
+ let WebLoginService = class WebLoginService {
36
+ db;
37
+ config;
38
+ participants;
39
+ constructor(db, config, participants) {
40
+ this.db = db;
41
+ this.config = config;
42
+ this.participants = participants;
43
+ }
44
+ async link(participant) {
45
+ const code = (0, tokens_1.randomToken)(40);
46
+ await this.db.insert(schema_1.loginCodes).values({
47
+ codeHash: (0, tokens_1.hashToken)(code), participantId: participant.id, expiresAt: new Date((0, clock_1.now)().getTime() + LINK_TTL_MS),
48
+ });
49
+ return `${this.config.url}/login/${code}`;
50
+ }
51
+ /** Ссылка одноразовая: первый же переход её гасит. → свежий токен браузера или null. */
52
+ async consume(code) {
53
+ const [used] = await this.db.delete(schema_1.loginCodes)
54
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.loginCodes.codeHash, (0, tokens_1.hashToken)(code)), (0, drizzle_orm_1.gt)(schema_1.loginCodes.expiresAt, (0, clock_1.now)())))
55
+ .returning();
56
+ if (!used)
57
+ return null;
58
+ const [participant] = await this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.eq)(schema_1.participants.id, used.participantId));
59
+ if (!participant || participant.kind !== 'human')
60
+ return null;
61
+ return { participant, token: await this.participants.issueExtraToken(participant.id, `web:${(0, tokens_1.randomToken)(12)}`) };
62
+ }
63
+ /** Выход отзывает только токен этого браузера. */
64
+ async logout(token) {
65
+ await this.db.delete(schema_1.participantTokens).where((0, drizzle_orm_1.eq)(schema_1.participantTokens.tokenHash, (0, tokens_1.hashToken)(token)));
66
+ }
67
+ };
68
+ exports.WebLoginService = WebLoginService;
69
+ exports.WebLoginService = WebLoginService = __decorate([
70
+ (0, common_1.Injectable)(),
71
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
72
+ __param(1, (0, common_1.Inject)(config_1.SERVER_CONFIG)),
73
+ __metadata("design:paramtypes", [Object, Object, participants_service_1.ParticipantsService])
74
+ ], WebLoginService);