team-chat 0.0.0-stage → 0.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.
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 +67 -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,282 @@
1
+ "use strict";
2
+ // Инструменты MCP — те же сервисы, что у HTTP: правило живёт в одном месте. Имена,
3
+ // описания и аргументы — как были у PHP-сервера (laravel/mcp): агенты на них опираются.
4
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
5
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
6
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
7
+ 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;
8
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
9
+ };
10
+ var __metadata = (this && this.__metadata) || function (k, v) {
11
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
12
+ };
13
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
14
+ return function (target, key) { decorator(target, key, paramIndex); }
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.ChatTools = void 0;
21
+ const common_1 = require("@nestjs/common");
22
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
23
+ const drizzle_orm_1 = require("drizzle-orm");
24
+ const node_fs_1 = __importDefault(require("node:fs"));
25
+ const zod_1 = require("zod");
26
+ const attachment_store_1 = require("../attachments/attachment-store");
27
+ const chats_service_1 = require("../chats/chats.service");
28
+ const database_module_1 = require("../db/database.module");
29
+ const schema_1 = require("../db/schema");
30
+ const decisions_service_1 = require("../decisions/decisions.service");
31
+ const documents_service_1 = require("../documents/documents.service");
32
+ const knowledge_service_1 = require("../knowledge/knowledge.service");
33
+ const mentions_service_1 = require("../mentions/mentions.service");
34
+ const message_presenter_1 = require("../messages/message.presenter");
35
+ const messages_service_1 = require("../messages/messages.service");
36
+ const directory_service_1 = require("../participants/directory.service");
37
+ const participants_service_1 = require("../participants/participants.service");
38
+ const INSTRUCTIONS = `Командный чат: владелец и агенты обсуждают работу. Чатов несколько — по одному
39
+ на проект (list-chats). У каждого свой тред, журнал решений и документы, поэтому
40
+ инструментам передавай chat — slug чата из задания. Отвечая с reply_to_id, chat
41
+ можно не указывать: ответ уходит в чат вопроса.
42
+
43
+ Две сущности, и путать их не следует:
44
+ * ТРЕД — разговор. Здесь можно сомневаться, спорить и передумывать.
45
+ * ЖУРНАЛ РЕШЕНИЙ — только утверждённое владельцем. Правке не подлежит: решение
46
+ отменяется выпуском нового. Фиксировать решения может ТОЛЬКО владелец,
47
+ поэтому инструмента для этого здесь нет — предлагай сообщением в треде.
48
+
49
+ С чего начинать: read_thread отдаёт журнал решений целиком (он маленький и
50
+ авторитетный) плюс окно последних сообщений. Всю историю тащить не нужно —
51
+ за старым иди в search_thread.
52
+
53
+ ДОКУМЕНТЫ проекта (планы, аудиты, хендоффы, отчёты) живут здесь же, а не в
54
+ репозиториях: list-documents, read-document, write-document. В репозиториях остаётся
55
+ только то, что читает машина (CLAUDE.md, AGENTS.md) и что описывает код. Смотри на
56
+ status документа: current — актуально, draft — не подтверждено, archived — протухло.
57
+
58
+ ОПЫТ КОМАНДЫ — уроки прошлых запусков агентов: в чём была причина, что сработало,
59
+ где был тупик. Берёшься за задачу, похожую на уже решённую, — find-knowledge.
60
+
61
+ Отвечать положено, когда тебя упомянули: pending_mentions покажет адресованное
62
+ тебе. После опубликованного ответа подтверди обращение через acknowledge_mention,
63
+ иначе оно останется висеть.`;
64
+ const CHAT_ARG = 'Slug чата-проекта — из задания или list-chats. У каждого проекта свой тред, журнал решений и документы.';
65
+ /** Ошибка инструмента: агент видит её текстом и сам решает, что делать дальше. */
66
+ class ToolError extends Error {
67
+ }
68
+ const json = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data) }] });
69
+ const text = (value) => ({ content: [{ type: 'text', text: value }] });
70
+ /**
71
+ * Сервер MCP для одного участника — на каждый запрос свой: инструменты действуют от
72
+ * имени того, чей токен пришёл. Решения фиксировать нечем — так задумано: это право
73
+ * владельца, и надёжнее отсутствие инструмента, чем запрет в его описании.
74
+ */
75
+ let ChatTools = class ChatTools {
76
+ db;
77
+ chats;
78
+ messages;
79
+ mentions;
80
+ decisions;
81
+ documents;
82
+ knowledge;
83
+ attachments;
84
+ presenter;
85
+ directories;
86
+ constructor(db, chats, messages, mentions, decisions, documents, knowledge, attachments, presenter, directories) {
87
+ this.db = db;
88
+ this.chats = chats;
89
+ this.messages = messages;
90
+ this.mentions = mentions;
91
+ this.decisions = decisions;
92
+ this.documents = documents;
93
+ this.knowledge = knowledge;
94
+ this.attachments = attachments;
95
+ this.presenter = presenter;
96
+ this.directories = directories;
97
+ }
98
+ serverFor(me) {
99
+ const server = new mcp_js_1.McpServer({ name: 'Team Chat', version: '1.0.0' }, { instructions: INSTRUCTIONS });
100
+ // Типы SDK выводят обработчик из схемы каждого инструмента; общей обёртке это не по силам,
101
+ // поэтому регистрация — через упрощённую сигнатуру. Аргументы проверяет сам SDK по схеме.
102
+ const register = server.registerTool.bind(server);
103
+ const tool = (name, title, description, inputSchema, handler) => register(name, { title, description, inputSchema }, args => guarded(() => handler(args)));
104
+ tool('list-chats', 'List Chats', 'Чаты-проекты: slug, название, описание проекта и каталог с его кодом. У каждого чата свой тред, '
105
+ + 'журнал решений и документы — остальным инструментам передавай chat.', {}, async () => json({ chats: (await this.chats.all()).map(chats_service_1.chatApi) }));
106
+ tool('read-thread', 'Read Thread', 'Ввести себя в курс дела: журнал решений целиком плюс окно последних сообщений треда и список '
107
+ + 'участников. С этого начинают, прежде чем отвечать.', {
108
+ chat: zod_1.z.string().optional().describe(CHAT_ARG),
109
+ limit: zod_1.z.number().int().optional().describe('Сколько последних сообщений вернуть (1–200, по умолчанию 50).'),
110
+ }, async ({ chat: slug, limit }) => {
111
+ const chat = await this.resolveChat(slug);
112
+ const [members, directory] = await Promise.all([
113
+ this.db.select().from(schema_1.participants).where((0, drizzle_orm_1.ne)(schema_1.participants.kind, participants_service_1.KIND_HOST)).orderBy((0, drizzle_orm_1.asc)(schema_1.participants.id)),
114
+ this.directories.load(),
115
+ ]);
116
+ return json({
117
+ chat: (0, chats_service_1.chatApi)(chat),
118
+ you: me.handle,
119
+ participants: members.map(p => ({ handle: p.handle, name: p.name, role: directory.roleIn(p, chat.id), disabled: p.disabledAt !== null })),
120
+ decisions: await this.decisions.list(chat),
121
+ messages: await this.messages.window(chat, Math.max(1, Math.min(200, limit || 50))),
122
+ });
123
+ });
124
+ tool('post-message', 'Post Message', 'Написать в тред. Упоминание @handle адресует сообщение участнику: @claude, @codex, @javlon или @all. '
125
+ + 'Упоминай, только если действительно нужен ответ.', {
126
+ chat: zod_1.z.string().optional().describe('Slug чата-проекта. Отвечая с reply_to_id, можно не указывать — возьмётся чат вопроса.'),
127
+ body: zod_1.z.string().describe('Текст сообщения.'),
128
+ reply_to_id: zod_1.z.number().int().optional().describe('Id сообщения, на которое отвечаешь. Отвечая на обращение, указывай его обязательно.'),
129
+ ask_owner: zod_1.z.boolean().optional().describe('true — в сообщении вопрос владельцу, без ответа на который работа не продолжится. Попадёт в список '
130
+ + '«Нужен ответ владельца» и будет висеть, пока владелец не ответит именно на это сообщение. Упоминание '
131
+ + '@javlon в тексте такой пометки не заменяет. Не ставь для отчётов, предложений и риторических вопросов.'),
132
+ }, async ({ chat: slug, body, reply_to_id, ask_owner }) => {
133
+ // Ответ живёт в чате вопроса — chat тогда можно не указывать. Указан и не совпадает —
134
+ // это ошибка агента, а не повод писать в чужой проект.
135
+ const question = reply_to_id === undefined ? null : await this.message(reply_to_id);
136
+ const chat = question && !slug ? await this.chatById(question.chatId) : await this.resolveChat(slug);
137
+ if (question && question.chatId !== chat.id) {
138
+ const questionChat = await this.chatById(question.chatId);
139
+ throw new ToolError(`Сообщение #${question.id} — в чате «${questionChat.slug}», а не «${chat.slug}». Отвечай в чате вопроса.`);
140
+ }
141
+ const message = await this.messages.publish(chat, me, { body, reply_to_id, ask_owner });
142
+ return json(await this.presenter.presentOne(message));
143
+ });
144
+ tool('list-decisions', 'List Decisions', 'Журнал принятых решений. Это авторитетная память проекта: решения неизменяемы, отменённое помечено '
145
+ + 'флагом superseded. Не спорь с зафиксированным — если не согласен, предложи в треде выпустить новое '
146
+ + 'решение взамен старого.', { chat: zod_1.z.string().optional().describe(CHAT_ARG) }, async ({ chat: slug }) => json({
147
+ decisions: await this.decisions.list(await this.resolveChat(slug)),
148
+ note: 'Фиксировать решения может только владелец.',
149
+ }));
150
+ tool('search-thread', 'Search Thread', 'Поиск по треду. Нужен, чтобы достать старое обсуждение, не затаскивая в контекст всю историю ради пары фактов.', {
151
+ chat: zod_1.z.string().optional().describe(CHAT_ARG),
152
+ query: zod_1.z.string().min(2).describe('Подстрока для поиска по тексту сообщений.'),
153
+ }, async ({ chat: slug, query }) => {
154
+ const found = await this.messages.search(await this.resolveChat(slug), { q: query }, 50);
155
+ return json({ count: found.length, messages: found });
156
+ });
157
+ tool('pending-mentions', 'Pending Mentions', 'Обращения, адресованные лично тебе и пока не обработанные. Чтение обращение НЕ гасит — после '
158
+ + 'опубликованного ответа подтверди его acknowledge_mention.', {}, async () => {
159
+ const open = await this.mentions.openFor(me, 50);
160
+ return json({ count: open.length, mentions: open });
161
+ });
162
+ tool('acknowledge-mention', 'Acknowledge Mention', 'Подтвердить, что обращение обработано. Вызывать ТОЛЬКО после того, как ответ действительно '
163
+ + 'опубликован в треде: иначе вопрос исчезнет из очереди без ответа.', { mention_id: zod_1.z.number().int().describe('Id обращения из pending_mentions.') }, async ({ mention_id }) => json(await this.mentions.acknowledge(me, mention_id)));
164
+ tool('list-documents', 'List Documents', 'Документы проекта: планы, аудиты, хендоффы, отчёты. Тела не отдаёт — только список, чтобы не тащить '
165
+ + 'в контекст лишнее. Смотри на status: current — актуально, draft — не подтверждено, archived — '
166
+ + 'протухло, ссылаться нельзя.', {
167
+ chat: zod_1.z.string().optional().describe(CHAT_ARG),
168
+ status: zod_1.z.enum(documents_service_1.STATUSES).optional().describe('Фильтр по состоянию: draft, current, archived.'),
169
+ kind: zod_1.z.enum(documents_service_1.KINDS).optional().describe('Фильтр по типу: plan, audit, handoff, report, reference.'),
170
+ }, async ({ chat: slug, status, kind }) => {
171
+ const documents = await this.documents.list(await this.resolveChat(slug), { status, kind });
172
+ return json({ count: documents.length, documents });
173
+ });
174
+ tool('read-document', 'Read Document', 'Прочитать документ целиком по slug из list-documents.', {
175
+ chat: zod_1.z.string().optional().describe(CHAT_ARG),
176
+ slug: zod_1.z.string().describe('Идентификатор документа.'),
177
+ }, async ({ chat: slug, slug: document }) => {
178
+ const found = await this.documents.read(await this.resolveChat(slug), document);
179
+ if (!found)
180
+ throw new ToolError(`Документа «${document}» нет. Список — в list-documents.`);
181
+ return json(found);
182
+ });
183
+ tool('read-attachment', 'Read Attachment', 'Открыть файл, приложенный к сообщению: id — из поля attachments сообщения. Картинку вернёт '
184
+ + 'изображением, небольшой текстовый файл — текстом. Остальное (PDF, архивы, таблицы) скачивается '
185
+ + 'по url из attachments с твоим токеном: curl -H "Authorization: Bearer $CHAT_TOKEN" -o <файл> <url>.', { id: zod_1.z.number().int().describe('Id вложения из поля attachments сообщения.') }, async ({ id }) => {
186
+ const attachment = await this.attachments.find(id);
187
+ if (!attachment)
188
+ throw new ToolError(`Вложения #${id} нет.`);
189
+ const file = this.attachments.absolute(attachment);
190
+ if ((0, attachment_store_1.isImage)(attachment)) {
191
+ return { content: [{ type: 'image', data: node_fs_1.default.readFileSync(file).toString('base64'), mimeType: attachment.mime }] };
192
+ }
193
+ if ((0, attachment_store_1.isText)(attachment))
194
+ return text(node_fs_1.default.readFileSync(file, 'utf8'));
195
+ const url = this.presenter.attachmentApi(attachment).url;
196
+ return text(`«${attachment.name}» (${attachment.mime}, ${attachment.size} байт) напрямую не читается. `
197
+ + `Скачай: curl -H "Authorization: Bearer $CHAT_TOKEN" -o '${attachment.name}' ${url}`);
198
+ });
199
+ tool('write-document', 'Write Document', 'Создать документ или добавить новую редакцию существующему. Прошлые редакции не переписываются — '
200
+ + 'они остаются в истории. Статус current ставит только владелец: агент пишет черновик, актуальность '
201
+ + 'подтверждает человек.', {
202
+ chat: zod_1.z.string().optional().describe(CHAT_ARG),
203
+ slug: zod_1.z.string().describe('Идентификатор; существующий — добавит редакцию.'),
204
+ title: zod_1.z.string().describe('Заголовок.'),
205
+ body: zod_1.z.string().describe('Текст документа (markdown).'),
206
+ kind: zod_1.z.enum(documents_service_1.KINDS).describe('Тип документа.'),
207
+ status: zod_1.z.enum(documents_service_1.STATUSES).optional().describe('По умолчанию draft.'),
208
+ note: zod_1.z.string().optional().describe('Чем вызвана правка.'),
209
+ path: zod_1.z.string().optional().describe('Путь в репозитории для выгрузки, напр. qurbuz-backend/docs/plan.md. Выгружаются только документы со статусом current.'),
210
+ }, async ({ chat: slug, ...document }) => json(await this.documents.writeFromAgent(await this.resolveChat(slug), me, document)));
211
+ tool('find-knowledge', 'Find Knowledge', 'Опыт команды: уроки прошлых запусков агентов, похожие на запрос, — симптом, причина, что сработало, '
212
+ + 'тупики и правило. Ищет по всем проектам (урок обобщён), уроки своего проекта выше. Опиши задачу или '
213
+ + 'ошибку словами — поиск по словам, не по смыслу. Урок — подсказка: сверь его с задачей.', {
214
+ query: zod_1.z.string().min(2).describe('Задача, симптом или ошибка — словами, можно с названиями технологий.'),
215
+ chat: zod_1.z.string().optional().describe('Slug чата-проекта: его уроки поднимутся выше.'),
216
+ category: zod_1.z.string().optional().describe(`Только уроки этого типа задач: ${knowledge_service_1.CATEGORIES}.`),
217
+ limit: zod_1.z.number().int().optional().describe('Сколько уроков вернуть (1–20, по умолчанию 5).'),
218
+ }, async ({ query, chat: slug, category, limit }) => {
219
+ const chat = slug ? await this.resolveChat(slug) : undefined;
220
+ const lessons = await this.knowledge.search(query, { chat, limit: limit ?? 5, category });
221
+ return json({ count: lessons.length, lessons });
222
+ });
223
+ return server;
224
+ }
225
+ /**
226
+ * Чат из аргумента. Без него — единственный чат, пока он один. Когда чатов несколько,
227
+ * молча подставить первый нельзя: агент ответил бы в чужой проект и решил бы, что всё
228
+ * в порядке. Лучше ошибка со списком.
229
+ */
230
+ async resolveChat(slug) {
231
+ const all = await this.chats.all();
232
+ const list = () => all.map(c => `${c.slug} (${c.name})`).join(', ');
233
+ if (slug) {
234
+ const chat = all.find(c => c.slug === slug);
235
+ if (!chat)
236
+ throw new ToolError(`Чата «${slug}» нет. Есть: ${list()}.`);
237
+ return chat;
238
+ }
239
+ if (all.length === 1)
240
+ return all[0];
241
+ throw new ToolError(`Чатов несколько — укажи chat (slug чата из задания). Есть: ${list()}.`);
242
+ }
243
+ async message(id) {
244
+ const [row] = await this.db.select().from(schema_1.messages).where((0, drizzle_orm_1.eq)(schema_1.messages.id, id));
245
+ if (!row)
246
+ throw new ToolError(`Сообщения #${id} нет.`);
247
+ return row;
248
+ }
249
+ async chatById(id) {
250
+ return (await this.chats.all()).find(c => c.id === id);
251
+ }
252
+ };
253
+ exports.ChatTools = ChatTools;
254
+ exports.ChatTools = ChatTools = __decorate([
255
+ (0, common_1.Injectable)(),
256
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
257
+ __metadata("design:paramtypes", [Object, chats_service_1.ChatsService,
258
+ messages_service_1.MessagesService,
259
+ mentions_service_1.MentionsService,
260
+ decisions_service_1.DecisionsService,
261
+ documents_service_1.DocumentsService,
262
+ knowledge_service_1.KnowledgeService,
263
+ attachment_store_1.AttachmentStore,
264
+ message_presenter_1.MessagePresenter,
265
+ directory_service_1.DirectoryService])
266
+ ], ChatTools);
267
+ /** Ошибки правил (нет прав, неверные данные) — ответом инструмента с isError, а не сбоем протокола. */
268
+ async function guarded(run) {
269
+ try {
270
+ return await run();
271
+ }
272
+ catch (e) {
273
+ if (e instanceof ToolError)
274
+ return { isError: true, content: [{ type: 'text', text: e.message }] };
275
+ if (e instanceof common_1.HttpException) {
276
+ const response = e.getResponse();
277
+ const message = typeof response === 'string' ? response : response.message ?? e.message;
278
+ return { isError: true, content: [{ type: 'text', text: message }] };
279
+ }
280
+ throw e;
281
+ }
282
+ }
@@ -0,0 +1,58 @@
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.McpController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
18
+ const participant_guard_1 = require("../auth/participant.guard");
19
+ const chat_tools_1 = require("./chat-tools");
20
+ /**
21
+ * MCP-сервер чата — тем же входом, что и HTTP API: адрес плюс токен.
22
+ *
23
+ * claude mcp add --transport http qbazar-chat http://localhost:8600/mcp --header "Authorization: Bearer <токен>"
24
+ * codex mcp add qbazar-chat --url http://localhost:8600/mcp --bearer-token-env-var QBAZAR_CHAT_TOKEN
25
+ *
26
+ * Без сессий: каждый запрос самодостаточен, ответ — обычный JSON, как у PHP-сервера.
27
+ */
28
+ let McpController = class McpController {
29
+ tools;
30
+ constructor(tools) {
31
+ this.tools = tools;
32
+ }
33
+ async handle(me, req, res) {
34
+ const server = this.tools.serverFor(me);
35
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
36
+ res.on('close', () => {
37
+ void transport.close();
38
+ void server.close();
39
+ });
40
+ await server.connect(transport);
41
+ await transport.handleRequest(req, res, req.body);
42
+ }
43
+ };
44
+ exports.McpController = McpController;
45
+ __decorate([
46
+ (0, common_1.All)(),
47
+ __param(0, (0, participant_guard_1.Me)()),
48
+ __param(1, (0, common_1.Req)()),
49
+ __param(2, (0, common_1.Res)()),
50
+ __metadata("design:type", Function),
51
+ __metadata("design:paramtypes", [Object, Object, Object]),
52
+ __metadata("design:returntype", Promise)
53
+ ], McpController.prototype, "handle", null);
54
+ exports.McpController = McpController = __decorate([
55
+ (0, common_1.Controller)('mcp'),
56
+ (0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
57
+ __metadata("design:paramtypes", [chat_tools_1.ChatTools])
58
+ ], McpController);
@@ -0,0 +1,24 @@
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.McpModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const conversation_module_1 = require("../conversation.module");
12
+ const knowledge_module_1 = require("../knowledge/knowledge.module");
13
+ const chat_tools_1 = require("./chat-tools");
14
+ const mcp_controller_1 = require("./mcp.controller");
15
+ let McpModule = class McpModule {
16
+ };
17
+ exports.McpModule = McpModule;
18
+ exports.McpModule = McpModule = __decorate([
19
+ (0, common_1.Module)({
20
+ imports: [conversation_module_1.ConversationModule, knowledge_module_1.KnowledgeModule],
21
+ providers: [chat_tools_1.ChatTools],
22
+ controllers: [mcp_controller_1.McpController],
23
+ })
24
+ ], McpModule);
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ // Состояние обработки обращения — выводится из фактов при каждом чтении и нигде не
3
+ // хранится готовой надписью. Иначе упавший мост оставил бы «работает» навсегда, то есть
4
+ // соврал бы ровно тогда, когда правда нужнее всего.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BEAT_TTL = exports.REPORTS = void 0;
7
+ exports.beatFresh = beatFresh;
8
+ exports.stateOf = stateOf;
9
+ exports.stateSince = stateSince;
10
+ exports.beat = beat;
11
+ exports.finish = finish;
12
+ /**
13
+ * Чем мост отчитывается. «working» продлевает сигнал жизни, остальное — исходы и отказ
14
+ * запускать по бюджету. «Ответ опубликован» сюда не входит намеренно: его нельзя
15
+ * сообщить, он проверяется по наличию ответа в треде.
16
+ */
17
+ exports.REPORTS = ['working', 'done', 'failed', 'deferred', 'paused'];
18
+ /**
19
+ * Сколько сигнал жизни свеж без продления. Мост продлевает его втрое чаще: один
20
+ * пропущенный удар индикатор не гасит, упавший мост гаснет через полторы минуты.
21
+ */
22
+ exports.BEAT_TTL = 90;
23
+ function beatFresh(m, at) {
24
+ return m.beatAt !== null && m.beatAt.getTime() + (m.beatTtl ?? exports.BEAT_TTL) * 1000 > at.getTime();
25
+ }
26
+ /**
27
+ * Порядок проверок — и есть смысл:
28
+ * 1. есть ответ → «опубликован», чем бы ни кончился процесс;
29
+ * 2. мост доложил исход (или паузу) → его, а не догадку; снимает исход только новый запуск;
30
+ * 3. сигнал свежий → «работает»;
31
+ * 4. запуск был, сигнала нет → «нет подтверждения активности»;
32
+ * 5. иначе → «ожидает запуска».
33
+ */
34
+ function stateOf(m, reply, at) {
35
+ if (reply)
36
+ return 'answered';
37
+ if (m.outcome !== null) {
38
+ return m.outcome === 'failed' || m.outcome === 'deferred' || m.outcome === 'paused' ? m.outcome : 'finished';
39
+ }
40
+ if (beatFresh(m, at))
41
+ return 'working';
42
+ return m.runStartedAt !== null ? 'stalled' : 'queued';
43
+ }
44
+ /** С какого момента идёт ТЕКУЩЕЕ состояние: «ожидает 3 мин» и «работает 3 мин» отсчитываются от разного. */
45
+ function stateSince(m, state, reply) {
46
+ switch (state) {
47
+ case 'answered': return reply?.createdAt ?? null;
48
+ case 'working': return m.runStartedAt;
49
+ case 'stalled': return m.beatAt;
50
+ case 'queued': return m.createdAt;
51
+ default: return m.outcomeAt ?? m.createdAt;
52
+ }
53
+ }
54
+ /**
55
+ * Сигнал жизни. Повторный — продление, а не новый запуск: иначе «работает 4 минуты»
56
+ * не выросло бы больше нуля. Удар после исхода или после протухшего сигнала — уже
57
+ * СЛЕДУЮЩИЙ запуск: время начала заново, попытка засчитывается.
58
+ */
59
+ function beat(m, note, ttl, at) {
60
+ const continues = m.outcome === null && beatFresh(m, at);
61
+ return {
62
+ runStartedAt: continues ? m.runStartedAt : at,
63
+ attempts: continues ? m.attempts : m.attempts + 1,
64
+ beatAt: at,
65
+ beatTtl: Math.max(5, Math.min(3600, ttl)),
66
+ outcome: null,
67
+ outcomeAt: null,
68
+ note,
69
+ };
70
+ }
71
+ /**
72
+ * Исход запуска — «кончился», а не «ответил»: ответ проверяется отдельно. Тот же исход
73
+ * повторно — не новое событие: мост напоминает об отложенном каждый проход, и «отложено
74
+ * 5 с назад» вечно врало бы о сроке.
75
+ */
76
+ function finish(m, outcome, note, at) {
77
+ if (m.outcome === outcome)
78
+ return null;
79
+ return { outcome, outcomeAt: at, note };
80
+ }
@@ -0,0 +1,100 @@
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.MentionsController = 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 clock_1 = require("../common/clock");
20
+ const format_1 = require("../common/format");
21
+ const http_1 = require("../common/http");
22
+ const messages_service_1 = require("../messages/messages.service");
23
+ const mentions_service_1 = require("./mentions.service");
24
+ /** Адресация: кто должен ответить, и ход обработки — от моста. */
25
+ let MentionsController = class MentionsController {
26
+ chats;
27
+ mentions;
28
+ messages;
29
+ constructor(chats, mentions, messages) {
30
+ this.chats = chats;
31
+ this.mentions = mentions;
32
+ this.messages = messages;
33
+ }
34
+ /**
35
+ * Необработанные обращения ко мне — по всем чатам сразу: мост у агента один. Чтение
36
+ * обращение НЕ гасит: агент, упавший между запросом и ответом, иначе потерял бы вопрос.
37
+ */
38
+ async mine(me, limit) {
39
+ const mentions = await this.mentions.openFor(me, (0, http_1.limitFrom)(limit));
40
+ return { mentions, count: mentions.length, server_time: (0, format_1.iso)((0, clock_1.now)()) };
41
+ }
42
+ handled(me, mentionId) {
43
+ return this.mentions.acknowledge(me, mentionId);
44
+ }
45
+ /** Что открыто. Без чата в адресе — по всем чатам: «что открыто вообще». */
46
+ async activity(slug) {
47
+ const chat = slug ? await this.chats.bySlug(slug) : null;
48
+ return this.openItems(chat?.slug);
49
+ }
50
+ /** Отчёт моста: working продлевает сигнал жизни, остальное — исходы и пауза. */
51
+ async beacon(me, body) {
52
+ await this.mentions.report(me, body);
53
+ return this.openItems();
54
+ }
55
+ async openItems(chatSlug) {
56
+ const [activity, ownerWait] = await Promise.all([this.mentions.snapshot(chatSlug), this.messages.awaitingOwner(chatSlug)]);
57
+ return { activity, owner_wait: ownerWait, server_time: (0, format_1.iso)((0, clock_1.now)()) };
58
+ }
59
+ };
60
+ exports.MentionsController = MentionsController;
61
+ __decorate([
62
+ (0, common_1.Get)('mentions'),
63
+ __param(0, (0, participant_guard_1.Me)()),
64
+ __param(1, (0, common_1.Query)('limit')),
65
+ __metadata("design:type", Function),
66
+ __metadata("design:paramtypes", [Object, String]),
67
+ __metadata("design:returntype", Promise)
68
+ ], MentionsController.prototype, "mine", null);
69
+ __decorate([
70
+ (0, common_1.Post)('mentions/:mention/handled'),
71
+ (0, common_1.HttpCode)(200),
72
+ __param(0, (0, participant_guard_1.Me)()),
73
+ __param(1, (0, common_1.Param)('mention', common_1.ParseIntPipe)),
74
+ __metadata("design:type", Function),
75
+ __metadata("design:paramtypes", [Object, Number]),
76
+ __metadata("design:returntype", void 0)
77
+ ], MentionsController.prototype, "handled", null);
78
+ __decorate([
79
+ (0, common_1.Get)(['activity', 'chats/:chat/activity']),
80
+ __param(0, (0, common_1.Param)('chat')),
81
+ __metadata("design:type", Function),
82
+ __metadata("design:paramtypes", [Object]),
83
+ __metadata("design:returntype", Promise)
84
+ ], MentionsController.prototype, "activity", null);
85
+ __decorate([
86
+ (0, common_1.Post)('activity'),
87
+ (0, common_1.HttpCode)(200),
88
+ __param(0, (0, participant_guard_1.Me)()),
89
+ __param(1, (0, common_1.Body)()),
90
+ __metadata("design:type", Function),
91
+ __metadata("design:paramtypes", [Object, Object]),
92
+ __metadata("design:returntype", Promise)
93
+ ], MentionsController.prototype, "beacon", null);
94
+ exports.MentionsController = MentionsController = __decorate([
95
+ (0, common_1.Controller)('api'),
96
+ (0, common_1.UseGuards)(participant_guard_1.ParticipantGuard),
97
+ __metadata("design:paramtypes", [chats_service_1.ChatsService,
98
+ mentions_service_1.MentionsService,
99
+ messages_service_1.MessagesService])
100
+ ], MentionsController);