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,43 @@
1
+ // Карточка опыта — то, что остаётся от запуска агента: с каким стеком он работал, как шёл
2
+ // к ответу и чем кончилось. Без кода и содержимого проекта — см. docs/plan-2026-10-24-knowledge.md.
3
+ //
4
+ // Здесь — часть без нейросети: факты из файлов проекта и записи запуска. Смысловая часть
5
+ // (problem, solution) — урок от нейросети, шаг K2; пока она пустая.
6
+
7
+ import { summarizeRun } from './process.js';
8
+ import { detectStack } from './stack.js';
9
+
10
+ export const CARD_FORMAT = 1;
11
+
12
+ /**
13
+ * @param {object} p
14
+ * @param {object} p.run run.json записи запуска (run-archive.js)
15
+ * @param {string} p.output ход работы агента — stdout записи
16
+ * @param {string|null} p.workdir каталог, где работал агент
17
+ */
18
+ export function buildCard({ run, output, workdir }) {
19
+ const process = summarizeRun(run.runtime, output);
20
+ const mentions = run.mentions ?? [];
21
+ return {
22
+ format: CARD_FORMAT,
23
+ stack: detectStack(workdir, process.touched),
24
+ process: {
25
+ engine: run.runtime,
26
+ role: mentions[0]?.agent_role ?? null,
27
+ steps: process.steps,
28
+ duration_s: run.duration_ms == null ? null : Math.round(run.duration_ms / 1000),
29
+ attempt: Math.max(1, ...mentions.map(m => m.attempt ?? 1)),
30
+ tools: process.tools,
31
+ errors: process.errors,
32
+ tokens: process.tokens,
33
+ },
34
+ problem: null,
35
+ solution: null,
36
+ outcome: {
37
+ answered: mentions.length > 0 && mentions.every(m => m.outcome === 'answered'),
38
+ exit_code: run.exit_code,
39
+ owner_reaction: null,
40
+ became_decision: false,
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,50 @@
1
+ // С какими ошибками столкнулся агент — сигнатурами, без путей и значений: «SQLSTATE 42S02 —
2
+ // table not found», а не строка лога целиком.
3
+
4
+ import { scrub } from './scrub.js';
5
+
6
+ const MAX_ERRORS = 10;
7
+ const MAX_TEXT = 120;
8
+
9
+ /** Сигнатуры по убыванию точности. → [вид, отрезок текста, по которому опознали]. */
10
+ const SIGNATURES = [
11
+ [/SQLSTATE\[?([0-9A-Z]{5})\]?/, m => `SQLSTATE ${m[1]}`],
12
+ [/\b(ERR_[A-Z0-9_]{3,})\b/, m => m[1]],
13
+ [/\b(E(?:CONNREFUSED|NOENT|ACCES|ADDRINUSE|TIMEDOUT|PERM|EXIST|NOTFOUND|PIPE))\b/, m => m[1]],
14
+ [/\b([A-Z][A-Za-z]*(?:Error|Exception))\b/, m => m[1]],
15
+ [/\bcommand not found\b/i, () => 'command not found'],
16
+ [/\b(?:Fatal error|Segmentation fault|panic:)/, m => m[0].replace(/:$/, '')],
17
+ [/(?:ℹ\s*fail\s+[1-9]|\b[1-9]\d*\s+(?:failed|failing)\b|Tests:.*\b[1-9]\d*\s+failed|\bFAILED\b|^FAIL\b)/m, () => 'tests failed'],
18
+ ];
19
+
20
+ /** Сигнатура ошибки в выводе команды или null, если ошибки не видно. */
21
+ export function errorSignature(output) {
22
+ const text = String(output ?? '');
23
+ for (const [pattern, kind] of SIGNATURES) {
24
+ const match = pattern.exec(text);
25
+ if (match) return { kind: kind(match), text: lineAround(text, match.index) };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /** Собиратель ошибок запуска: без повторов и не больше десяти. */
31
+ export class ErrorLog {
32
+ #seen = new Set();
33
+ list = [];
34
+
35
+ add(error) {
36
+ if (!error || this.list.length >= MAX_ERRORS) return;
37
+ const key = `${error.kind}|${error.text}`;
38
+ if (this.#seen.has(key)) return;
39
+ this.#seen.add(key);
40
+ this.list.push(error);
41
+ }
42
+ }
43
+
44
+ /** Строка, где нашлась сигнатура, — очищенная от секретов и путей и обрезанная. */
45
+ function lineAround(text, index) {
46
+ const start = text.lastIndexOf('\n', index) + 1;
47
+ const end = text.indexOf('\n', index);
48
+ const line = scrub(text.slice(start, end === -1 ? undefined : end).trim()).replace(/\s+/g, ' ');
49
+ return line.length > MAX_TEXT ? `${line.slice(0, MAX_TEXT - 1)}…` : line;
50
+ }
@@ -0,0 +1,153 @@
1
+ // Урок из запуска — смысловая часть карточки опыта: что за задача, в чём была причина,
2
+ // какой подход сработал, где был тупик, какое правило отсюда следует. Пишет нейросеть по
3
+ // жёсткой схеме; ответ проверяется и проходит фильтр секретов. Не прошёл — урока нет:
4
+ // карточка без урока лучше карточки с мусором.
5
+
6
+ import { spawn } from 'node:child_process';
7
+ import { scrub } from './scrub.js';
8
+ import { splitWords } from '../lib/shell-words.js';
9
+
10
+ /** Тип задачи — по нему уроки разбираются и ищутся. Пояснения уходят модели вместе со схемой. */
11
+ const CATEGORY_MEANINGS = {
12
+ bug: 'исправление ошибки',
13
+ task: 'новая возможность или доработка',
14
+ search: 'поиск и разбор: найти причину, место в коде, ответ в документации',
15
+ review: 'ревью кода или результата чужой работы',
16
+ refactor: 'переделка без смены поведения',
17
+ test: 'тесты',
18
+ config: 'настройка',
19
+ infra: 'сборка, выкладка, окружение',
20
+ performance: 'скорость и ресурсы',
21
+ security: 'безопасность',
22
+ question: 'ответ на вопрос без изменений',
23
+ other: 'ничего из перечисленного',
24
+ };
25
+
26
+ export const CATEGORIES = Object.keys(CATEGORY_MEANINGS);
27
+
28
+ const MAX_FIELD = 300;
29
+ const MAX_ITEMS = 8;
30
+
31
+ export const LESSON_PROMPT = `Ниже — запись работы ИИ-агента над задачей в программном проекте. Извлеки из неё опыт,
32
+ который пригодится в похожей задаче в другом проекте.
33
+
34
+ Правила:
35
+ - Без кода, без имён файлов и путей, без названий проекта, компаний, людей, без данных и ключей.
36
+ - Технологии называй (Laravel, PostgreSQL, React — можно), предметную область обобщай
37
+ («модель заказа» → «модель предметной области»).
38
+ - Пиши на языке задачи, коротко и по существу: каждое поле — одна-две фразы.
39
+ - Если агент не решал техническую задачу (только поговорил или ничего не сделал) — ответь {"skip": true}.
40
+ - category — тип задачи:
41
+ ${Object.entries(CATEGORY_MEANINGS).map(([name, meaning]) => ` ${name} — ${meaning}`).join('\n')}
42
+
43
+ Ответ — только JSON, без пояснений, строго по схеме:
44
+ {
45
+ "problem": {
46
+ "category": "${CATEGORIES.join('|')}",
47
+ "area": "область: база данных / миграции, сборка, API, интерфейс…",
48
+ "symptom": "как проявлялась задача или проблема",
49
+ "root_cause": "в чём была причина (null, если это не поиск причины)"
50
+ },
51
+ "solution": {
52
+ "approach": ["шаги, которые привели к результату"],
53
+ "dead_ends": ["что пробовал и не сработало (пустой список, если ничего)"],
54
+ "verified_by": "как проверено, что решено (null, если не проверялось)",
55
+ "rule": "одно правило, которое стоит применить в похожей задаче",
56
+ "applies_when": "когда это правило применимо: стек, условия"
57
+ }
58
+ }
59
+
60
+ Запись работы:
61
+ `;
62
+
63
+ /**
64
+ * Запустить команду урока (distill из runner.json) и разобрать ответ.
65
+ * @returns {Promise<{problem: object, solution: object} | null>} null — урока нет (пустяк, сбой, мусор)
66
+ */
67
+ export async function distillLesson(command, transcript, { timeoutMs = 120_000, env, cwd } = {}) {
68
+ const [cmd, ...args] = splitWords(command);
69
+ const output = await run(cmd, args, LESSON_PROMPT + transcript, { timeoutMs, env, cwd });
70
+ return parseLesson(output);
71
+ }
72
+
73
+ /** Разобрать ответ модели: первый объект JSON в тексте, проверка схемы, фильтр секретов. */
74
+ export function parseLesson(text) {
75
+ const data = firstJson(text);
76
+ if (!data || data.skip === true) return null;
77
+ const { problem, solution } = data;
78
+ if (!isObject(problem) || !isObject(solution)) return null;
79
+
80
+ const lesson = {
81
+ problem: {
82
+ category: CATEGORIES.includes(problem.category) ? problem.category : 'other',
83
+ area: field(problem.area),
84
+ symptom: field(problem.symptom),
85
+ root_cause: field(problem.root_cause),
86
+ },
87
+ solution: {
88
+ approach: list(solution.approach),
89
+ dead_ends: list(solution.dead_ends),
90
+ verified_by: field(solution.verified_by),
91
+ rule: field(solution.rule),
92
+ applies_when: field(solution.applies_when),
93
+ },
94
+ };
95
+ // Без симптома и правила урок пустой — его нечем применить.
96
+ return lesson.problem.symptom && lesson.solution.rule ? lesson : null;
97
+ }
98
+
99
+ function field(value) {
100
+ if (typeof value !== 'string' || !value.trim()) return null;
101
+ const clean = scrub(value.trim()).replace(/`+/g, '');
102
+ return clean.length > MAX_FIELD ? `${clean.slice(0, MAX_FIELD - 1)}…` : clean;
103
+ }
104
+
105
+ function list(value) {
106
+ return Array.isArray(value) ? value.map(field).filter(Boolean).slice(0, MAX_ITEMS) : [];
107
+ }
108
+
109
+ function isObject(value) {
110
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
111
+ }
112
+
113
+ /** Модели любят оборачивать JSON в ```json … ``` и пояснения — берём первый объект. */
114
+ function firstJson(text) {
115
+ const source = String(text ?? '');
116
+ for (let start = source.indexOf('{'); start !== -1; start = source.indexOf('{', start + 1)) {
117
+ let depth = 0;
118
+ let inString = false;
119
+ for (let i = start; i < source.length; i++) {
120
+ const c = source[i];
121
+ if (inString) {
122
+ if (c === '\\') i++;
123
+ else if (c === '"') inString = false;
124
+ } else if (c === '"') inString = true;
125
+ else if (c === '{') depth++;
126
+ else if (c === '}' && --depth === 0) {
127
+ try {
128
+ return JSON.parse(source.slice(start, i + 1));
129
+ } catch {
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /** Команда с заданием на stdin; ответ — stdout. Не уложилась во время — убита, урока нет. */
139
+ function run(cmd, args, input, { timeoutMs, env, cwd }) {
140
+ return new Promise((resolve, reject) => {
141
+ const child = spawn(cmd, args, { env, cwd, stdio: ['pipe', 'pipe', 'ignore'] });
142
+ let out = '';
143
+ const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
144
+ child.stdout.on('data', chunk => { out += chunk; });
145
+ child.stdin.on('error', () => {}); // команда могла выйти, не дочитав задание
146
+ child.on('error', e => { clearTimeout(timer); reject(e); });
147
+ child.on('close', code => {
148
+ clearTimeout(timer);
149
+ code === 0 ? resolve(out) : reject(new Error(`команда урока завершилась с кодом ${code}`));
150
+ });
151
+ child.stdin.end(input);
152
+ });
153
+ }
@@ -0,0 +1,160 @@
1
+ // Ход работы агента — из его построчного вывода: сколько шагов, какие виды работы, с
2
+ // какими ошибками столкнулся, какие файлы трогал. У каждого движка свой формат, разбор —
3
+ // свой; результат один.
4
+
5
+ import { ErrorLog, errorSignature } from './errors.js';
6
+ import { scrub } from './scrub.js';
7
+ import { classifyCommand } from './shell.js';
8
+
9
+ /** Виды работы, в выводе которых ошибка — это сбой, а не текст прочитанного файла. */
10
+ const CHECKED = new Set(['test', 'build', 'run', 'deps', 'db', 'infra', 'shell']);
11
+
12
+ /** Инструменты Claude Code → вид работы. Служебные (поиск инструментов) — не шаги. */
13
+ const CLAUDE_TOOLS = {
14
+ Read: 'read', Glob: 'search', Grep: 'search', LS: 'read',
15
+ Edit: 'edit', MultiEdit: 'edit', Write: 'edit', NotebookEdit: 'edit',
16
+ WebFetch: 'web', WebSearch: 'web', Task: 'delegate', Agent: 'delegate', TodoWrite: 'plan',
17
+ };
18
+ const CLAUDE_META = new Set(['ToolSearch']);
19
+
20
+ /** Разборщик на каждый запуск: у Claude он помнит вызовы, чтобы связать их с результатами. */
21
+ const PARSERS = { claude: () => new ClaudeParser(), codex: () => new CodexParser() };
22
+
23
+ /**
24
+ * @param {string} engine движок (claude, codex)
25
+ * @param {string} output построчный JSON-вывод агента (stdout записи запуска)
26
+ * @returns {{steps: number, tools: Record<string, number>, errors: {kind: string, text: string}[],
27
+ * touched: string[], tokens: {input: number, output: number} | null, cost_usd: number | null}}
28
+ * touched — пути файлов; в карточку не попадают, нужны, чтобы понять, чей это стек
29
+ */
30
+ export function summarizeRun(engine, output) {
31
+ const run = new RunSummary();
32
+ const parser = PARSERS[engine]?.();
33
+ if (parser) for (const event of events(output)) parser.feed(event, run);
34
+ return run.result();
35
+ }
36
+
37
+ class RunSummary {
38
+ steps = 0;
39
+ tools = {};
40
+ errors = new ErrorLog();
41
+ touched = new Set();
42
+ tokens = null;
43
+ cost = null;
44
+
45
+ step(kinds) {
46
+ this.steps++;
47
+ for (const kind of kinds) this.tools[kind] = (this.tools[kind] ?? 0) + 1;
48
+ }
49
+
50
+ /** Вывод шага: явный сбой — всегда ошибка; у проверяющих команд — если видна сигнатура. */
51
+ outcome(kinds, output, failed) {
52
+ const signature = errorSignature(output);
53
+ if (failed) this.errors.add(signature ?? { kind: 'error', text: firstLine(output) });
54
+ else if (signature && kinds.some(k => CHECKED.has(k))) this.errors.add(signature);
55
+ }
56
+
57
+ result() {
58
+ return {
59
+ steps: this.steps, tools: this.tools, errors: this.errors.list,
60
+ touched: [...this.touched], tokens: this.tokens, cost_usd: this.cost,
61
+ };
62
+ }
63
+ }
64
+
65
+ // --------------------------------------------------------------- claude --output-format stream-json
66
+
67
+ class ClaudeParser {
68
+ #calls = new Map(); // id вызова → виды работы: результат приходит отдельным событием
69
+
70
+ feed(event, run) {
71
+ for (const part of event.message?.content ?? []) {
72
+ if (part.type === 'tool_use' && !CLAUDE_META.has(part.name)) this.#call(part, run);
73
+ if (part.type === 'tool_result' && this.#calls.has(part.tool_use_id)) {
74
+ run.outcome(this.#calls.get(part.tool_use_id), text(part.content), part.is_error === true);
75
+ }
76
+ }
77
+ if (event.type === 'result') {
78
+ const u = event.usage ?? {};
79
+ run.tokens = {
80
+ input: (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0),
81
+ output: u.output_tokens ?? 0,
82
+ };
83
+ run.cost = typeof event.total_cost_usd === 'number' ? event.total_cost_usd : null;
84
+ }
85
+ }
86
+
87
+ #call(part, run) {
88
+ const kinds = part.name === 'Bash' ? classifyCommand(part.input?.command ?? '')
89
+ : part.name.startsWith('mcp__') ? [part.name.includes('chat') ? 'chat' : 'mcp']
90
+ : [CLAUDE_TOOLS[part.name] ?? 'other'];
91
+ this.#calls.set(part.id, kinds);
92
+ run.step(kinds);
93
+ for (const key of ['file_path', 'path', 'notebook_path']) if (part.input?.[key]) run.touched.add(part.input[key]);
94
+ }
95
+ }
96
+
97
+ // --------------------------------------------------------------- codex exec --json
98
+
99
+ class CodexParser {
100
+ feed(event, run) {
101
+ if (event.type === 'turn.completed' && event.usage) {
102
+ const u = event.usage;
103
+ run.tokens = {
104
+ input: (run.tokens?.input ?? 0) + (u.input_tokens ?? 0),
105
+ output: (run.tokens?.output ?? 0) + (u.output_tokens ?? 0) + (u.reasoning_output_tokens ?? 0),
106
+ };
107
+ }
108
+ if (event.type === 'item.completed') this.#item(event.item ?? {}, run);
109
+ }
110
+
111
+ #item(item, run) {
112
+ switch (item.type) {
113
+ case 'command_execution': {
114
+ const kinds = classifyCommand(item.command ?? '');
115
+ run.step(kinds);
116
+ run.outcome(kinds, item.aggregated_output, typeof item.exit_code === 'number' && item.exit_code !== 0);
117
+ break;
118
+ }
119
+ case 'file_change':
120
+ run.step(['edit']);
121
+ for (const change of item.changes ?? []) if (change.path) run.touched.add(change.path);
122
+ break;
123
+ case 'mcp_tool_call':
124
+ run.step([String(item.server ?? '').includes('chat') ? 'chat' : 'mcp']);
125
+ if (item.error) run.outcome(['mcp'], JSON.stringify(item.error), true);
126
+ break;
127
+ case 'web_search':
128
+ run.step(['web']);
129
+ break;
130
+ case 'todo_list':
131
+ run.step(['plan']);
132
+ break;
133
+ default:
134
+ break; // рассуждения и реплики агента — не шаги
135
+ }
136
+ }
137
+ }
138
+
139
+ // ---------------------------------------------------------------
140
+
141
+ function* events(output) {
142
+ for (const line of String(output ?? '').split('\n')) {
143
+ if (!line.trim()) continue;
144
+ try {
145
+ yield JSON.parse(line);
146
+ } catch {
147
+ // не JSON — служебная строка движка, пропускаем
148
+ }
149
+ }
150
+ }
151
+
152
+ function text(content) {
153
+ if (typeof content === 'string') return content;
154
+ if (Array.isArray(content)) return content.map(c => (typeof c === 'string' ? c : c?.text ?? '')).join('\n');
155
+ return '';
156
+ }
157
+
158
+ function firstLine(output) {
159
+ return scrub(String(output ?? '').trim().split('\n')[0] ?? '').slice(0, 120);
160
+ }
@@ -0,0 +1,29 @@
1
+ // Фильтр перед тем, как текст попадёт в карточку опыта: секреты и пути не должны покинуть
2
+ // машину. Срабатывает с запасом — лучше вырезать лишнее, чем пропустить ключ.
3
+
4
+ const SECRETS = [
5
+ /\b(?:gl(?:pat|dt|rt|ptt)|ghp|gho|ghu|ghs|ghr|github_pat)[-_][A-Za-z0-9_-]{10,}/g, // GitLab, GitHub
6
+ /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g, // Anthropic, OpenAI
7
+ /\bxox[abprs]-[A-Za-z0-9-]{10,}/g, // Slack
8
+ /\bAKIA[0-9A-Z]{16}\b/g, // AWS
9
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, // JWT
10
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
11
+ /\b[A-Za-z0-9+/_-]{40,}={0,2}/g, // длинные случайные строки — токены, хеши, ключи
12
+ ];
13
+
14
+ /** ключ=значение, где ключ звучит как секрет: PASSWORD=…, "api_key": "…", token: … */
15
+ const SECRET_ASSIGNMENT = /\b((?:[A-Za-z_][\w.-]*)?(?:pass(?:word)?|secret|token|api[_-]?key|private[_-]?key|credential)[\w.-]*)\b(["']?\s*[:=]\s*["']?)([^\s"',;]+)/gi;
16
+
17
+ /** URL со вписанными логином и паролем: scheme://user:pass@host */
18
+ const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi;
19
+
20
+ /** Абсолютные пути: в них имя пользователя и устройство его машины. */
21
+ const PATHS = /(?:\/(?:home|Users|root|tmp|var|opt|mnt|srv|workspace|work)\/[^\s"'`:,;)]+|[A-Z]:\\[^\s"'`:,;)]+)/g;
22
+
23
+ export function scrub(text) {
24
+ let out = String(text);
25
+ out = out.replace(URL_CREDENTIALS, '$1[скрыто]@');
26
+ out = out.replace(SECRET_ASSIGNMENT, '$1$2[скрыто]');
27
+ for (const pattern of SECRETS) out = out.replace(pattern, '[скрыто]');
28
+ return out.replace(PATHS, '<путь>');
29
+ }
@@ -0,0 +1,74 @@
1
+ // Что агент делал командой оболочки: читал, искал, гонял тесты, ставил зависимости…
2
+ // Сама команда в карточку не попадает — только вид работы.
3
+
4
+ /** Вид работы → признаки: первое слово команды или начало команды целиком. */
5
+ const KINDS = [
6
+ ['test', [
7
+ /^(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?test\b/, /^npx\s+(?:jest|vitest|mocha|playwright\s+test|cypress)\b/,
8
+ /^(?:jest|vitest|mocha|pytest|phpunit|pest|rspec|tox)\b/, /(?:^|\/)vendor\/bin\/(?:phpunit|pest)\b/,
9
+ /^php\s+artisan\s+test\b/, /^(?:go|cargo|dotnet|mvn|gradle|deno)\s+test\b/, /^node\s+--test\b/, /^python\d?\s+-m\s+(?:pytest|unittest)\b/,
10
+ ]],
11
+ ['build', [/^(?:npm|yarn|pnpm|bun)\s+run\s+build\b/, /^(?:tsc|make|webpack|vite\s+build)\b/, /^(?:go|cargo|dotnet)\s+build\b/, /^docker\s+(?:compose\s+)?build\b/]],
12
+ ['deps', [/^(?:npm|pnpm|bun)\s+(?:install|i|ci|add|update|remove)\b/, /^yarn(?:\s+(?:add|install|remove|upgrade))?$/, /^yarn\s+(?:add|install|remove|upgrade)\b/,
13
+ /^composer\s+(?:install|require|update|remove)\b/, /^pip\d?\s+install\b/, /^(?:go\s+get|cargo\s+add|bundle(?:\s+install)?)\b/, /^(?:poetry|uv)\s+(?:add|install|sync)\b/]],
14
+ ['db', [/^(?:mysql|psql|sqlite3|mongosh|redis-cli)\b/, /^php\s+artisan\s+(?:migrate|db:|tinker)/, /^(?:npx\s+)?(?:prisma|drizzle-kit|knex|sequelize)\b/, /^(?:alembic|flyway|liquibase)\b/]],
15
+ ['infra', [/^(?:docker|docker-compose|kubectl|helm|systemctl|journalctl|terraform)\b/]],
16
+ ['vcs', [/^git\b/, /^gh\b/, /^glab\b/]],
17
+ ['edit', [/^sed\s+(?:-\w*\s+)*-i/, /^(?:tee|patch|perl\s+-pi)\b/]],
18
+ ['search', [/^(?:grep|rg|ag|ack|find|fd|locate)\b/]],
19
+ ['read', [/^(?:cat|head|tail|less|more|sed|awk|ls|tree|wc|stat|file|jq|diff|nl|bat)\b/]],
20
+ ['net', [/^(?:curl|wget|http|ping|nc|ssh|scp)\b/]],
21
+ ['run', [/^(?:node|php|python\d?|ruby|go\s+run|cargo\s+run|deno|bun|npx|npm\s+run|yarn|pnpm)\b/]],
22
+ ];
23
+
24
+ /** Виды работы в команде; склеенная команда («ls; cat x; npm test») — по частям, без повторов. */
25
+ export function classifyCommand(command) {
26
+ const kinds = new Set();
27
+ for (const part of segments(unwrap(command))) {
28
+ const kind = KINDS.find(([, patterns]) => patterns.some(p => p.test(part)))?.[0] ?? 'shell';
29
+ kinds.add(kind);
30
+ }
31
+ return kinds.size ? [...kinds] : ['shell'];
32
+ }
33
+
34
+ /** «/bin/bash -lc "…"» — так Codex оборачивает каждую команду. */
35
+ function unwrap(command) {
36
+ const match = /^(?:\/(?:usr\/)?bin\/)?(?:ba|z)?sh\s+-l?c\s+(['"])([\s\S]*)\1$/.exec(command.trim());
37
+ return match ? match[2] : command;
38
+ }
39
+
40
+ /** Части команды по ; && || | и переводам строк — вне кавычек; без cd, sudo и присваиваний впереди. */
41
+ function segments(command) {
42
+ return splitOutsideQuotes(command)
43
+ .map(part => part.trim()
44
+ .replace(/^(?:sudo\s+|time\s+|(?:[A-Z_][A-Z0-9_]*=\S*\s+)+)+/, '')
45
+ .replace(/\s*[12]?>&?\s*\S+$/, ''))
46
+ .filter(part => part && !/^(?:cd|pwd|export|set|echo|true|false|sleep|printf|:)\b/.test(part));
47
+ }
48
+
49
+ function splitOutsideQuotes(command) {
50
+ const parts = [];
51
+ let current = '';
52
+ let quote = null;
53
+ for (let i = 0; i < command.length; i++) {
54
+ const c = command[i];
55
+ if (quote) {
56
+ if (c === quote) quote = null;
57
+ // \" внутри двойных кавычек не закрывает их
58
+ current += c === '\\' && quote === '"' ? c + (command[++i] ?? '') : c;
59
+ continue;
60
+ }
61
+ if (c === "'" || c === '"') {
62
+ quote = c;
63
+ current += c;
64
+ } else if (c === ';' || c === '\n' || c === '|' || (c === '&' && command[i + 1] === '&')) {
65
+ if (command[i + 1] === c) i++; // && и ||
66
+ parts.push(current);
67
+ current = '';
68
+ } else {
69
+ current += c;
70
+ }
71
+ }
72
+ parts.push(current);
73
+ return parts;
74
+ }