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,81 @@
1
+ // Настройки раннера — из окружения процесса, и движки этой машины — из runner.json.
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const BRIDGE_MAIN = fileURLToPath(new URL('../bridge/main.js', import.meta.url));
9
+ const WORKSPACE = path.join(os.homedir(), 'agent-workspace');
10
+
11
+ export class ConfigError extends Error {}
12
+
13
+ /**
14
+ * @param {NodeJS.ProcessEnv} env
15
+ * @param {object} overrides поля поверх окружения: тесты
16
+ */
17
+ export function runnerConfig(env = process.env, overrides = {}) {
18
+ if (!env.CHAT_RUNNER_TOKEN) {
19
+ throw new ConfigError('нужен CHAT_RUNNER_TOKEN — токен участника host: '
20
+ + 'php artisan chat:participant host --kind=host --role=host');
21
+ }
22
+
23
+ return Object.freeze({
24
+ api: env.CHAT_API || 'http://localhost:8600/api',
25
+ token: env.CHAT_RUNNER_TOKEN,
26
+ machineFile: env.CHAT_RUNNER_CONFIG || path.join(WORKSPACE, 'runner.json'),
27
+ logsDir: env.CHAT_RUNNER_LOGS || path.join(WORKSPACE, 'logs'),
28
+ intervalMs: seconds(env, 'CHAT_RUNNER_INTERVAL', 15) * 1000,
29
+ // Кнопка «перезапустить мосты»: сервер кладёт файл, раннер его забирает. Под systemd
30
+ // то же делает qbazar-bridge-restart.path; у пакета systemd нет — следит сам.
31
+ restartFile: env.CHAT_RESTART_FILE || '',
32
+ // Записи запусков мостов (тот же каталог, что у моста) и сколько дней хранить их сырьё —
33
+ // задание и ход работы. Карточка опыта остаётся навсегда. 0 — хранить всё.
34
+ runsDir: env.CHAT_RUNS_DIR || path.join(WORKSPACE, 'runs'),
35
+ keepRunsDays: days(env, 'CHAT_KEEP_RUNS_DAYS', 30),
36
+ // Мост, упавший быстрее flapMs, — не случайность: следующий запуск не раньше backoffMs.
37
+ flapMs: 30_000,
38
+ backoffMs: 60_000,
39
+ bridgeCommand: [process.execPath, BRIDGE_MAIN, '--watch'],
40
+ // Окружение, от которого мосты наследуют своё: PATH, HOME, CHAT_STATE_DIR…
41
+ env,
42
+ ...overrides,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Движки ЭТОЙ машины. Сервер называет агенту только движок, а команду движка знает
48
+ * только этот файл — иначе запись в базе чата стала бы удалённым запуском команд
49
+ * на машине. Перечитывается на каждом проходе: поправил — подхватилось.
50
+ *
51
+ * {"runtimes": {"claude": {"cmd": "…", "cwd": "~/…", "env": {"PATH": "…"}}},
52
+ * "trigger_roles": "any"}
53
+ *
54
+ * @returns {{runtimes: Record<string, {cmd: string, cwd?: string, env?: Record<string, string>}>, triggerRoles: string}}
55
+ */
56
+ export function loadMachine(file) {
57
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
58
+ return { runtimes: data.runtimes ?? {}, triggerRoles: data.trigger_roles ?? 'any' };
59
+ }
60
+
61
+ /** ~ в начале пути — домашний каталог, как в оболочке. */
62
+ export function expandHome(value) {
63
+ const text = String(value);
64
+ return text === '~' || text.startsWith('~/') ? os.homedir() + text.slice(1) : text;
65
+ }
66
+
67
+ function days(env, name, fallback) {
68
+ const raw = env[name];
69
+ if (raw === undefined || raw === '') return fallback;
70
+ const value = Number(raw);
71
+ if (!Number.isInteger(value) || value < 0) throw new ConfigError(`${name}=${raw} — нужно целое число дней (0 — хранить всё)`);
72
+ return value;
73
+ }
74
+
75
+ function seconds(env, name, fallback) {
76
+ const raw = env[name];
77
+ if (raw === undefined || raw === '') return fallback;
78
+ const value = Number(raw);
79
+ if (!Number.isInteger(value) || value < 1) throw new ConfigError(`${name}=${raw} — нужно целое число секунд`);
80
+ return value;
81
+ }
@@ -0,0 +1,69 @@
1
+ // Точка входа раннера.
2
+ //
3
+ // CHAT_RUNNER_TOKEN=… node host/runner/main.js
4
+ //
5
+ // Движки машины — в runner.json (CHAT_RUNNER_CONFIG, по умолчанию ~/agent-workspace/runner.json,
6
+ // образец — deploy/systemd/runner.json.example). Остальное — переменные CHAT_* (см. config.js).
7
+
8
+ import fs from 'node:fs';
9
+ import { ChatUnavailableError } from '../lib/chat-api.js';
10
+ import { log } from '../lib/log.js';
11
+ import { ConfigError, runnerConfig } from './config.js';
12
+ import { RunRetention } from './retention.js';
13
+ import { createRunner, takeRestartRequest } from './runner.js';
14
+
15
+ let config;
16
+ try {
17
+ config = runnerConfig();
18
+ } catch (e) {
19
+ if (!(e instanceof ConfigError)) throw e;
20
+ exit(e.message);
21
+ }
22
+ if (!fs.existsSync(config.machineFile)) {
23
+ exit(`нет ${config.machineFile} — опиши движки этой машины (образец: deploy/systemd/runner.json.example)`);
24
+ }
25
+
26
+ const runner = createRunner(config, { log });
27
+ const retention = new RunRetention({ dir: config.runsDir, keepDays: config.keepRunsDays, log });
28
+
29
+ let stopping = false;
30
+ for (const signal of ['SIGINT', 'SIGTERM']) {
31
+ process.on(signal, async () => {
32
+ if (stopping) return;
33
+ stopping = true;
34
+ log('остановка: гашу мосты');
35
+ await runner.stopAll();
36
+ process.exit(0);
37
+ });
38
+ }
39
+
40
+ log(`раннер: проверка агентов раз в ${config.intervalMs / 1000} с, движки — ${config.machineFile}`);
41
+ while (!stopping) {
42
+ retention.tick();
43
+ try {
44
+ await runner.reconcile();
45
+ } catch (e) {
46
+ // Раннер не должен умирать из-за одного плохого прохода.
47
+ log(e instanceof ChatUnavailableError
48
+ ? `${e.message} — повтор через ${config.intervalMs / 1000} с`
49
+ : `ошибка прохода: ${e.stack}`);
50
+ }
51
+ await untilNextPass();
52
+ }
53
+
54
+ /** Ждём следующего прохода, но на кнопку перезапуска отвечаем за секунду. */
55
+ async function untilNextPass() {
56
+ for (let waited = 0; waited < config.intervalMs && !stopping; waited += 1000) {
57
+ if (takeRestartRequest(config.restartFile)) {
58
+ log('перезапуск по запросу из чата: гашу мосты, следующий проход поднимет заново');
59
+ await runner.stopAll();
60
+ return;
61
+ }
62
+ await new Promise(resolve => setTimeout(resolve, 1000));
63
+ }
64
+ }
65
+
66
+ function exit(message) {
67
+ console.error(message);
68
+ process.exit(1);
69
+ }
@@ -0,0 +1,40 @@
1
+ // Срок хранения сырья записей запусков (docs/plan-2026-10-24-knowledge.md, K5). Раннер один
2
+ // на машину и живёт постоянно — ему и чистить: раз в сутки, первый раз — сразу при старте.
3
+
4
+ import { pruneRuns } from '../bridge/run-archive.js';
5
+
6
+ const DAY_MS = 86_400_000;
7
+
8
+ export class RunRetention {
9
+ #dir;
10
+ #keepDays;
11
+ #log;
12
+ #now;
13
+ #last = -Infinity;
14
+
15
+ /**
16
+ * @param {object} p
17
+ * @param {string} p.dir корень записей
18
+ * @param {number} p.keepDays сколько дней хранить сырьё; 0 — хранить всё
19
+ * @param {(...parts: any[]) => void} p.log
20
+ * @param {() => number} [p.now] часы, мс
21
+ */
22
+ constructor({ dir, keepDays, log, now = Date.now }) {
23
+ this.#dir = dir;
24
+ this.#keepDays = keepDays;
25
+ this.#log = log;
26
+ this.#now = now;
27
+ }
28
+
29
+ /** Почистить, если с прошлого раза прошли сутки. Сбой чистки раннер не роняет. */
30
+ tick() {
31
+ if (this.#keepDays === 0 || this.#now() - this.#last < DAY_MS) return;
32
+ this.#last = this.#now();
33
+ try {
34
+ const pruned = pruneRuns(this.#dir, { keepDays: this.#keepDays, now: new Date(this.#now()) });
35
+ if (pruned) this.#log(`записи запусков старше ${this.#keepDays} дн.: сырьё удалено у ${pruned}, карточки опыта на месте`);
36
+ } catch (e) {
37
+ this.#log(`чистка записей запусков не удалась: ${e.message}`);
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,161 @@
1
+ // Раннер: один процесс на машине, который держит мост для КАЖДОГО агента этой машины.
2
+ //
3
+ // Агентов заводит владелец в интерфейсе. Раннер раз в проход спрашивает сервер, кто
4
+ // есть, и приводит мосты в соответствие: новому агенту — мост, отключённому или
5
+ // удалённому — остановка, упавший мост — перезапуск.
6
+ //
7
+ // КАК запускать агента, сервер не говорит: он называет только движок, а команда движка
8
+ // лежит в runner.json этой машины.
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { ChatApi } from '../lib/chat-api.js';
13
+ import { BridgeProcess } from './bridge-process.js';
14
+ import { expandHome, loadMachine } from './config.js';
15
+
16
+ export class Runner {
17
+ /** @type {Map<string, BridgeProcess>} handle → мост */
18
+ #bridges = new Map();
19
+ #config;
20
+ #api;
21
+ #log;
22
+ #now;
23
+ #tokens = new Map(); // handle → токен агента, выданный раннеру: живёт, пока жив раннер
24
+ #notBefore = new Map(); // handle → когда можно снова запускать упавший мост
25
+ #warned = new Set(); // о каких агентах с неизвестным движком уже сказали
26
+
27
+ /**
28
+ * @param {object} deps
29
+ * @param {ReturnType<import('./config.js').runnerConfig>} deps.config
30
+ * @param {ChatApi} deps.api
31
+ * @param {(...parts: any[]) => void} deps.log
32
+ * @param {() => number} [deps.now] часы, мс
33
+ */
34
+ constructor({ config, api, log, now = Date.now }) {
35
+ this.#config = config;
36
+ this.#api = api;
37
+ this.#log = log;
38
+ this.#now = now;
39
+ }
40
+
41
+ /** Работающие мосты: handle → мост. Копия — менять состав может только сам раннер. */
42
+ get bridges() {
43
+ return new Map(this.#bridges);
44
+ }
45
+
46
+ /** Один проход: привести мосты в соответствие с агентами на сервере. */
47
+ async reconcile() {
48
+ const machine = loadMachine(this.#config.machineFile);
49
+ const wanted = await this.#wantedAgents(machine.runtimes);
50
+
51
+ for (const [handle, bridge] of this.#bridges) {
52
+ const agent = wanted.get(handle);
53
+ if (!agent || agent.runtime !== bridge.runtime) {
54
+ this.#log(`@${handle}: ${agent ? 'сменился движок' : 'агент отключён или удалён'} — мост останавливается`);
55
+ await bridge.stop();
56
+ this.#bridges.delete(handle);
57
+ } else if (!bridge.alive()) {
58
+ this.#forgetCrashed(bridge);
59
+ }
60
+ }
61
+
62
+ for (const [handle, agent] of wanted) {
63
+ if (this.#bridges.has(handle) || this.#now() < (this.#notBefore.get(handle) ?? 0)) continue;
64
+ try {
65
+ await this.#spawn(agent, machine.runtimes[agent.runtime], machine.triggerRoles);
66
+ } catch (e) {
67
+ this.#log(`@${handle}: мост не запустился: ${e.message}`);
68
+ this.#notBefore.set(handle, this.#now() + this.#config.backoffMs);
69
+ }
70
+ }
71
+ }
72
+
73
+ async stopAll() {
74
+ await Promise.all([...this.#bridges.values()].map(bridge => bridge.stop()));
75
+ this.#bridges.clear();
76
+ }
77
+
78
+ /** Агенты сервера, которых эта машина умеет запускать. О прочих — одно предупреждение. */
79
+ async #wantedAgents(runtimes) {
80
+ const agents = await this.#api.runnerAgents(Object.keys(runtimes).sort());
81
+ const wanted = new Map();
82
+ for (const agent of agents) {
83
+ if (agent.runtime in runtimes) {
84
+ wanted.set(agent.handle, agent);
85
+ } else if (!this.#warned.has(agent.handle)) {
86
+ this.#warned.add(agent.handle);
87
+ this.#log(`@${agent.handle}: движка «${agent.runtime}» на этой машине нет — `
88
+ + `добавь его в ${this.#config.machineFile} или смени агенту движок`);
89
+ }
90
+ }
91
+ return wanted;
92
+ }
93
+
94
+ /** Упавший мост убираем; упавший сразу после запуска — ещё и не поднимаем по кругу. */
95
+ #forgetCrashed(bridge) {
96
+ const { handle, exitCode } = bridge;
97
+ if (this.#now() - bridge.startedAt < this.#config.flapMs) {
98
+ this.#notBefore.set(handle, this.#now() + this.#config.backoffMs);
99
+ this.#log(`@${handle}: мост упал сразу после запуска (код ${exitCode}) — `
100
+ + `повтор через ${this.#config.backoffMs / 1000} с, см. ${this.#logFile(handle)}`);
101
+ } else {
102
+ this.#log(`@${handle}: мост завершился (код ${exitCode}) — перезапуск`);
103
+ }
104
+ this.#bridges.delete(handle);
105
+ }
106
+
107
+ async #spawn(agent, runtime, triggerRoles) {
108
+ const { handle } = agent;
109
+ if (!this.#tokens.has(handle)) this.#tokens.set(handle, await this.#api.agentToken(handle));
110
+
111
+ fs.mkdirSync(this.#config.logsDir, { recursive: true });
112
+ const bridge = new BridgeProcess({
113
+ handle,
114
+ runtime: agent.runtime,
115
+ command: this.#config.bridgeCommand,
116
+ env: this.#bridgeEnv(agent, runtime, triggerRoles),
117
+ logFile: this.#logFile(handle),
118
+ startedAt: this.#now(),
119
+ });
120
+ this.#bridges.set(handle, bridge);
121
+ this.#log(`@${handle}: мост запущен (движок ${agent.runtime}, pid ${bridge.pid})`);
122
+ }
123
+
124
+ #bridgeEnv(agent, runtime, triggerRoles) {
125
+ const env = {
126
+ ...this.#config.env,
127
+ ...Object.fromEntries(Object.entries(runtime.env ?? {}).map(([k, v]) => [k, expandHome(v)])),
128
+ CHAT_API: this.#config.api,
129
+ CHAT_TOKEN: this.#tokens.get(agent.handle),
130
+ CHAT_HANDLE: agent.handle,
131
+ CHAT_RUNTIME: agent.runtime,
132
+ CHAT_AGENT_CMD: runtime.cmd,
133
+ CHAT_AGENT_CWD: runtime.cwd ? expandHome(runtime.cwd) : '',
134
+ CHAT_TRIGGER_ROLES: triggerRoles,
135
+ CHAT_DISTILL_CMD: runtime.distill?.cmd ?? '',
136
+ };
137
+ delete env.CHAT_RUNNER_TOKEN; // агенту токен раннера ни к чему
138
+ return env;
139
+ }
140
+
141
+ #logFile(handle) {
142
+ return path.join(this.#config.logsDir, `bridge-${handle}.log`);
143
+ }
144
+ }
145
+
146
+ /** Раннер в сборе — из настроек. */
147
+ export function createRunner(config, { log }) {
148
+ return new Runner({ config, api: new ChatApi({ url: config.api, token: config.token, timeoutMs: 15_000 }), log });
149
+ }
150
+
151
+ /** Забрать запрос на перезапуск: файл удаляется — так страница узнаёт, что он принят. */
152
+ export function takeRestartRequest(file) {
153
+ if (!file) return false;
154
+ try {
155
+ fs.unlinkSync(file);
156
+ return true;
157
+ } catch (e) {
158
+ if (e.code === 'ENOENT') return false;
159
+ throw e;
160
+ }
161
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
- "name": "team-chat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "team-chat",
3
+ "version": "0.3.0",
4
+ "description": "Командный чат владельца и ИИ-агентов (claude, codex): тред, журнал решений, документы — по проекту на чат",
5
+ "license": "MIT",
6
+ "author": "j4vlon <fedot.pushkin98@gmail.com>",
7
+ "keywords": [
8
+ "chat",
9
+ "ai-agents",
10
+ "claude",
11
+ "codex",
12
+ "mcp",
13
+ "decision-log",
14
+ "cli"
15
+ ],
16
+ "type": "module",
17
+ "bin": {
18
+ "team-chat": "cli/team-chat.js"
19
+ },
20
+ "engines": {
21
+ "node": ">=20.19"
22
+ },
23
+ "files": [
24
+ "cli/",
25
+ "agents/",
26
+ "bin/bridge",
27
+ "bin/runner",
28
+ "host/lib/",
29
+ "host/bridge/",
30
+ "host/runner/",
31
+ "host/knowledge/",
32
+ "server/package.json",
33
+ "server/dist/src/",
34
+ "server/drizzle/",
35
+ "server/views/"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc -p server",
39
+ "db:generate": "cd server && drizzle-kit generate",
40
+ "check:pack": "node scripts/check-pack.js",
41
+ "test": "npm run test:host && npm run test:server",
42
+ "test:host": "node --test host/test/*.test.js",
43
+ "test:server": "tsc -p server && node --test --test-concurrency=1 server/dist/test/*.test.js"
44
+ },
45
+ "dependencies": {
46
+ "@electric-sql/pglite": "0.5.8",
47
+ "@modelcontextprotocol/sdk": "1.32.1",
48
+ "@nestjs/common": "11.2.7",
49
+ "@nestjs/core": "11.2.7",
50
+ "@nestjs/platform-express": "11.2.7",
51
+ "drizzle-orm": "0.45.4",
52
+ "express": "5.3.0",
53
+ "multer": "2.4.0",
54
+ "pg": "8.23.1",
55
+ "reflect-metadata": "0.2.2",
56
+ "rxjs": "7.8.2",
57
+ "zod": "4.6.5"
58
+ },
59
+ "devDependencies": {
60
+ "@nestjs/testing": "11.2.7",
61
+ "@types/express": "5.0.6",
62
+ "@types/multer": "2.3.0",
63
+ "@types/node": "20.19.43",
64
+ "@types/pg": "8.23.1",
65
+ "drizzle-kit": "0.31.11",
66
+ "typescript": "5.9.3"
67
+ }
68
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createApp = createApp;
4
+ require("reflect-metadata");
5
+ const core_1 = require("@nestjs/core");
6
+ const express_1 = require("express");
7
+ const app_module_1 = require("./app.module");
8
+ /**
9
+ * Приложение в сборе — без порта: его слушает main.ts, а тесты — случайный свободный.
10
+ * База приходит открытой и смигрированной; закрывает её приложение при остановке.
11
+ */
12
+ async function createApp({ config, database, logger = ['error', 'warn'] }) {
13
+ const app = await core_1.NestFactory.create(app_module_1.AppModule.forRoot({ config, database }), {
14
+ logger,
15
+ // Тело запроса разбираем сами — с пределом под самый большой документ (500 000 знаков).
16
+ bodyParser: false,
17
+ });
18
+ app.disable('x-powered-by');
19
+ // Дамп при восстановлении — вся история чата; ему предел больше, остальным хватает 2 МБ.
20
+ app.use('/api/local/restore', (0, express_1.json)({ limit: '256mb' }));
21
+ app.useBodyParser('json', { limit: '2mb' });
22
+ app.useBodyParser('urlencoded', { limit: '2mb', extended: true });
23
+ app.enableShutdownHooks();
24
+ return app;
25
+ }
@@ -0,0 +1,50 @@
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 ConfigModule_1, AppModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.AppModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const config_1 = require("./config/config");
13
+ const database_module_1 = require("./db/database.module");
14
+ const conversation_module_1 = require("./conversation.module");
15
+ const collector_module_1 = require("./collector/collector.module");
16
+ const health_controller_1 = require("./health/health.controller");
17
+ const knowledge_module_1 = require("./knowledge/knowledge.module");
18
+ const local_module_1 = require("./local/local.module");
19
+ const mcp_module_1 = require("./mcp/mcp.module");
20
+ const participants_module_1 = require("./participants/participants.module");
21
+ const sharing_module_1 = require("./sharing/sharing.module");
22
+ const team_module_1 = require("./team/team.module");
23
+ const web_module_1 = require("./web/web.module");
24
+ /** Настройки — всем модулям, как и база. */
25
+ let ConfigModule = ConfigModule_1 = class ConfigModule {
26
+ static forRoot(config) {
27
+ return {
28
+ module: ConfigModule_1,
29
+ providers: [{ provide: config_1.SERVER_CONFIG, useValue: config }],
30
+ exports: [config_1.SERVER_CONFIG],
31
+ };
32
+ }
33
+ };
34
+ ConfigModule = ConfigModule_1 = __decorate([
35
+ (0, common_1.Global)(),
36
+ (0, common_1.Module)({})
37
+ ], ConfigModule);
38
+ let AppModule = AppModule_1 = class AppModule {
39
+ static forRoot({ config, database }) {
40
+ return {
41
+ module: AppModule_1,
42
+ imports: [ConfigModule.forRoot(config), database_module_1.DatabaseModule.forRoot(database), participants_module_1.ParticipantsModule, conversation_module_1.ConversationModule, knowledge_module_1.KnowledgeModule, sharing_module_1.SharingModule, collector_module_1.CollectorModule, team_module_1.TeamModule, mcp_module_1.McpModule, web_module_1.WebModule, local_module_1.LocalModule],
43
+ controllers: [health_controller_1.HealthController],
44
+ };
45
+ }
46
+ };
47
+ exports.AppModule = AppModule;
48
+ exports.AppModule = AppModule = AppModule_1 = __decorate([
49
+ (0, common_1.Module)({})
50
+ ], AppModule);
@@ -0,0 +1,111 @@
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.AttachmentStore = exports.isText = exports.isImage = exports.MAX_BYTES = exports.MAX_FILES = void 0;
19
+ const common_1 = require("@nestjs/common");
20
+ const drizzle_orm_1 = require("drizzle-orm");
21
+ const node_crypto_1 = __importDefault(require("node:crypto"));
22
+ const node_fs_1 = __importDefault(require("node:fs"));
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ const config_1 = require("../config/config");
25
+ const database_module_1 = require("../db/database.module");
26
+ const schema_1 = require("../db/schema");
27
+ const mime_1 = require("./mime");
28
+ /** Пределы — как были у PHP-сервера: до 10 файлов по 20 МБ на сообщение. */
29
+ exports.MAX_FILES = 10;
30
+ exports.MAX_BYTES = 20 * 1024 * 1024;
31
+ /** Текст, который агент читает как есть, без скачивания. */
32
+ const TEXT_LIMIT = 200 * 1024;
33
+ const TEXT_TYPES = new Set(['application/json', 'application/xml', 'application/x-yaml']);
34
+ /**
35
+ * Картинки, которые можно показать прямо в браузере и отдать агенту изображением.
36
+ * SVG нарочно нет: это документ со скриптами, открытый на нашем домене он получил бы
37
+ * токен владельца со страницы.
38
+ */
39
+ const IMAGES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
40
+ const isImage = (a) => IMAGES.has(a.mime);
41
+ exports.isImage = isImage;
42
+ const isText = (a) => a.size <= TEXT_LIMIT && (a.mime.startsWith('text/') || TEXT_TYPES.has(a.mime));
43
+ exports.isText = isText;
44
+ /**
45
+ * Файлы сообщений — в каталоге данных: <данные>/files/attachments/<сообщение>/<случайное имя>.
46
+ * Раскладка та же, что у PHP-сервера (storage/app/private/attachments/…): при переносе
47
+ * файлы просто копируются, пути в базе остаются верными.
48
+ */
49
+ let AttachmentStore = class AttachmentStore {
50
+ db;
51
+ root;
52
+ constructor(db, config) {
53
+ this.db = db;
54
+ this.root = node_path_1.default.join(config.dataDir, 'files');
55
+ }
56
+ /** Каталог для приёма файлов: на той же файловой системе, что хранилище, — перенос без копирования. */
57
+ get incoming() {
58
+ const dir = node_path_1.default.join(this.root, 'incoming');
59
+ node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
60
+ return dir;
61
+ }
62
+ /**
63
+ * Сохранить файлы сообщения — в транзакции его публикации. Возвращает пути сохранённых
64
+ * файлов: откатится транзакция — их надо убрать (remove), иначе останутся сиротами.
65
+ */
66
+ async save(tx, messageId, uploads, stored) {
67
+ for (const upload of uploads) {
68
+ const relative = node_path_1.default.join('attachments', String(messageId), node_crypto_1.default.randomBytes(20).toString('hex') + extension(upload.name));
69
+ const target = this.absolute(relative);
70
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(target), { recursive: true, mode: 0o700 });
71
+ const mime = (0, mime_1.sniffMime)(upload.path);
72
+ node_fs_1.default.renameSync(upload.path, target);
73
+ stored.push(relative);
74
+ await tx.insert(schema_1.attachments).values({
75
+ messageId,
76
+ name: [...(upload.name || 'file')].slice(0, 255).join(''),
77
+ mime,
78
+ size: upload.size,
79
+ path: relative,
80
+ });
81
+ }
82
+ }
83
+ remove(relativePaths) {
84
+ for (const relative of relativePaths)
85
+ node_fs_1.default.rmSync(this.absolute(relative), { force: true });
86
+ }
87
+ async find(id) {
88
+ const [row] = await this.db.select().from(schema_1.attachments).where((0, drizzle_orm_1.eq)(schema_1.attachments.id, id));
89
+ return row ?? null;
90
+ }
91
+ absolute(a) {
92
+ const relative = typeof a === 'string' ? a : a.path;
93
+ const full = node_path_1.default.resolve(this.root, relative);
94
+ // Путь из базы — не повод выйти за пределы хранилища.
95
+ if (!full.startsWith(this.root + node_path_1.default.sep))
96
+ throw new Error(`путь вложения вне хранилища: ${relative}`);
97
+ return full;
98
+ }
99
+ };
100
+ exports.AttachmentStore = AttachmentStore;
101
+ exports.AttachmentStore = AttachmentStore = __decorate([
102
+ (0, common_1.Injectable)(),
103
+ __param(0, (0, common_1.Inject)(database_module_1.DATABASE)),
104
+ __param(1, (0, common_1.Inject)(config_1.SERVER_CONFIG)),
105
+ __metadata("design:paramtypes", [Object, Object])
106
+ ], AttachmentStore);
107
+ /** Расширение из имени автора — только безопасное: латиница и цифры. */
108
+ function extension(name) {
109
+ const ext = node_path_1.default.extname(name).toLowerCase();
110
+ return /^\.[a-z0-9]{1,10}$/.test(ext) ? ext : '';
111
+ }