zames_pro 1.1.0 → 1.2.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.
package/src/input.js ADDED
@@ -0,0 +1,447 @@
1
+ import { theme } from './theme.js'
2
+ import { renderMarkdown } from './markdown.js'
3
+ import { randomThinkingPhrase, stripEllipsis } from './spinner.js'
4
+
5
+ // Постоянная строка ввода внизу терминала + область статуса/вывода над ней.
6
+ //
7
+ // Зачем свой редактор:
8
+ // * строка ввода видна ВСЕГДА и не затирается выводом агента;
9
+ // * спиннер/статус и ответы печатаются ВЫШЕ строки ввода (printAbove);
10
+ // * перерисовка учитывает перенос по ширине терминала, поэтому нет
11
+ // дублирования строк при многострочном вводе;
12
+ // * многострочный ввод: Enter отправляет, Ctrl+J (и Shift+Enter в
13
+ // терминалах с расширенным протоколом) вставляют перевод строки.
14
+ //
15
+ // Все управляющие символы собираются из кодов, чтобы в файле не было
16
+ // «сырых» ESC/CR/LF в строковых литералах (см. AGENTS.md).
17
+
18
+ const NL = String.fromCharCode(10)
19
+ const CR = String.fromCharCode(13)
20
+ const ESC = String.fromCharCode(27)
21
+ const PASTE_START = ESC + '[200~'
22
+ const PASTE_END = ESC + '[201~'
23
+
24
+ const DOTS = ['.', '..', '...']
25
+ const HINT = theme.dim(' · Enter — отправить, Ctrl+J — новая строка, Esc — стоп')
26
+
27
+ function safeJson(v) {
28
+ try {
29
+ return JSON.stringify(v)
30
+ } catch {
31
+ return String(v)
32
+ }
33
+ }
34
+
35
+ // Видимая длина строки без ANSI-последовательностей.
36
+ export function visLen(s) {
37
+ s = String(s)
38
+ let n = 0
39
+ let i = 0
40
+ while (i < s.length) {
41
+ if (s.charCodeAt(i) === 27) {
42
+ i++
43
+ if (s[i] === '[') {
44
+ i++
45
+ while (i < s.length && !/[A-Za-z]/.test(s[i])) i++
46
+ i++
47
+ }
48
+ continue
49
+ }
50
+ n++
51
+ i++
52
+ }
53
+ return n
54
+ }
55
+
56
+ // Раскладка ввода на визуальные строки с учётом ширины терминала.
57
+ // Возвращает строки (с префиксом) и позицию курсора в визуальных координатах.
58
+ export function layoutInput(promptStr, buf, cursor, cols) {
59
+ const width = Math.max(20, Number(cols) || 80)
60
+ const promptW = visLen(promptStr)
61
+ const pad = ' '.repeat(promptW)
62
+ const chars = Array.from(String(buf))
63
+ const cur = Math.max(0, Math.min(Number(cursor) || 0, chars.length))
64
+ const rows = []
65
+ let i = 0
66
+ while (true) {
67
+ const first = rows.length === 0
68
+ const prefix = first ? promptStr : pad
69
+ const avail = Math.max(1, width - promptW)
70
+ let text = ''
71
+ let count = 0
72
+ const start = i
73
+ while (i < chars.length && chars[i] !== NL && count < avail) {
74
+ text += chars[i]
75
+ i++
76
+ count++
77
+ }
78
+ if (i < chars.length && chars[i] === NL) {
79
+ i++
80
+ rows.push({ prefix, text, start })
81
+ continue
82
+ }
83
+ rows.push({ prefix, text, start })
84
+ if (i >= chars.length) break
85
+ }
86
+
87
+ let cursorRow = 0
88
+ let cursorCol = promptW
89
+ for (let r = 0; r < rows.length; r++) {
90
+ const row = rows[r]
91
+ const len = Array.from(row.text).length
92
+ const from = row.start
93
+ const to = row.start + len
94
+ if (cur >= from && cur <= to) {
95
+ const next = rows[r + 1]
96
+ if (cur === to && next && next.start === to) continue
97
+ cursorRow = r
98
+ cursorCol = promptW + (cur - from)
99
+ break
100
+ }
101
+ }
102
+ return { rows, cursorRow, cursorCol }
103
+ }
104
+
105
+ export class LineEditor {
106
+ constructor({ prompt = '> ' } = {}) {
107
+ this.promptStr = prompt
108
+ this.buf = ''
109
+ this.cursor = 0
110
+ this.statusText = ''
111
+ this.pendingText = null
112
+ this.rendered = false
113
+ this.cursorRowFromTop = 0
114
+ this.busy = false
115
+ this.onSubmit = null
116
+ this.onEscape = null
117
+ this.onCtrlC = null
118
+ this._inPaste = false
119
+ this._dotTimer = null
120
+ this._dotPhase = 0
121
+ this._thinkBase = ''
122
+ this._wasRaw = false
123
+ this._onData = (b) => this._handle(b)
124
+ }
125
+
126
+ start() {
127
+ const stdin = process.stdin
128
+ if (!stdin.isTTY || !stdin.setRawMode) return false
129
+ this._wasRaw = stdin.isRaw
130
+ stdin.setRawMode(true)
131
+ stdin.resume()
132
+ process.stdout.write(ESC + '[?2004h')
133
+ stdin.on('data', this._onData)
134
+ this._writeBlock()
135
+ return true
136
+ }
137
+
138
+ dispose() {
139
+ const stdin = process.stdin
140
+ stdin.removeListener('data', this._onData)
141
+ process.stdout.write(ESC + '[?2004l')
142
+ this._stopDots()
143
+ if (this.rendered) this._eraseBlock()
144
+ if (stdin.setRawMode) stdin.setRawMode(this._wasRaw || false)
145
+ }
146
+
147
+ setPrompt(str) {
148
+ this.promptStr = str
149
+ this._render()
150
+ }
151
+
152
+ clear() {
153
+ this.buf = ''
154
+ this.cursor = 0
155
+ this._render()
156
+ }
157
+
158
+ // ---------- отрисовка ----------
159
+
160
+ _eraseBlock() {
161
+ if (!this.rendered) return
162
+ if (this.cursorRowFromTop > 0) {
163
+ process.stdout.write(ESC + '[' + this.cursorRowFromTop + 'A')
164
+ }
165
+ process.stdout.write(CR + ESC + '[J')
166
+ this.rendered = false
167
+ }
168
+
169
+ _writeBlock() {
170
+ const cols = process.stdout.columns || 80
171
+ let out = ''
172
+ let top = 0
173
+ if (this.statusText) {
174
+ out += this.statusText + NL
175
+ top = 1
176
+ }
177
+ const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols)
178
+ out += lay.rows.map((r) => r.prefix + r.text).join(NL)
179
+ process.stdout.write(out)
180
+ const lastRow = lay.rows.length - 1
181
+ const up = lastRow - lay.cursorRow
182
+ if (up > 0) process.stdout.write(ESC + '[' + up + 'A')
183
+ process.stdout.write(CR)
184
+ if (lay.cursorCol > 0) process.stdout.write(ESC + '[' + lay.cursorCol + 'C')
185
+ this.rendered = true
186
+ this.cursorRowFromTop = top + lay.cursorRow
187
+ }
188
+
189
+ _render() {
190
+ this._eraseBlock()
191
+ this._writeBlock()
192
+ }
193
+
194
+ // Напечатать блок ВЫШЕ строки ввода и вернуть строку ввода на место.
195
+ printAbove(text) {
196
+ this._eraseBlock()
197
+ process.stdout.write(String(text) + NL)
198
+ this._writeBlock()
199
+ }
200
+
201
+ // ---------- интерфейс, совместимый со спиннером ----------
202
+
203
+ setStatus(text) {
204
+ this.statusText = text || ''
205
+ this._render()
206
+ }
207
+
208
+ _stopDots() {
209
+ if (this._dotTimer) {
210
+ clearInterval(this._dotTimer)
211
+ this._dotTimer = null
212
+ }
213
+ }
214
+
215
+ _startThinking() {
216
+ if (this.pendingText) {
217
+ this.setStatus(theme.prompt('✎ ') + this.pendingText + HINT)
218
+ return
219
+ }
220
+ this._thinkBase = theme.brown(stripEllipsis(randomThinkingPhrase()))
221
+ this._dotPhase = 0
222
+ this.setStatus(this._thinkBase + DOTS[0] + HINT)
223
+ this._stopDots()
224
+ this._dotTimer = setInterval(() => {
225
+ this._dotPhase = (this._dotPhase + 1) % DOTS.length
226
+ this.setStatus(this._thinkBase + DOTS[this._dotPhase] + HINT)
227
+ }, 400)
228
+ if (this._dotTimer.unref) this._dotTimer.unref()
229
+ }
230
+
231
+ thinking() {
232
+ this._startThinking()
233
+ }
234
+
235
+ setPending(text) {
236
+ this.pendingText = text && String(text).length ? String(text) : null
237
+ if (this.pendingText) {
238
+ this._stopDots()
239
+ this.setStatus(theme.prompt('✎ ') + this.pendingText + HINT)
240
+ } else {
241
+ this._startThinking()
242
+ }
243
+ }
244
+
245
+ stop() {
246
+ this._stopDots()
247
+ this.setStatus('')
248
+ }
249
+
250
+ toolCall(name, args) {
251
+ const preview = safeJson(args).slice(0, 120)
252
+ this.printAbove(theme.tool('🔧 ' + name) + ' ' + theme.dim(preview))
253
+ }
254
+
255
+ toolResult(result) {
256
+ const text = typeof result === 'string' ? result : safeJson(result)
257
+ const preview = text.slice(0, 200).split(NL).join(' ↵ ')
258
+ this.printAbove(theme.toolResult(' → ' + preview))
259
+ }
260
+
261
+ assistant(msg) {
262
+ this.stop()
263
+ const rendered = renderMarkdown(msg)
264
+ this.printAbove(
265
+ NL + theme.assistant('● Ответ') + NL + rendered + NL + theme.dim('─'.repeat(60)),
266
+ )
267
+ }
268
+
269
+ // ---------- ввод ----------
270
+
271
+ _submit() {
272
+ const text = this.buf
273
+ if (!text.trim()) {
274
+ this._render()
275
+ return
276
+ }
277
+ this.buf = ''
278
+ this.cursor = 0
279
+ this.pendingText = null
280
+ this._stopDots()
281
+ this.statusText = ''
282
+ this.printAbove(theme.user('❯ ') + text)
283
+ if (this.onSubmit) this.onSubmit(text)
284
+ }
285
+
286
+ _insert(text) {
287
+ const chars = Array.from(this.buf)
288
+ const ins = Array.from(String(text))
289
+ const next = chars.slice(0, this.cursor).concat(ins, chars.slice(this.cursor))
290
+ this.buf = next.join('')
291
+ this.cursor += ins.length
292
+ }
293
+
294
+ _backspace() {
295
+ if (this.cursor <= 0) return
296
+ const chars = Array.from(this.buf)
297
+ chars.splice(this.cursor - 1, 1)
298
+ this.buf = chars.join('')
299
+ this.cursor--
300
+ }
301
+
302
+ _delete() {
303
+ const chars = Array.from(this.buf)
304
+ if (this.cursor >= chars.length) return
305
+ chars.splice(this.cursor, 1)
306
+ this.buf = chars.join('')
307
+ }
308
+
309
+ _left() {
310
+ if (this.cursor > 0) this.cursor--
311
+ }
312
+
313
+ _right() {
314
+ if (this.cursor < Array.from(this.buf).length) this.cursor++
315
+ }
316
+
317
+ _home() {
318
+ const chars = Array.from(this.buf)
319
+ let i = this.cursor
320
+ while (i > 0 && chars[i - 1] !== NL) i--
321
+ this.cursor = i
322
+ }
323
+
324
+ _end() {
325
+ const chars = Array.from(this.buf)
326
+ let i = this.cursor
327
+ while (i < chars.length && chars[i] !== NL) i++
328
+ this.cursor = i
329
+ }
330
+
331
+ _up() {
332
+ const chars = Array.from(this.buf)
333
+ let start = this.cursor
334
+ while (start > 0 && chars[start - 1] !== NL) start--
335
+ if (start === 0) return
336
+ const col = this.cursor - start
337
+ const prevEnd = start - 1
338
+ let prevStart = prevEnd
339
+ while (prevStart > 0 && chars[prevStart - 1] !== NL) prevStart--
340
+ this.cursor = prevStart + Math.min(col, prevEnd - prevStart)
341
+ }
342
+
343
+ _down() {
344
+ const chars = Array.from(this.buf)
345
+ let end = this.cursor
346
+ while (end < chars.length && chars[end] !== NL) end++
347
+ if (end >= chars.length) return
348
+ let lineStart = this.cursor
349
+ while (lineStart > 0 && chars[lineStart - 1] !== NL) lineStart--
350
+ const col = this.cursor - lineStart
351
+ const nextStart = end + 1
352
+ let nextEnd = nextStart
353
+ while (nextEnd < chars.length && chars[nextEnd] !== NL) nextEnd++
354
+ this.cursor = nextStart + Math.min(col, nextEnd - nextStart)
355
+ }
356
+
357
+ _handle(data) {
358
+ let s = data.toString('utf-8')
359
+
360
+ if (!this._inPaste && s === ESC) {
361
+ if (this.onEscape) this.onEscape()
362
+ return
363
+ }
364
+
365
+ while (s.length) {
366
+ if (this._inPaste) {
367
+ const end = s.indexOf(PASTE_END)
368
+ if (end === -1) {
369
+ this._insert(s)
370
+ s = ''
371
+ } else {
372
+ this._insert(s.slice(0, end))
373
+ s = s.slice(end + PASTE_END.length)
374
+ this._inPaste = false
375
+ }
376
+ this._render()
377
+ continue
378
+ }
379
+
380
+ const pasteAt = s.indexOf(PASTE_START)
381
+ if (pasteAt !== -1) {
382
+ const before = s.slice(0, pasteAt)
383
+ s = s.slice(pasteAt + PASTE_START.length)
384
+ this._inPaste = true
385
+ if (before) {
386
+ this._insert(before)
387
+ this._render()
388
+ }
389
+ continue
390
+ }
391
+
392
+ // Shift+Enter в терминалах с расширенным протоколом и Alt+Enter.
393
+ if (s.startsWith(ESC + '[13;2u')) { this._insert(NL); s = s.slice(7); this._render(); continue }
394
+ if (s.startsWith(ESC + '[27;2;13~')) { this._insert(NL); s = s.slice(10); this._render(); continue }
395
+ if (s.startsWith(ESC + NL)) { this._insert(NL); s = s.slice(2); this._render(); continue }
396
+
397
+ const ch = s[0]
398
+ const code = s.charCodeAt(0)
399
+ s = s.slice(1)
400
+
401
+ if (ch === CR) { this._submit(); continue }
402
+ if (code === 10) { this._insert(NL); this._render(); continue }
403
+ if (code === 3) { if (this.onCtrlC) this.onCtrlC(); continue }
404
+ if (code === 4) { if (!this.buf && this.onCtrlC) this.onCtrlC(); continue }
405
+ if (code === 1) { this._home(); this._render(); continue }
406
+ if (code === 5) { this._end(); this._render(); continue }
407
+ if (code === 21) { this.buf = ''; this.cursor = 0; this._render(); continue }
408
+ if (code === 127 || code === 8) { this._backspace(); this._render(); continue }
409
+
410
+ if (code === 27) {
411
+ if (s.startsWith('[D')) { this._left(); s = s.slice(2); this._render(); continue }
412
+ if (s.startsWith('[C')) { this._right(); s = s.slice(2); this._render(); continue }
413
+ if (s.startsWith('[A')) { this._up(); s = s.slice(2); this._render(); continue }
414
+ if (s.startsWith('[B')) { this._down(); s = s.slice(2); this._render(); continue }
415
+ if (s.startsWith('[H') || s.startsWith('[1~')) { this._home(); s = s.slice(s.startsWith('[1~') ? 3 : 2); this._render(); continue }
416
+ if (s.startsWith('[F') || s.startsWith('[4~')) { this._end(); s = s.slice(s.startsWith('[4~') ? 3 : 2); this._render(); continue }
417
+ if (s.startsWith('[3~')) { this._delete(); s = s.slice(3); this._render(); continue }
418
+ let j = 0
419
+ while (j < s.length && !/[A-Za-z~]/.test(s[j])) j++
420
+ s = s.slice(j + 1)
421
+ continue
422
+ }
423
+
424
+ if (code < 32) continue
425
+ this._insert(ch)
426
+ this._render()
427
+ }
428
+ }
429
+ }
430
+
431
+ // Проверка раскладки: node src/input.js --selftest
432
+ export function selftest() {
433
+ const cases = [
434
+ { p: '> ', b: 'hello', c: 5, w: 80 },
435
+ { p: '> ', b: 'a' + NL + 'b', c: 3, w: 80 },
436
+ { p: '> ', b: 'x'.repeat(200), c: 200, w: 40 },
437
+ { p: '> ', b: '', c: 0, w: 40 },
438
+ ]
439
+ for (const t of cases) {
440
+ const r = layoutInput(t.p, t.b, t.c, t.w)
441
+ console.log(JSON.stringify({ rows: r.rows.length, row: r.cursorRow, col: r.cursorCol }))
442
+ }
443
+ }
444
+
445
+ if (process.argv[1] && process.argv[1].endsWith('input.js') && process.argv.includes('--selftest')) {
446
+ selftest()
447
+ }
package/src/spinner.js CHANGED
@@ -2,8 +2,56 @@ import ora from 'ora'
2
2
  import { theme } from './theme.js'
