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,204 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{ chat.name }} — документы</title>
7
+ <style>
8
+ :root{--bg:#f6f7f9;--card:#fff;--line:#e4e7ec;--ink:#14171c;--muted:#6b7280;
9
+ --brand:#0057c2;--ok:#11a36b;--warn:#b45309;
10
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,sans-serif;}
11
+ *{box-sizing:border-box}
12
+ body{margin:0;background:var(--bg);color:var(--ink);font-size:15px;line-height:1.55}
13
+ header{background:var(--card);border-bottom:1px solid var(--line);padding:12px 20px;
14
+ display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:5}
15
+ header h1{font-size:15px;margin:0;font-weight:600}
16
+ header a{color:var(--muted);text-decoration:none;font-size:13px}
17
+ header a:hover{color:var(--brand)}
18
+ .layout{display:grid;grid-template-columns:380px minmax(0,1fr);gap:16px;
19
+ max-width:1300px;margin:0 auto;padding:16px 20px}
20
+ @media (max-width:900px){.layout{grid-template-columns:1fr}}
21
+ .panel{background:var(--card);border:1px solid var(--line);border-radius:12px}
22
+ .panel h2{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);
23
+ margin:0;padding:14px 16px;border-bottom:1px solid var(--line)}
24
+ .filters{display:flex;gap:6px;padding:10px 12px;border-bottom:1px solid var(--line);flex-wrap:wrap}
25
+ .filters button{border:1px solid var(--line);background:#fff;border-radius:99px;
26
+ padding:3px 10px;font:inherit;font-size:12px;cursor:pointer;color:var(--muted)}
27
+ .filters button.on{border-color:var(--brand);color:var(--brand);background:#f2f7ff}
28
+ #list{max-height:calc(100vh - 210px);overflow-y:auto}
29
+ .doc{padding:10px 16px;border-bottom:1px solid var(--line);cursor:pointer}
30
+ .doc:last-child{border-bottom:0}
31
+ .doc:hover{background:#fafbfc}
32
+ .doc.sel{background:#f2f7ff;box-shadow:inset 3px 0 0 var(--brand)}
33
+ .doc .t{font-weight:600;font-size:14px;word-break:break-word}
34
+ .doc .m{font-size:12px;color:var(--muted);margin-top:2px}
35
+ .pill{display:inline-block;font-size:11px;border-radius:99px;padding:0 7px;border:1px solid}
36
+ .st-draft{color:var(--muted);border-color:var(--line)}
37
+ .st-current{color:var(--ok);border-color:var(--ok)}
38
+ .st-archived{color:var(--warn);border-color:var(--warn)}
39
+ #view{padding:16px 20px;max-height:calc(100vh - 150px);overflow-y:auto}
40
+ #view pre{white-space:pre-wrap;word-wrap:break-word;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;
41
+ background:#fbfcfd;border:1px solid var(--line);border-radius:8px;padding:14px}
42
+ .bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid var(--line)}
43
+ .bar button{border:1px solid var(--line);background:#fff;border-radius:8px;padding:5px 12px;
44
+ font:inherit;font-size:13px;cursor:pointer}
45
+ .panel h2{display:flex;align-items:center}
46
+ .panel h2 button,.ed button{border:1px solid var(--line);background:#fff;border-radius:8px;padding:3px 11px;
47
+ font:inherit;font-size:13px;cursor:pointer;text-transform:none;letter-spacing:0;color:var(--ink)}
48
+ .panel h2 button{margin-left:auto}
49
+ .ed button.primary,.panel h2 button.primary{background:var(--brand);border-color:var(--brand);color:#fff;font-weight:600}
50
+ /* Редактор документа: markdown как есть, без визуального слоя — его же читают агенты. */
51
+ .ed{display:flex;flex-direction:column;gap:10px}
52
+ .ed .two{display:grid;grid-template-columns:1fr 1fr;gap:10px}
53
+ .ed label{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var(--muted)}
54
+ .ed input,.ed select,.ed textarea{font:inherit;color:var(--ink);border:1px solid var(--line);
55
+ border-radius:8px;padding:8px 10px;outline:none;background:#fff}
56
+ .ed input:disabled{background:#f6f7f9;color:var(--muted)}
57
+ .ed textarea{min-height:52vh;resize:vertical;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}
58
+ .ed .actions{display:flex;gap:8px;justify-content:flex-end;align-items:center}
59
+ .ed .err{color:#b91c1c;font-size:13px;margin-right:auto}
60
+ .bar button:hover{border-color:var(--brand);color:var(--brand)}
61
+ .bar .path{font-size:12px;color:var(--muted);margin-left:auto;font-family:ui-monospace,monospace}
62
+ .empty{padding:18px;color:var(--muted);font-size:13px}
63
+ .warn{background:#fef3c7;border:1px solid #fcd34d;border-radius:10px;padding:12px 16px;margin:16px auto;max-width:1260px}
64
+ </style>
65
+ </head>
66
+ <body>
67
+ <header>
68
+ <h1>{{ chat.name }} — документы</h1>
69
+ <a href="/c/{{ chat.slug }}">← в чат</a>
70
+ <span style="margin-left:auto;color:var(--muted);font-size:13px">{{ me.name }} · {{ me.role }}</span>
71
+ </header>
72
+
73
+ <div class="layout">
74
+ <section class="panel">
75
+ <h2>Список <button type="button" class="primary" id="newdoc">+ документ</button></h2>
76
+ <div class="filters" id="filters"></div>
77
+ <div id="list"><div class="empty">Загрузка…</div></div>
78
+ </section>
79
+ <section class="panel">
80
+ <div class="bar" id="bar"><span class="empty" style="padding:0">Выберите документ слева</span></div>
81
+ <div id="view"></div>
82
+ </section>
83
+ </div>
84
+
85
+ <script>
86
+ const TOKEN = {{json token}};
87
+ // Документы — свои у каждого чата-проекта.
88
+ const API = '/api/chats/' + encodeURIComponent({{json chat.slug}});
89
+ const IS_OWNER = {{json owner}};
90
+ const H = {'Authorization':'Bearer '+TOKEN,'Content-Type':'application/json','Accept':'application/json'};
91
+ const esc = s => (s??'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
92
+
93
+ let docs = [], filter = {status:'', kind:''}, selected = null, kinds = [], current = null;
94
+
95
+ function renderFilters(statuses, kinds){
96
+ const f = document.getElementById('filters');
97
+ const group = (name, values, current) => [`<button data-k="${name}" data-v="" class="${current?'':'on'}">все</button>`]
98
+ .concat(values.map(v => `<button data-k="${name}" data-v="${v}" class="${current===v?'on':''}">${v}</button>`)).join('');
99
+ f.innerHTML = group('status', statuses, filter.status) + '<span style="width:100%;height:0"></span>' + group('kind', kinds, filter.kind);
100
+ f.querySelectorAll('button').forEach(b => b.onclick = () => { filter[b.dataset.k] = b.dataset.v; load(); });
101
+ }
102
+
103
+ function renderList(){
104
+ const el = document.getElementById('list');
105
+ if (!docs.length){ el.innerHTML = '<div class="empty">Ничего не найдено.</div>'; return; }
106
+ el.innerHTML = docs.map(d => `
107
+ <div class="doc ${selected===d.slug?'sel':''}" data-slug="${esc(d.slug)}">
108
+ <div class="t">${esc(d.title)}</div>
109
+ <div class="m"><span class="pill st-${d.status}">${d.status}</span> · ${esc(d.kind)} · ревизия ${d.revision ?? '—'}</div>
110
+ <div class="m">${esc(d.origin ?? 'путь не задан')}</div>
111
+ </div>`).join('');
112
+ el.querySelectorAll('.doc').forEach(x => x.onclick = () => open(x.dataset.slug));
113
+ }
114
+
115
+ async function load(){
116
+ const q = new URLSearchParams(Object.entries(filter).filter(([,v]) => v)).toString();
117
+ const r = await fetch(API + '/documents' + (q?'?'+q:''), {headers:H});
118
+ const d = await r.json();
119
+ docs = d.documents;
120
+ kinds = d.kinds;
121
+ renderFilters(d.statuses, d.kinds);
122
+ renderList();
123
+ }
124
+
125
+ async function open(slug){
126
+ selected = slug; renderList();
127
+ const r = await fetch(API + '/documents/'+encodeURIComponent(slug), {headers:H});
128
+ const d = await r.json();
129
+ current = d;
130
+
131
+ // Подтверждать актуальность вправе только владелец — как и фиксировать решения.
132
+ const actions = IS_OWNER ? ['draft','current','archived']
133
+ .filter(s => s !== d.status)
134
+ .map(s => `<button data-st="${s}">${s === 'current' ? 'подтвердить (current)' : 'в '+s}</button>`).join('') : '';
135
+
136
+ document.getElementById('bar').innerHTML =
137
+ `<span class="pill st-${d.status}">${d.status}</span> ${actions}
138
+ <button data-edit="1">редактировать</button>
139
+ <span class="path">${esc(d.origin ?? 'путь не задан')}</span>`;
140
+ document.getElementById('view').innerHTML =
141
+ `<h3 style="margin:0 0 4px">${esc(d.title)}</h3>
142
+ <div class="empty" style="padding:0 0 12px">ревизия ${d.revision} · ${d.history.length} редакц.</div>
143
+ <pre>${esc(d.body)}</pre>`;
144
+
145
+ document.querySelector('[data-edit]').onclick = () => editor(current);
146
+ document.getElementById('bar').querySelectorAll('button[data-st]').forEach(b => b.onclick = async () => {
147
+ const res = await fetch(API + '/documents/'+encodeURIComponent(slug), {
148
+ method:'PATCH', headers:H, body: JSON.stringify({status: b.dataset.st})});
149
+ if (!res.ok){ alert('Не удалось: ' + res.status); return; }
150
+ await load(); open(slug);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Новый документ или новая редакция. Прошлые редакции не переписываются — сервер
156
+ * добавляет ревизию. Правка подтверждённого документа не-владельцем вернёт его в черновик.
157
+ */
158
+ function editor(doc){
159
+ selected = doc ? doc.slug : null; renderList();
160
+ document.getElementById('bar').innerHTML = `<b>${doc ? 'Новая редакция' : 'Новый документ'}</b>
161
+ ${doc ? `<span class="path">сейчас ревизия ${doc.revision}</span>` : ''}`;
162
+ const view = document.getElementById('view');
163
+ view.innerHTML = `<form class="ed" id="edform">
164
+ <div class="two">
165
+ <label>Заголовок<input name="title" required maxlength="300"></label>
166
+ <label>Адрес (латиница, цифры, точка, дефис)<input name="slug" required maxlength="200" pattern="[a-z0-9][a-z0-9._\-]*" placeholder="plan-q4"></label>
167
+ </div>
168
+ <div class="two">
169
+ <label>Тип<select name="kind">${kinds.map(k => `<option>${esc(k)}</option>`).join('')}</select></label>
170
+ <label>Путь при выгрузке в репозиторий (необязательно)<input name="origin" maxlength="300" placeholder="qurbuz-backend/docs/plan.md"></label>
171
+ </div>
172
+ <label>Текст (markdown)<textarea name="body" required></textarea></label>
173
+ <label>Чем вызвана правка (необязательно)<input name="note" maxlength="300"></label>
174
+ <div class="actions"><span class="err" id="ederr"></span>
175
+ <button type="button" id="edcancel">отмена</button>
176
+ <button type="submit" class="primary">Сохранить</button></div>
177
+ </form>`;
178
+ const f = document.getElementById('edform');
179
+ if (doc){
180
+ f.title.value = doc.title; f.slug.value = doc.slug; f.slug.disabled = true;
181
+ f.kind.value = doc.kind; f.origin.value = doc.origin || ''; f.body.value = doc.body || '';
182
+ }
183
+ f.title.focus();
184
+ document.getElementById('edcancel').onclick = () => doc ? open(doc.slug) : (view.innerHTML = '', document.getElementById('bar').innerHTML = '<span class="empty" style="padding:0">Выберите документ слева</span>');
185
+ f.onsubmit = async e => {
186
+ e.preventDefault();
187
+ const data = {slug: f.slug.value.trim(), title: f.title.value.trim(), kind: f.kind.value,
188
+ body: f.body.value, origin: f.origin.value.trim() || null, note: f.note.value.trim() || null};
189
+ const r = await fetch(API + '/documents', {method:'POST', headers:H, body:JSON.stringify(data)});
190
+ const d = await r.json().catch(() => null);
191
+ if (!r.ok){
192
+ document.getElementById('ederr').textContent = d && d.errors ? Object.values(d.errors).flat().join(' ') : ((d && d.message) || 'Ошибка ' + r.status);
193
+ return;
194
+ }
195
+ await load(); open(d.slug);
196
+ };
197
+ }
198
+
199
+ document.getElementById('newdoc').onclick = () => editor(null);
200
+
201
+ load();
202
+ </script>
203
+ </body>
204
+ </html>
@@ -0,0 +1,31 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Вход — командный чат</title>
7
+ <style>
8
+ :root{--bg:#f6f7f9;--card:#fff;--line:#e4e7ec;--ink:#14171c;--muted:#6b7280;--brand:#0057c2;
9
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,sans-serif}
10
+ body{margin:0;background:var(--bg);color:var(--ink);font-size:15px;line-height:1.55;
11
+ min-height:100vh;display:grid;place-items:center;padding:16px;box-sizing:border-box}
12
+ .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:24px 28px;max-width:480px}
13
+ h1{font-size:18px;margin:0 0 8px}
14
+ p{margin:8px 0;color:var(--muted)}
15
+ code{display:block;background:#f1f3f6;border-radius:8px;padding:8px 12px;margin:8px 0;color:var(--ink);
16
+ font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <div class="card">
21
+ <h1>Вход по ссылке</h1>
22
+ {{#expired}}
23
+ <p>Вход в этом браузере больше не действует — нужна новая ссылка.</p>
24
+ {{/expired}}{{#used}}
25
+ <p>Ссылка уже использована или истекла (живёт 15 минут).</p>
26
+ {{/used}}
27
+ <p>Чат открывается по одноразовой ссылке. Получить её можно на машине, где он запущен:</p>
28
+ <code>team-chat login</code>
29
+ </div>
30
+ </body>
31
+ </html>
@@ -0,0 +1,60 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Первый чат — командный чат</title>
7
+ <style>
8
+ :root{--bg:#f6f7f9;--card:#fff;--line:#e4e7ec;--ink:#14171c;--muted:#6b7280;--brand:#0057c2;
9
+ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,sans-serif}
10
+ *{box-sizing:border-box}
11
+ body{margin:0;background:var(--bg);color:var(--ink);font-size:15px;line-height:1.55;
12
+ min-height:100vh;display:grid;place-items:center;padding:16px}
13
+ .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:24px 28px;width:min(560px,100%)}
14
+ h1{font-size:18px;margin:0 0 6px}
15
+ p{margin:0 0 14px;color:var(--muted)}
16
+ form{display:flex;flex-direction:column;gap:12px}
17
+ label{display:flex;flex-direction:column;gap:4px;font-size:13px;color:var(--muted)}
18
+ input,textarea{font:inherit;color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 10px;outline:none}
19
+ input:focus,textarea:focus{border-color:var(--brand)}
20
+ textarea{min-height:110px;resize:vertical}
21
+ button{align-self:flex-end;background:var(--brand);color:#fff;border:0;border-radius:8px;padding:9px 18px;
22
+ font:inherit;font-weight:600;cursor:pointer}
23
+ .err{color:#b91c1c;font-size:13px;min-height:1em}
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <div class="card">
28
+ {{#owner}}
29
+ <h1>Первый чат</h1>
30
+ <p>Чат — это проект: у него свой тред, журнал решений, документы и описание для агентов.
31
+ Потом можно завести сколько угодно.</p>
32
+ <form id="f">
33
+ <label>Название проекта<input name="name" required maxlength="100" placeholder="Мой проект" autofocus></label>
34
+ <label>Каталог с кодом проекта на этой машине — агенты будут работать в нём (необязательно)
35
+ <input name="workdir" maxlength="500" placeholder="/home/…/projects/my-project"></label>
36
+ <label>О проекте — что это, какие репозитории, где главное. Уходит агенту в каждое задание (необязательно)
37
+ <textarea name="description" maxlength="5000"></textarea></label>
38
+ <div class="err" id="err"></div>
39
+ <button type="submit">Создать</button>
40
+ </form>
41
+ <script>
42
+ const f = document.getElementById('f');
43
+ f.onsubmit = async e => {
44
+ e.preventDefault();
45
+ const r = await fetch('/api/chats', {method:'POST', headers:{'Authorization':'Bearer '+{{json token}},
46
+ 'Content-Type':'application/json','Accept':'application/json'},
47
+ body: JSON.stringify({name: f.name.value.trim(), workdir: f.workdir.value.trim() || null,
48
+ description: f.description.value.trim() || null})});
49
+ const d = await r.json().catch(() => null);
50
+ if (!r.ok){ document.getElementById('err').textContent = d && d.errors ? Object.values(d.errors).flat().join(' ') : 'Ошибка ' + r.status; return; }
51
+ location.href = '/c/' + encodeURIComponent(d.chat.slug);
52
+ };
53
+ </script>
54
+ {{/owner}}{{^owner}}
55
+ <h1>Чатов пока нет</h1>
56
+ <p>Первый чат заводит владелец. Загляните позже.</p>
57
+ {{/owner}}
58
+ </div>
59
+ </body>
60
+ </html>