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,67 @@
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.AttachmentsController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const participant_guard_1 = require("../auth/participant.guard");
18
+ const errors_1 = require("../common/errors");
19
+ const attachment_store_1 = require("./attachment-store");
20
+ let AttachmentsController = class AttachmentsController {
21
+ store;
22
+ constructor(store) {
23
+ this.store = store;
24
+ }
25
+ /**
26
+ * Скачать вложение. Прямо в браузере показываем только растровые картинки: HTML или SVG,
27
+ * открытый на нашем домене, исполнил бы свои скрипты рядом с токеном владельца.
28
+ * Всё остальное — только скачивание.
29
+ */
30
+ async download(id, res) {
31
+ const attachment = await this.store.find(id);
32
+ if (!attachment)
33
+ throw (0, errors_1.notFound)();
34
+ const image = (0, attachment_store_1.isImage)(attachment);
35
+ res.set({
36
+ 'Content-Type': image ? attachment.mime : 'application/octet-stream',
37
+ 'X-Content-Type-Options': 'nosniff',
38
+ 'Content-Disposition': disposition(image ? 'inline' : 'attachment', attachment.name),
39
+ });
40
+ res.sendFile(this.store.absolute(attachment), { headers: { 'Content-Type': res.get('Content-Type') } });
41
+ }
42
+ };
43
+ exports.AttachmentsController = AttachmentsController;
44
+ __decorate([
45
+ (0, common_1.Get)(':id'),
46
+ __param(0, (0, common_1.Param)('id', common_1.ParseIntPipe)),
47
+ __param(1, (0, common_1.Res)()),
48
+ __metadata("design:type", Function),
49
+ __metadata("design:paramtypes", [Number, Object]),
50
+ __metadata("design:returntype", Promise)
51
+ ], AttachmentsController.prototype, "download", null);
52
+ exports.AttachmentsController = AttachmentsController = __decorate([
53
+ (0, common_1.Controller)('api/attachments'),
54
+ (0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
55
+ __metadata("design:paramtypes", [attachment_store_1.AttachmentStore])
56
+ ], AttachmentsController);
57
+ /**
58
+ * Content-Disposition как у Symfony (на нём отдавал файлы PHP-сервер): простое имя — как
59
+ * есть, сложное — в кавычках, не-ASCII — запасным именем и filename* в UTF-8.
60
+ */
61
+ function disposition(type, name) {
62
+ const fallback = name.replace(/[^\x20-\x7e]/g, '_').replace(/["\\]/g, '_');
63
+ const quoted = /^[A-Za-z0-9!#$&+.^_`|~-]+$/.test(fallback) ? fallback : `"${fallback}"`;
64
+ return fallback === name
65
+ ? `${type}; filename=${quoted}`
66
+ : `${type}; filename=${quoted}; filename*=utf-8''${encodeURIComponent(name)}`;
67
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ // Тип файла — по содержимому, а не по тому, что прислал браузер: от типа зависит, покажет
3
+ // ли браузер файл на нашем домене. Как fileinfo у PHP-сервера, в объёме, который нужен чату.
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.sniffMime = sniffMime;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const SNIFF_BYTES = 8192;
11
+ const SIGNATURES = [
12
+ ['image/png', b => b.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))],
13
+ ['image/jpeg', b => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff],
14
+ ['image/gif', b => /^GIF8[79]a/.test(b.toString('latin1', 0, 6))],
15
+ ['image/webp', b => b.toString('latin1', 0, 4) === 'RIFF' && b.toString('latin1', 8, 12) === 'WEBP'],
16
+ ['application/pdf', b => b.toString('latin1', 0, 5) === '%PDF-'],
17
+ ['application/zip', b => b.toString('latin1', 0, 4) === 'PK\x03\x04'],
18
+ ];
19
+ /** Тип по первым байтам файла; не узнали — бинарный. */
20
+ function sniffMime(file) {
21
+ const head = readHead(file);
22
+ for (const [mime, test] of SIGNATURES)
23
+ if (test(head))
24
+ return mime;
25
+ if (isText(head)) {
26
+ const text = head.toString('utf8').trimStart();
27
+ // SVG и HTML — документы со скриптами: их тип должен быть честным, чтобы их не показали как картинку.
28
+ if (/^(<\?xml[^>]*>\s*)?<svg[\s>]/i.test(text))
29
+ return 'image/svg+xml';
30
+ if (/^<!doctype html|^<html[\s>]/i.test(text))
31
+ return 'text/html';
32
+ if (/^[{[]/.test(text) && looksLikeJson(file))
33
+ return 'application/json';
34
+ return 'text/plain';
35
+ }
36
+ return 'application/octet-stream';
37
+ }
38
+ function readHead(file) {
39
+ const fd = node_fs_1.default.openSync(file, 'r');
40
+ try {
41
+ const buffer = Buffer.alloc(SNIFF_BYTES);
42
+ return buffer.subarray(0, node_fs_1.default.readSync(fd, buffer, 0, SNIFF_BYTES, 0));
43
+ }
44
+ finally {
45
+ node_fs_1.default.closeSync(fd);
46
+ }
47
+ }
48
+ /** Текст — это валидный UTF-8 без нулевых байтов. Обрезанный на границе символ не в счёт. */
49
+ function isText(head) {
50
+ if (head.includes(0))
51
+ return false;
52
+ try {
53
+ new TextDecoder('utf-8', { fatal: true }).decode(trimPartialChar(head));
54
+ return true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ function trimPartialChar(b) {
61
+ let end = b.length;
62
+ for (let i = 1; i <= 3 && i <= b.length; i++) {
63
+ const byte = b[b.length - i];
64
+ if ((byte & 0xc0) !== 0x80) {
65
+ const need = byte >= 0xf0 ? 4 : byte >= 0xe0 ? 3 : byte >= 0xc0 ? 2 : 1;
66
+ if (need > i)
67
+ end = b.length - i;
68
+ break;
69
+ }
70
+ }
71
+ return b.subarray(0, end);
72
+ }
73
+ /** JSON целиком разбираем только у небольших файлов: двадцать мегабайт ради типа — перебор. */
74
+ function looksLikeJson(file) {
75
+ if (node_fs_1.default.statSync(file).size > 1_000_000)
76
+ return false;
77
+ try {
78
+ JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
79
+ return true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
@@ -0,0 +1,72 @@
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 __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.UploadsInterceptor = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const multer_1 = __importDefault(require("multer"));
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const rxjs_1 = require("rxjs");
20
+ const errors_1 = require("../common/errors");
21
+ const attachment_store_1 = require("./attachment-store");
22
+ /** Поля с файлами: страница шлёт files[], клиенты попроще — files. */
23
+ const FIELDS = new Set(['files', 'files[]']);
24
+ /**
25
+ * Приём файлов сообщения (multipart). Сверх предела — 422 с текстом для поля files, как
26
+ * у Laravel, а не голое «слишком большой запрос». Принятые файлы лежат во временном
27
+ * каталоге, пока их не заберёт публикация; что осталось после запроса — удаляется.
28
+ */
29
+ let UploadsInterceptor = class UploadsInterceptor {
30
+ store;
31
+ constructor(store) {
32
+ this.store = store;
33
+ }
34
+ async intercept(context, next) {
35
+ const http = context.switchToHttp();
36
+ const request = http.getRequest();
37
+ if (!request.is('multipart/form-data'))
38
+ return next.handle();
39
+ const files = await this.receive(request, http.getResponse());
40
+ request.uploads = files
41
+ .filter(f => FIELDS.has(f.fieldname))
42
+ .map(f => ({ path: f.path, name: Buffer.from(f.originalname, 'latin1').toString('utf8'), size: f.size }));
43
+ return next.handle().pipe((0, rxjs_1.finalize)(() => {
44
+ for (const f of files)
45
+ node_fs_1.default.rmSync(f.path, { force: true });
46
+ }));
47
+ }
48
+ receive(request, response) {
49
+ const upload = (0, multer_1.default)({
50
+ dest: this.store.incoming,
51
+ limits: { fileSize: attachment_store_1.MAX_BYTES, files: attachment_store_1.MAX_FILES },
52
+ }).any();
53
+ return new Promise((resolve, reject) => {
54
+ upload(request, response, error => {
55
+ if (!error)
56
+ return resolve(request.files ?? []);
57
+ if (error instanceof multer_1.default.MulterError) {
58
+ const message = error.code === 'LIMIT_FILE_SIZE' ? 'Файл больше 20 МБ.'
59
+ : error.code === 'LIMIT_FILE_COUNT' ? `Не больше ${attachment_store_1.MAX_FILES} файлов в сообщении.`
60
+ : `Файлы не приняты: ${error.message}`;
61
+ return reject(errors_1.ValidationFailed.field('files', message));
62
+ }
63
+ reject(error);
64
+ });
65
+ });
66
+ }
67
+ };
68
+ exports.UploadsInterceptor = UploadsInterceptor;
69
+ exports.UploadsInterceptor = UploadsInterceptor = __decorate([
70
+ (0, common_1.Injectable)(),
71
+ __metadata("design:paramtypes", [attachment_store_1.AttachmentStore])
72
+ ], UploadsInterceptor);
@@ -0,0 +1,53 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Me = exports.ParticipantGuard = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const errors_1 = require("../common/errors");
15
+ const http_1 = require("../common/http");
16
+ const participants_service_1 = require("../participants/participants.service");
17
+ /**
18
+ * Участник по токену: `Authorization: Bearer <token>`. Сессий нет — человек со страницы
19
+ * и агенты из терминала ходят одним способом, один путь кода.
20
+ */
21
+ let ParticipantGuard = class ParticipantGuard {
22
+ participants;
23
+ constructor(participants) {
24
+ this.participants = participants;
25
+ }
26
+ async canActivate(context) {
27
+ const request = context.switchToHttp().getRequest();
28
+ const token = (0, http_1.bearerToken)(request);
29
+ const participant = token ? await this.participants.byToken(token) : null;
30
+ if (!participant)
31
+ throw (0, errors_1.unauthenticated)('Нужен заголовок Authorization: Bearer <token>.');
32
+ // Раннер — не собеседник: ему открыты только его пути. Иначе токен раннера давал бы
33
+ // писать в чат и читать его от имени служебного участника.
34
+ if ((0, participants_service_1.isHost)(participant) && !request.path.startsWith('/api/runner/')) {
35
+ throw (0, errors_1.forbidden)('Токен раннера годится только для /api/runner/*.');
36
+ }
37
+ await this.participants.touchSeen(participant.id);
38
+ request.participant = participant;
39
+ return true;
40
+ }
41
+ };
42
+ exports.ParticipantGuard = ParticipantGuard;
43
+ exports.ParticipantGuard = ParticipantGuard = __decorate([
44
+ (0, common_1.Injectable)(),
45
+ __metadata("design:paramtypes", [participants_service_1.ParticipantsService])
46
+ ], ParticipantGuard);
47
+ /** Участник запроса — после ParticipantGuard. */
48
+ exports.Me = (0, common_1.createParamDecorator)((_, context) => {
49
+ const participant = context.switchToHttp().getRequest().participant;
50
+ if (!participant)
51
+ throw new Error('@Me() без ParticipantGuard');
52
+ return participant;
53
+ });
@@ -0,0 +1,80 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.BridgeRestartService = void 0;
19
+ const common_1 = require("@nestjs/common");
20
+ const drizzle_orm_1 = require("drizzle-orm");
21
+ const node_fs_1 = __importDefault(require("node:fs"));
22
+ const node_path_1 = __importDefault(require("node:path"));
23
+ const clock_1 = require("../common/clock");
24
+ const format_1 = require("../common/format");
25
+ const config_1 = require("../config/config");
26
+ const database_module_1 = require("../db/database.module");
27
+ const schema_1 = require("../db/schema");
28
+ /**
29
+ * Перезапуск мостов по кнопке из чата. Сервер только просит: кладёт файл-запрос и больше
30
+ * ничего. Что перезапускать и чем, знает лишь машина с агентами, — иначе кнопка в
31
+ * браузере была бы удалённым запуском команд на ней. Забрав запрос, машина удаляет файл;
32
+ * файл, висящий долго, значит, что забирать некому.
33
+ */
34
+ let BridgeRestartService = class BridgeRestartService {
35
+ db;
36
+ config;
37
+ constructor(db, config) {
38
+ this.db = db;
39
+ this.config = config;
40
+ }
41
+ request(by) {
42
+ const file = this.config.bridgeRestartFile;
43
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
44
+ node_fs_1.default.writeFileSync(file, JSON.stringify({ requested_at: (0, format_1.iso)((0, clock_1.now)()), by: by.handle }));
45
+ }
46
+ /** Когда запрошен перезапуск, который машина ещё не забрала. */
47
+ pendingSince() {
48
+ let raw;
49
+ try {
50
+ raw = node_fs_1.default.readFileSync(this.config.bridgeRestartFile, 'utf8');
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ try {
56
+ const at = new Date(JSON.parse(raw).requested_at);
57
+ if (!Number.isNaN(at.getTime()))
58
+ return at;
59
+ }
60
+ catch { /* не JSON — время по файлу */ }
61
+ return node_fs_1.default.statSync(this.config.bridgeRestartFile).mtime;
62
+ }
63
+ /** Для страницы: когда каждый агент последний раз выходил на связь и не висит ли запрос. */
64
+ async status() {
65
+ const agents = await this.db.select().from(schema_1.participants)
66
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.participants.kind, 'ai'), (0, drizzle_orm_1.isNull)(schema_1.participants.disabledAt)))
67
+ .orderBy((0, drizzle_orm_1.asc)(schema_1.participants.id));
68
+ return {
69
+ restart_requested_at: (0, format_1.iso)(this.pendingSince()),
70
+ agents: agents.map(a => ({ handle: a.handle, name: a.name, accent: a.accent, seen_at: (0, format_1.iso)(a.lastSeenAt) })),
71
+ };
72
+ }
73
+ };
74
+ exports.BridgeRestartService = BridgeRestartService;
75
+ exports.BridgeRestartService = BridgeRestartService = __decorate([
76
+ (0, common_1.Injectable)(),
77
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
78
+ __param(1, (0, common_1.Inject)(config_1.SERVER_CONFIG)),
79
+ __metadata("design:paramtypes", [Object, Object])
80
+ ], BridgeRestartService);
@@ -0,0 +1,47 @@
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.BridgesController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const participant_guard_1 = require("../auth/participant.guard");
18
+ const errors_1 = require("../common/errors");
19
+ const participants_service_1 = require("../participants/participants.service");
20
+ const bridge_restart_service_1 = require("./bridge-restart.service");
21
+ let BridgesController = class BridgesController {
22
+ bridges;
23
+ constructor(bridges) {
24
+ this.bridges = bridges;
25
+ }
26
+ /** Попросить машину перезапустить мосты. Только владелец: перезапуск обрывает агента на полуслове. */
27
+ async restart(me) {
28
+ if (!(0, participants_service_1.isOwner)(me))
29
+ throw (0, errors_1.forbidden)('Перезапускать мосты может только владелец.');
30
+ this.bridges.request(me);
31
+ return { bridges: await this.bridges.status() };
32
+ }
33
+ };
34
+ exports.BridgesController = BridgesController;
35
+ __decorate([
36
+ (0, common_1.Post)('restart'),
37
+ (0, common_1.HttpCode)(202),
38
+ __param(0, (0, participant_guard_1.Me)()),
39
+ __metadata("design:type", Function),
40
+ __metadata("design:paramtypes", [Object]),
41
+ __metadata("design:returntype", Promise)
42
+ ], BridgesController.prototype, "restart", null);
43
+ exports.BridgesController = BridgesController = __decorate([
44
+ (0, common_1.Controller)('api/bridges'),
45
+ (0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
46
+ __metadata("design:paramtypes", [bridge_restart_service_1.BridgeRestartService])
47
+ ], BridgesController);
@@ -0,0 +1,83 @@
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.ChatsController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const participant_guard_1 = require("../auth/participant.guard");
18
+ const errors_1 = require("../common/errors");
19
+ const mentions_service_1 = require("../mentions/mentions.service");
20
+ const messages_service_1 = require("../messages/messages.service");
21
+ const participants_service_1 = require("../participants/participants.service");
22
+ const chats_service_1 = require("./chats.service");
23
+ /**
24
+ * Чаты-проекты. Заводит и правит их только владелец: описание и каталог проекта уходят
25
+ * агенту в каждый запуск, то есть это его инструкции.
26
+ */
27
+ let ChatsController = class ChatsController {
28
+ chats;
29
+ mentions;
30
+ messages;
31
+ constructor(chats, mentions, messages) {
32
+ this.chats = chats;
33
+ this.mentions = mentions;
34
+ this.messages = messages;
35
+ }
36
+ async index() {
37
+ const [activity, ownerWait] = await Promise.all([this.mentions.snapshot(), this.messages.awaitingOwner()]);
38
+ return { chats: await this.chats.summaries({ activity, ownerWait }) };
39
+ }
40
+ async store(me, body) {
41
+ ownerOnly(me);
42
+ return { chat: (0, chats_service_1.chatApi)(await this.chats.create(body)) };
43
+ }
44
+ async update(me, slug, body) {
45
+ ownerOnly(me);
46
+ return { chat: (0, chats_service_1.chatApi)(await this.chats.update(await this.chats.bySlug(slug), body)) };
47
+ }
48
+ };
49
+ exports.ChatsController = ChatsController;
50
+ __decorate([
51
+ (0, common_1.Get)(),
52
+ __metadata("design:type", Function),
53
+ __metadata("design:paramtypes", []),
54
+ __metadata("design:returntype", Promise)
55
+ ], ChatsController.prototype, "index", null);
56
+ __decorate([
57
+ (0, common_1.Post)(),
58
+ __param(0, (0, participant_guard_1.Me)()),
59
+ __param(1, (0, common_1.Body)()),
60
+ __metadata("design:type", Function),
61
+ __metadata("design:paramtypes", [Object, Object]),
62
+ __metadata("design:returntype", Promise)
63
+ ], ChatsController.prototype, "store", null);
64
+ __decorate([
65
+ (0, common_1.Patch)(':chat'),
66
+ __param(0, (0, participant_guard_1.Me)()),
67
+ __param(1, (0, common_1.Param)('chat')),
68
+ __param(2, (0, common_1.Body)()),
69
+ __metadata("design:type", Function),
70
+ __metadata("design:paramtypes", [Object, String, Object]),
71
+ __metadata("design:returntype", Promise)
72
+ ], ChatsController.prototype, "update", null);
73
+ exports.ChatsController = ChatsController = __decorate([
74
+ (0, common_1.Controller)('api/chats'),
75
+ (0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
76
+ __metadata("design:paramtypes", [chats_service_1.ChatsService,
77
+ mentions_service_1.MentionsService,
78
+ messages_service_1.MessagesService])
79
+ ], ChatsController);
80
+ function ownerOnly(me) {
81
+ if (!(0, participants_service_1.isOwner)(me))
82
+ throw (0, errors_1.forbidden)('Чаты заводит и настраивает только владелец.');
83
+ }
@@ -0,0 +1,128 @@
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.ChatsService = exports.chatApi = exports.SLUG_PATTERN = 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 errors_1 = require("../common/errors");
20
+ const slug_1 = require("../common/slug");
21
+ const validation_1 = require("../common/validation");
22
+ const database_module_1 = require("../db/database.module");
23
+ const schema_1 = require("../db/schema");
24
+ const directory_service_1 = require("../participants/directory.service");
25
+ const participants_service_1 = require("../participants/participants.service");
26
+ exports.SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,39}$/;
27
+ /** Что видят клиенты о чате. */
28
+ const chatApi = (c) => ({ slug: c.slug, name: c.name, description: c.description, workdir: c.workdir });
29
+ exports.chatApi = chatApi;
30
+ const settings = {
31
+ description: (0, validation_1.optionalText)(5000),
32
+ // Путь на машине с мостами. Есть ли такой каталог, сервер проверить не может — проверяет мост.
33
+ workdir: zod_1.z.string().max(500).startsWith('/', { error: 'Каталог проекта — абсолютный путь' }).nullish(),
34
+ };
35
+ const createSchema = zod_1.z.object({
36
+ name: zod_1.z.string().max(100),
37
+ slug: zod_1.z.string().regex(exports.SLUG_PATTERN, { error: 'Адрес чата — латиница, цифры и дефис, до 40 знаков.' }),
38
+ ...settings,
39
+ });
40
+ const updateSchema = zod_1.z.object({
41
+ name: zod_1.z.string().max(100).optional(),
42
+ // Адрес чата не меняется: на него ссылаются закладки и упоминания в тексте.
43
+ slug: zod_1.z.undefined({ error: 'Адрес чата не меняется.' }).optional(),
44
+ ...settings,
45
+ });
46
+ /** Чаты-проекты: у каждого свой тред, журнал решений, документы и контекст агента. */
47
+ let ChatsService = class ChatsService {
48
+ db;
49
+ directory;
50
+ constructor(db, directory) {
51
+ this.db = db;
52
+ this.directory = directory;
53
+ }
54
+ all() {
55
+ return this.db.select().from(schema_1.chats).orderBy((0, drizzle_orm_1.asc)(schema_1.chats.id));
56
+ }
57
+ async bySlug(slug) {
58
+ const [chat] = await this.db.select().from(schema_1.chats).where((0, drizzle_orm_1.eq)(schema_1.chats.slug, slug));
59
+ if (!chat)
60
+ throw (0, errors_1.notFound)(`Чата «${slug}» нет.`);
61
+ return chat;
62
+ }
63
+ /**
64
+ * Чат запроса: из адреса /api/chats/{chat}/…, а без него — самый первый. Пути без чата
65
+ * остались от времени, когда чат был один; агенты и скрипты на них ломать незачем.
66
+ */
67
+ async resolve(slug) {
68
+ if (slug)
69
+ return this.bySlug(slug);
70
+ const [first] = await this.db.select().from(schema_1.chats).orderBy((0, drizzle_orm_1.asc)(schema_1.chats.id)).limit(1);
71
+ if (!first)
72
+ throw (0, errors_1.notFound)('Чатов ещё нет.');
73
+ return first;
74
+ }
75
+ async create(input) {
76
+ const raw = (input ?? {});
77
+ // Адрес из названия, если не задан: «Kettu Driver» → kettu-driver.
78
+ const data = (0, validation_1.validate)(createSchema, { ...raw, slug: raw.slug || (0, slug_1.slugify)(String(raw.name ?? '')) });
79
+ if (await this.exists(data.slug))
80
+ throw errors_1.ValidationFailed.field('slug', 'Чат с таким адресом уже есть.');
81
+ const [chat] = await this.db.insert(schema_1.chats).values({
82
+ slug: data.slug, name: data.name, description: data.description ?? null, workdir: data.workdir ?? null,
83
+ }).returning();
84
+ return chat;
85
+ }
86
+ async update(chat, input) {
87
+ const data = (0, validation_1.validate)(updateSchema, input);
88
+ const changes = Object.fromEntries(Object.entries({ name: data.name, description: data.description, workdir: data.workdir })
89
+ .filter(([, v]) => v !== undefined));
90
+ if (Object.keys(changes).length === 0)
91
+ return chat;
92
+ const [updated] = await this.db.update(schema_1.chats).set(changes).where((0, drizzle_orm_1.eq)(schema_1.chats.id, chat.id)).returning();
93
+ return updated;
94
+ }
95
+ /**
96
+ * Сводка по чатам для боковой панели: команда чата, последнее сообщение и сколько
97
+ * где ждут ответа. Команду же получает агент в задании.
98
+ */
99
+ async summaries({ activity, ownerWait }) {
100
+ const [all, last, members, directory] = await Promise.all([
101
+ this.all(),
102
+ this.db.select({ chatId: schema_1.messages.chatId, lastId: (0, drizzle_orm_1.max)(schema_1.messages.id) }).from(schema_1.messages).groupBy(schema_1.messages.chatId),
103
+ this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.isNull)(schema_1.participants.disabledAt)).orderBy((0, drizzle_orm_1.asc)(schema_1.participants.id)),
104
+ this.directory.load(),
105
+ ]);
106
+ const lastByChat = new Map(last.map(r => [r.chatId, r.lastId ?? 0]));
107
+ return all.map(chat => ({
108
+ ...(0, exports.chatApi)(chat),
109
+ team: members.filter(participants_service_1.isMember).map(p => {
110
+ const role = directory.roleIn(p, chat.id);
111
+ return { handle: p.handle, name: p.name, kind: p.kind, role, role_name: directory.roleName(role) };
112
+ }),
113
+ last_message_id: lastByChat.get(chat.id) ?? 0,
114
+ owner_wait: ownerWait.filter(w => w.chat === chat.slug).length,
115
+ agents_open: activity.filter(a => a.chat === chat.slug).length,
116
+ }));
117
+ }
118
+ async exists(slug) {
119
+ const [row] = await this.db.select({ id: schema_1.chats.id }).from(schema_1.chats).where((0, drizzle_orm_1.eq)(schema_1.chats.slug, slug));
120
+ return Boolean(row);
121
+ }
122
+ };
123
+ exports.ChatsService = ChatsService;
124
+ exports.ChatsService = ChatsService = __decorate([
125
+ (0, common_1.Injectable)(),
126
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
127
+ __metadata("design:paramtypes", [Object, directory_service_1.DirectoryService])
128
+ ], ChatsService);