3
3
  import { renderMarkdown } from './markdown.js'
4
4
 
5
+ // Фразы для спиннера «агент работает». Выбираются в случайном порядке.
6
+ const THINKING_PHRASES = [
7
+ 'Замешиваю кал…',
8
+ 'Взбиваю кал до однородной массы…',
9
+ 'Мешаю кал с логикой…',
10
+ 'Взбалтываю кал в коктейль…',
11
+ 'Кручу венчиком по калу…',
12
+ 'Смешиваю кал с кофеином…',
13
+ 'Замешиваю тесто из кала…',
14
+ 'Взбиваю пену из кала…',
15
+ 'Перемешиваю кал лопатой…',
16
+ 'Замешиваю глину из кала…',
17
+ 'Гомогенизирую кал до состояния бетона…',
18
+ 'Взбалтываю кал до просветления…',
19
+ 'Блендерю кал в смузи…',
20
+ 'Замешиваю цемент из кала…',
21
+ 'Взбиваю кашу из кала…',
22
+ 'Мешаю кал с надеждой…',
23
+ 'Взбалтываю осадок из кала…',
24
+ 'Кручу блендером по калу…',
25
+ 'Смешиваю кал до неразличимости…',
26
+ 'Замешиваю раствор из кала…',
27
+ 'Взбиваю коктейль «кал»…',
28
+ 'Перемешиваю кал лопатой дедлайна…',
29
+ 'Замешиваю кашу из кала…',
30
+ 'Взбиваю пенку из кала…',
31
+ 'Гомогенизирую кал до однородности…',
32
+ 'Взбалтываю кал в бетономешалке…',
33
+ 'Замешиваю кал в тесто…',
34
+ 'Сбиваю кал в однородную массу…',
35
+ 'Размешиваю кал до просветления…',
36
+ 'Замешиваю кал из всего подряд…',
37
+ ]
38
+
39
+ export function randomThinkingPhrase() {
40
+ return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)]
41
+ }
42
+
43
+ // Убираем завершающее многоточие из фразы — точки анимируем отдельно.
44
+ export function stripEllipsis(phrase) {
45
+ return phrase.replace(/[.…]+\s*$/, '')
46
+ }
47
+
5
48
  export function createSpinner() {
6
49
  let spinner = null
50
+ let dotTimer = null
51
+ let dotPhase = 0
52
+ let pending = null
53
+
54
+ const DOTS = ['.', '..', '...']
7
55
 
8
56
  const start = (text) => {
9
57
  if (!spinner) spinner = ora(text).start()
@@ -11,11 +59,59 @@ export function createSpinner() {
11
59
  }
12
60
 
13
61
  const stop = () => {
62
+ if (dotTimer) {
63
+ clearInterval(dotTimer)
64
+ dotTimer = null
65
+ }
14
66
  if (spinner) spinner.stop()
15
67
  }
16
68
 
69
+ // Подсказка в строке статуса: во время работы агента терминал живой,
70
+ // можно печатать следующее сообщение. Без неё это неочевидно.
71
+ const HINT = theme.dim(' · печатай — Enter в очередь, Esc стоп')
72
+
73
+ // Запуск анимированного статуса: коричневый текст + «бегущие» точки.
74
+ const startThinking = () => {
75
+ const base = theme.brown(stripEllipsis(randomThinkingPhrase()))
76
+ start(base + DOTS[0] + HINT)
77
+ dotPhase = 0
78
+ if (dotTimer) clearInterval(dotTimer)
79
+ dotTimer = setInterval(() => {
80
+ if (!spinner) return
81
+ dotPhase = (dotPhase + 1) % DOTS.length
82
+ spinner.text = base + DOTS[dotPhase] + HINT
83
+ }, 400)
84
+ if (dotTimer.unref) dotTimer.unref()
85
+ }
86
+
87
+ // Показать набранный, но ещё не отправленный текст вместо спиннера.
88
+ // Позволяет печатать сообщение прямо во время работы агента.
89
+ const showPending = () => {
90
+ if (dotTimer) {
91
+ clearInterval(dotTimer)
92
+ dotTimer = null
93
+ }
94
+ if (!spinner) spinner = ora('')
95
+ spinner.start()
96
+ spinner.text = theme.prompt('✎ ') + pending + HINT
97
+ }
98
+
17
99
  return {
18
- thinking: () => start(theme.system('Думаю...')),
100
+ thinking: () => {
101
+ if (pending) {
102
+ showPending()
103
+ return
104
+ }
105
+ startThinking()
106
+ },
107
+
108
+ // Текст, который пользователь набирает во время работы агента.
109
+ // Пустая строка / null — вернуть обычный спиннер.
110
+ setPending: (text) => {
111
+ pending = text && String(text).length ? String(text) : null
112
+ if (pending) showPending()
113
+ else startThinking()
114
+ },
19
115
 
20
116
  toolCall: (name, args) => {
21
117
  stop()
@@ -14,6 +14,34 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null }) {
14
14
 
15
15
  You work in the directory: ${workdir}
16
16
 
17
+ ## SILENT OPERATION (most important rule)
18
+
19
+ Work silently. While you are still solving the task you must output ONLY
20
+ tool calls — nothing else. Do NOT narrate what you are about to do, do NOT
21
+ summarize what you just did, do NOT think out loud, do NOT print progress
22
+ like "Now I will read the file...". Every extra sentence pollutes the chat
23
+ and the context, so keep it to zero.
24
+
25
+ The operator does NOT read free text in the chat. Anything written outside a
26
+ tool call is lost: the operator only sees tool calls and their results. In
27
+ particular, the operator reads EXACTLY ONE thing from you — the message of the
28
+ LAST respond tool call before you stop. Everything else is ignored.
29
+
30
+ Therefore the only way to talk to the operator is the respond tool, and it
31
+ must be your LAST action. Call respond in exactly two situations:
32
+ 1. The task is done — report the result.
33
+ 2. You cannot continue and must ask the operator a question or a decision.
34
+
35
+ Never call respond in the middle, and never split a report across several
36
+ messages: put the complete operator-facing text into that single final
37
+ respond. Do NOT end a turn with plain text, and do NOT rely on plain text to
38
+ ask or answer anything — if you have something to say, say it via respond and
39
+ stop. If you are still working, emit a tool call instead.
40
+
41
+ So the pattern is: tool call, tool call, tool call, ..., then a single final
42
+ respond. A bare text message without a tool call ends the task and the
43
+ operator will not read it, so never use plain text.\n\nNO PROSE AROUND TOOL CALLS. Each turn must contain ONLY the JSON of the tool\ncall(s) — not a single word before or after, not even a short lead-in like\n"Let me check..." or "Now I'll fix it.". The JSON must be the entire response.\n\nWRONG: "Let me read the file first." then a Read call.\nWRONG: a Read call then "I'll analyze the result next."\nRIGHT: only the JSON of the tool call, nothing else.\n\nIf you feel the urge to explain, do not: put it in the final respond when the\ntask is done (or when you must ask the operator), not between tool calls.
44
+
17
45
  You have access to the following tools:
18
46
 
19
47
  ${toolDescriptions}
@@ -28,7 +56,9 @@ To call MULTIPLE tools at once (only if they are independent — e.g. reading se
28
56
 
29
57
  [{"tool": "Read", "args": {"path": "a.js"}}, {"tool": "Read", "args": {"path": "b.js"}}]
30
58
 
31
- After the tool result(s) come back, decide the next action.
59
+ After the tool result(s) come back, decide the next action and immediately
60
+ emit the next tool call as bare JSON. Do NOT write a sentence about what
61
+ you learned or what you will do next — no commentary between tool calls.
32
62
 
33
63
  To give a final answer to the user, respond with:
34
64
 
@@ -68,9 +98,9 @@ Inside that string:
68
98
  - Do NOT write any text before or after the JSON. No explanations, no greetings, no plans.
69
99
  - Do NOT wrap JSON in markdown fences.
70
100
  - Do NOT use XML-like tags such as tool_calls, invoke, parameter, or their DSML variants.
71
- - If you want to tell the user something, call the respond tool with a message.
72
- - If JSON parsing fails, the tool call will NOT run and the user will see raw text. Keep JSON valid: escape every double quote inside strings, escape backslashes, use
73
- for newlines.
101
+ - The ONLY way to reach the operator is the respond tool, as your LAST action.
102
+ Any other text is not read by the operator.
103
+ - If JSON parsing fails, the tool call will NOT run and the user will see raw text. Keep JSON valid: escape every double quote inside strings, escape backslashes, use \n for newlines.
74
104
 
75
105
  ## Rules
76
106
 
@@ -82,7 +112,11 @@ Inside that string:
82
112
  - Never output anything except JSON when you want to call a tool.
83
113
  - Your ENTIRE response must be exactly one JSON object (or array of objects).
84
114
 
85
- REMINDER: respond with exactly one JSON object or array. No text, no markdown, no explanations.
115
+ REMINDER: every intermediate response is exactly one JSON object or array —
116
+ no text, no markdown, no explanations, no progress narration. Keep working
117
+ with tool calls only. Talk to the operator with a single final respond call:
118
+ when the task is done, or when you are blocked and must ask the operator.
119
+ Anything you write outside that one respond is not read by the operator.
86
120
  ## Web access
87
121
 
88
122
  - You have WebSearch and WebFetch. Use them when the answer requires current information (versions, changelogs, recent bugs, docs).
package/src/theme.js CHANGED
@@ -28,6 +28,7 @@ export const theme = {
28
28
  warn: chalk.hex('#c9a86a'), // спокойный охра
29
29
  error: chalk.hex('#c98a80'), // приглушённый терракот
30
30
  success: chalk.hex('#a9c08c'), // мягкий шалфейный
31
+ brown: chalk.hex('#a1723f'), // коричневый (статус/спиннер)
31
32
  bold: chalk.bold,
32
33
  }
33
34