zames_pro 1.2.1 → 2.1.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/dist/input.js ADDED
@@ -0,0 +1,523 @@
1
+ import { theme } from './theme.js';
2
+ import { renderMarkdown } from './markdown.js';
3
+ import { randomThinkingPhrase, stripEllipsis } from './spinner.js';
4
+ // Постоянная строка ввода внизу терминала + область статуса/вывода над ней.
5
+ //
6
+ // Зачем свой редактор:
7
+ // * строка ввода видна ВСЕГДА и не затирается выводом агента;
8
+ // * спиннер/статус и ответы печатаются ВЫШЕ строки ввода (printAbove);
9
+ // * перерисовка учитывает перенос по ширине терминала, поэтому нет
10
+ // дублирования строк при многострочном вводе;
11
+ // * многострочный ввод: Enter отправляет, Ctrl+J (и Shift+Enter в
12
+ // терминалах с расширенным протоколом) вставляют перевод строки.
13
+ //
14
+ // Все управляющие символы собираются из кодов, чтобы в файле не было
15
+ // «сырых» ESC/CR/LF в строковых литералах (см. AGENTS.md).
16
+ const NL = String.fromCharCode(10);
17
+ const CR = String.fromCharCode(13);
18
+ const ESC = String.fromCharCode(27);
19
+ const PASTE_START = ESC + '[200~';
20
+ const PASTE_END = ESC + '[201~';
21
+ const DOTS = ['.', '..', '...'];
22
+ const HINT = theme.dim(' · Enter — отправить, Ctrl+J — новая строка, Esc — стоп');
23
+ function safeJson(v) {
24
+ try {
25
+ return JSON.stringify(v);
26
+ }
27
+ catch {
28
+ return String(v);
29
+ }
30
+ }
31
+ // Видимая длина строки без ANSI-последовательностей.
32
+ export function visLen(s) {
33
+ s = String(s);
34
+ let n = 0;
35
+ let i = 0;
36
+ while (i < s.length) {
37
+ if (s.charCodeAt(i) === 27) {
38
+ i++;
39
+ if (s[i] === '[') {
40
+ i++;
41
+ while (i < s.length && !/[A-Za-z]/.test(s[i]))
42
+ i++;
43
+ i++;
44
+ }
45
+ continue;
46
+ }
47
+ n++;
48
+ i++;
49
+ }
50
+ return n;
51
+ }
52
+ export function layoutInput(promptStr, buf, cursor, cols) {
53
+ const width = Math.max(20, Number(cols) || 80);
54
+ const promptW = visLen(promptStr);
55
+ const pad = ' '.repeat(promptW);
56
+ const chars = Array.from(String(buf));
57
+ const cur = Math.max(0, Math.min(Number(cursor) || 0, chars.length));
58
+ const rows = [];
59
+ let i = 0;
60
+ while (true) {
61
+ const first = rows.length === 0;
62
+ const prefix = first ? promptStr : pad;
63
+ const avail = Math.max(1, width - promptW);
64
+ let text = '';
65
+ let count = 0;
66
+ const start = i;
67
+ while (i < chars.length && chars[i] !== NL && count < avail) {
68
+ text += chars[i];
69
+ i++;
70
+ count++;
71
+ }
72
+ if (i < chars.length && chars[i] === NL) {
73
+ i++;
74
+ rows.push({ prefix, text, start });
75
+ continue;
76
+ }
77
+ rows.push({ prefix, text, start });
78
+ if (i >= chars.length)
79
+ break;
80
+ }
81
+ let cursorRow = 0;
82
+ let cursorCol = promptW;
83
+ for (let r = 0; r < rows.length; r++) {
84
+ const row = rows[r];
85
+ const len = Array.from(row.text).length;
86
+ const from = row.start;
87
+ const to = row.start + len;
88
+ if (cur >= from && cur <= to) {
89
+ const next = rows[r + 1];
90
+ if (cur === to && next && next.start === to)
91
+ continue;
92
+ cursorRow = r;
93
+ cursorCol = promptW + (cur - from);
94
+ break;
95
+ }
96
+ }
97
+ return { rows, cursorRow, cursorCol };
98
+ }
99
+ export class LineEditor {
100
+ promptStr;
101
+ buf;
102
+ cursor;
103
+ statusText;
104
+ pendingText;
105
+ rendered;
106
+ cursorRowFromTop;
107
+ busy;
108
+ onSubmit;
109
+ onEscape;
110
+ onCtrlC;
111
+ _inPaste;
112
+ _dotTimer;
113
+ _dotPhase;
114
+ _thinkBase;
115
+ _wasRaw;
116
+ _onData;
117
+ constructor({ prompt = '> ' } = {}) {
118
+ this.promptStr = prompt;
119
+ this.buf = '';
120
+ this.cursor = 0;
121
+ this.statusText = '';
122
+ this.pendingText = null;
123
+ this.rendered = false;
124
+ this.cursorRowFromTop = 0;
125
+ this.busy = false;
126
+ this.onSubmit = null;
127
+ this.onEscape = null;
128
+ this.onCtrlC = null;
129
+ this._inPaste = false;
130
+ this._dotTimer = null;
131
+ this._dotPhase = 0;
132
+ this._thinkBase = '';
133
+ this._wasRaw = false;
134
+ this._onData = (b) => this._handle(b);
135
+ }
136
+ start() {
137
+ const stdin = process.stdin;
138
+ if (!stdin.isTTY || !stdin.setRawMode)
139
+ return false;
140
+ this._wasRaw = stdin.isRaw;
141
+ stdin.setRawMode(true);
142
+ stdin.resume();
143
+ process.stdout.write(ESC + '[?2004h');
144
+ stdin.on('data', this._onData);
145
+ this._writeBlock();
146
+ return true;
147
+ }
148
+ dispose() {
149
+ const stdin = process.stdin;
150
+ stdin.removeListener('data', this._onData);
151
+ process.stdout.write(ESC + '[?2004l');
152
+ this._stopDots();
153
+ if (this.rendered)
154
+ this._eraseBlock();
155
+ if (stdin.setRawMode)
156
+ stdin.setRawMode(this._wasRaw || false);
157
+ }
158
+ setPrompt(str) {
159
+ this.promptStr = str;
160
+ this._render();
161
+ }
162
+ clear() {
163
+ this.buf = '';
164
+ this.cursor = 0;
165
+ this._render();
166
+ }
167
+ // ---------- отрисовка ----------
168
+ _eraseBlock() {
169
+ if (!this.rendered)
170
+ return;
171
+ if (this.cursorRowFromTop > 0) {
172
+ process.stdout.write(ESC + '[' + this.cursorRowFromTop + 'A');
173
+ }
174
+ process.stdout.write(CR + ESC + '[J');
175
+ this.rendered = false;
176
+ }
177
+ _writeBlock() {
178
+ const cols = process.stdout.columns || 80;
179
+ let out = '';
180
+ let top = 0;
181
+ if (this.statusText) {
182
+ out += this.statusText + NL;
183
+ top = 1;
184
+ }
185
+ const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols);
186
+ out += lay.rows.map((r) => r.prefix + r.text).join(NL);
187
+ process.stdout.write(out);
188
+ const lastRow = lay.rows.length - 1;
189
+ const up = lastRow - lay.cursorRow;
190
+ if (up > 0)
191
+ process.stdout.write(ESC + '[' + up + 'A');
192
+ process.stdout.write(CR);
193
+ if (lay.cursorCol > 0)
194
+ process.stdout.write(ESC + '[' + lay.cursorCol + 'C');
195
+ this.rendered = true;
196
+ this.cursorRowFromTop = top + lay.cursorRow;
197
+ }
198
+ _render() {
199
+ this._eraseBlock();
200
+ this._writeBlock();
201
+ }
202
+ // Напечатать блок ВЫШЕ строки ввода и вернуть строку ввода на место.
203
+ printAbove(text) {
204
+ this._eraseBlock();
205
+ process.stdout.write(String(text) + NL);
206
+ this._writeBlock();
207
+ }
208
+ // ---------- интерфейс, совместимый со спиннером ----------
209
+ setStatus(text) {
210
+ this.statusText = text || '';
211
+ this._render();
212
+ }
213
+ _stopDots() {
214
+ if (this._dotTimer) {
215
+ clearInterval(this._dotTimer);
216
+ this._dotTimer = null;
217
+ }
218
+ }
219
+ _startThinking() {
220
+ if (this.pendingText) {
221
+ this.setStatus(theme.prompt('✎ ') + this.pendingText + HINT);
222
+ return;
223
+ }
224
+ this._thinkBase = theme.brown(stripEllipsis(randomThinkingPhrase()));
225
+ this._dotPhase = 0;
226
+ this.setStatus(this._thinkBase + DOTS[0] + HINT);
227
+ this._stopDots();
228
+ this._dotTimer = setInterval(() => {
229
+ this._dotPhase = (this._dotPhase + 1) % DOTS.length;
230
+ this.setStatus(this._thinkBase + DOTS[this._dotPhase] + HINT);
231
+ }, 400);
232
+ if (this._dotTimer.unref)
233
+ this._dotTimer.unref();
234
+ }
235
+ thinking() {
236
+ this._startThinking();
237
+ }
238
+ setPending(text) {
239
+ this.pendingText = text && String(text).length ? String(text) : null;
240
+ if (this.pendingText) {
241
+ this._stopDots();
242
+ this.setStatus(theme.prompt('✎ ') + this.pendingText + HINT);
243
+ }
244
+ else {
245
+ this._startThinking();
246
+ }
247
+ }
248
+ stop() {
249
+ this._stopDots();
250
+ this.setStatus('');
251
+ }
252
+ toolCall(name, args) {
253
+ const preview = safeJson(args).slice(0, 120);
254
+ this.printAbove(theme.tool('🔧 ' + name) + ' ' + theme.dim(preview));
255
+ }
256
+ toolResult(result) {
257
+ const text = typeof result === 'string' ? result : safeJson(result);
258
+ const preview = text.slice(0, 200).split(NL).join(' ↵ ');
259
+ this.printAbove(theme.toolResult(' → ' + preview));
260
+ }
261
+ assistant(msg) {
262
+ this.stop();
263
+ const rendered = renderMarkdown(msg);
264
+ this.printAbove(NL + theme.assistant('● Ответ') + NL + rendered + NL + theme.dim('─'.repeat(60)));
265
+ }
266
+ // ---------- ввод ----------
267
+ _submit() {
268
+ const text = this.buf;
269
+ if (!text.trim()) {
270
+ this._render();
271
+ return;
272
+ }
273
+ this.buf = '';
274
+ this.cursor = 0;
275
+ this.pendingText = null;
276
+ this._stopDots();
277
+ this.statusText = '';
278
+ this.printAbove(theme.user('❯ ') + text);
279
+ if (this.onSubmit)
280
+ this.onSubmit(text);
281
+ }
282
+ _insert(text) {
283
+ const chars = Array.from(this.buf);
284
+ const ins = Array.from(String(text));
285
+ const next = chars.slice(0, this.cursor).concat(ins, chars.slice(this.cursor));
286
+ this.buf = next.join('');
287
+ this.cursor += ins.length;
288
+ }
289
+ _backspace() {
290
+ if (this.cursor <= 0)
291
+ return;
292
+ const chars = Array.from(this.buf);
293
+ chars.splice(this.cursor - 1, 1);
294
+ this.buf = chars.join('');
295
+ this.cursor--;
296
+ }
297
+ _delete() {
298
+ const chars = Array.from(this.buf);
299
+ if (this.cursor >= chars.length)
300
+ return;
301
+ chars.splice(this.cursor, 1);
302
+ this.buf = chars.join('');
303
+ }
304
+ _left() {
305
+ if (this.cursor > 0)
306
+ this.cursor--;
307
+ }
308
+ _right() {
309
+ if (this.cursor < Array.from(this.buf).length)
310
+ this.cursor++;
311
+ }
312
+ _home() {
313
+ const chars = Array.from(this.buf);
314
+ let i = this.cursor;
315
+ while (i > 0 && chars[i - 1] !== NL)
316
+ i--;
317
+ this.cursor = i;
318
+ }
319
+ _end() {
320
+ const chars = Array.from(this.buf);
321
+ let i = this.cursor;
322
+ while (i < chars.length && chars[i] !== NL)
323
+ i++;
324
+ this.cursor = i;
325
+ }
326
+ _up() {
327
+ const chars = Array.from(this.buf);
328
+ let start = this.cursor;
329
+ while (start > 0 && chars[start - 1] !== NL)
330
+ start--;
331
+ if (start === 0)
332
+ return;
333
+ const col = this.cursor - start;
334
+ const prevEnd = start - 1;
335
+ let prevStart = prevEnd;
336
+ while (prevStart > 0 && chars[prevStart - 1] !== NL)
337
+ prevStart--;
338
+ this.cursor = prevStart + Math.min(col, prevEnd - prevStart);
339
+ }
340
+ _down() {
341
+ const chars = Array.from(this.buf);
342
+ let end = this.cursor;
343
+ while (end < chars.length && chars[end] !== NL)
344
+ end++;
345
+ if (end >= chars.length)
346
+ return;
347
+ let lineStart = this.cursor;
348
+ while (lineStart > 0 && chars[lineStart - 1] !== NL)
349
+ lineStart--;
350
+ const col = this.cursor - lineStart;
351
+ const nextStart = end + 1;
352
+ let nextEnd = nextStart;
353
+ while (nextEnd < chars.length && chars[nextEnd] !== NL)
354
+ nextEnd++;
355
+ this.cursor = nextStart + Math.min(col, nextEnd - nextStart);
356
+ }
357
+ _handle(data) {
358
+ let s = data.toString('utf-8');
359
+ if (!this._inPaste && s === ESC) {
360
+ if (this.onEscape)
361
+ this.onEscape();
362
+ return;
363
+ }
364
+ while (s.length) {
365
+ if (this._inPaste) {
366
+ const end = s.indexOf(PASTE_END);
367
+ if (end === -1) {
368
+ this._insert(s);
369
+ s = '';
370
+ }
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
+ const pasteAt = s.indexOf(PASTE_START);
380
+ if (pasteAt !== -1) {
381
+ const before = s.slice(0, pasteAt);
382
+ s = s.slice(pasteAt + PASTE_START.length);
383
+ this._inPaste = true;
384
+ if (before) {
385
+ this._insert(before);
386
+ this._render();
387
+ }
388
+ continue;
389
+ }
390
+ // Shift+Enter в терминалах с расширенным протоколом и Alt+Enter.
391
+ if (s.startsWith(ESC + '[13;2u')) {
392
+ this._insert(NL);
393
+ s = s.slice(7);
394
+ this._render();
395
+ continue;
396
+ }
397
+ if (s.startsWith(ESC + '[27;2;13~')) {
398
+ this._insert(NL);
399
+ s = s.slice(10);
400
+ this._render();
401
+ continue;
402
+ }
403
+ if (s.startsWith(ESC + NL)) {
404
+ this._insert(NL);
405
+ s = s.slice(2);
406
+ this._render();
407
+ continue;
408
+ }
409
+ const ch = s[0];
410
+ const code = s.charCodeAt(0);
411
+ s = s.slice(1);
412
+ if (ch === CR) {
413
+ this._submit();
414
+ continue;
415
+ }
416
+ if (code === 10) {
417
+ this._insert(NL);
418
+ this._render();
419
+ continue;
420
+ }
421
+ if (code === 3) {
422
+ if (this.onCtrlC)
423
+ this.onCtrlC();
424
+ continue;
425
+ }
426
+ if (code === 4) {
427
+ if (!this.buf && this.onCtrlC)
428
+ this.onCtrlC();
429
+ continue;
430
+ }
431
+ if (code === 1) {
432
+ this._home();
433
+ this._render();
434
+ continue;
435
+ }
436
+ if (code === 5) {
437
+ this._end();
438
+ this._render();
439
+ continue;
440
+ }
441
+ if (code === 21) {
442
+ this.buf = '';
443
+ this.cursor = 0;
444
+ this._render();
445
+ continue;
446
+ }
447
+ if (code === 127 || code === 8) {
448
+ this._backspace();
449
+ this._render();
450
+ continue;
451
+ }
452
+ if (code === 27) {
453
+ if (s.startsWith('[D')) {
454
+ this._left();
455
+ s = s.slice(2);
456
+ this._render();
457
+ continue;
458
+ }
459
+ if (s.startsWith('[C')) {
460
+ this._right();
461
+ s = s.slice(2);
462
+ this._render();
463
+ continue;
464
+ }
465
+ if (s.startsWith('[A')) {
466
+ this._up();
467
+ s = s.slice(2);
468
+ this._render();
469
+ continue;
470
+ }
471
+ if (s.startsWith('[B')) {
472
+ this._down();
473
+ s = s.slice(2);
474
+ this._render();
475
+ continue;
476
+ }
477
+ if (s.startsWith('[H') || s.startsWith('[1~')) {
478
+ this._home();
479
+ s = s.slice(s.startsWith('[1~') ? 3 : 2);
480
+ this._render();
481
+ continue;
482
+ }
483
+ if (s.startsWith('[F') || s.startsWith('[4~')) {
484
+ this._end();
485
+ s = s.slice(s.startsWith('[4~') ? 3 : 2);
486
+ this._render();
487
+ continue;
488
+ }
489
+ if (s.startsWith('[3~')) {
490
+ this._delete();
491
+ s = s.slice(3);
492
+ this._render();
493
+ continue;
494
+ }
495
+ let j = 0;
496
+ while (j < s.length && !/[A-Za-z~]/.test(s[j]))
497
+ j++;
498
+ s = s.slice(j + 1);
499
+ continue;
500
+ }
501
+ if (code < 32)
502
+ continue;
503
+ this._insert(ch);
504
+ this._render();
505
+ }
506
+ }
507
+ }
508
+ // Проверка раскладки: node src/input.js --selftest
509
+ export function selftest() {
510
+ const cases = [
511
+ { p: '> ', b: 'hello', c: 5, w: 80 },
512
+ { p: '> ', b: 'a' + NL + 'b', c: 3, w: 80 },
513
+ { p: '> ', b: 'x'.repeat(200), c: 200, w: 40 },
514
+ { p: '> ', b: '', c: 0, w: 40 },
515
+ ];
516
+ for (const t of cases) {
517
+ const r = layoutInput(t.p, t.b, t.c, t.w);
518
+ console.log(JSON.stringify({ rows: r.rows.length, row: r.cursorRow, col: r.cursorCol }));
519
+ }
520
+ }
521
+ if (process.argv[1] && process.argv[1].endsWith('input.js') && process.argv.includes('--selftest')) {
522
+ selftest();
523
+ }
@@ -0,0 +1,47 @@
1
+ import { render } from 'markdansi';
2
+ import { highlight } from 'cli-highlight';
3
+ const theme = {
4
+ heading: { color: '#9fb8d8', bold: true },
5
+ strong: { color: '#e6e0cc', bold: true },
6
+ emph: { color: '#cfc9b0', italic: true },
7
+ inlineCode: { color: '#9ecb9e' },
8
+ blockCode: { color: '#c9d1c9' },
9
+ code: { color: '#9ecb9e' },
10
+ link: { color: '#8fb8d8', underline: true },
11
+ quote: { color: '#8a8f9a', italic: true },
12
+ hr: { color: '#5b616e' },
13
+ listMarker: { color: '#8a9bb5' },
14
+ tableHeader: { color: '#9fb8d8', bold: true },
15
+ tableCell: { color: '#d0d0d0' },
16
+ };
17
+ // Подсветка кода через cli-highlight (транзитивно уже есть у нас).
18
+ function highlighter(code, lang) {
19
+ try {
20
+ return highlight(code, {
21
+ language: lang || 'plaintext',
22
+ ignoreIllegals: true,
23
+ });
24
+ }
25
+ catch {
26
+ return code;
27
+ }
28
+ }
29
+ export function renderMarkdown(text) {
30
+ if (!text)
31
+ return '';
32
+ try {
33
+ return render(String(text), {
34
+ width: Math.min(process.stdout.columns || 80, 100),
35
+ theme: theme,
36
+ highlighter,
37
+ codeBox: true,
38
+ tableBorder: 'unicode',
39
+ listIndent: 2,
40
+ hyperlinks: false,
41
+ });
42
+ }
43
+ catch {
44
+ return String(text);
45
+ }
46
+ }
47
+ export default renderMarkdown;
@@ -0,0 +1,100 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ // Извлечение СЫРОГО текста ответа модели из сетевых данных DeepSeek.
5
+ //
6
+ // Зачем: чтение ответа идёт из ОТРЕНДЕРЕННОГО DOM, а рендер DeepSeek
7
+ // искажает ответ — превращает доллар-формулы в LaTeX (символ доллара
8
+ // теряется), нормализует переводы строк, делает автолинки. Из-за этого
9
+ // tool-call с шаблонными строками и экранированными переводами строк в
10
+ // аргументах доходил до инструментов искажённым. Здесь мы достаём исходный
11
+ // текст из тела сетевого ответа.
12
+ export function extractFromSse(body) {
13
+ const chunks = [];
14
+ const lines = body.split(String.fromCharCode(10));
15
+ for (const line of lines) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed.startsWith('data:'))
18
+ continue;
19
+ const payload = trimmed.slice(5).trim();
20
+ if (!payload || payload === '[DONE]')
21
+ continue;
22
+ let obj;
23
+ try {
24
+ obj = JSON.parse(payload);
25
+ }
26
+ catch {
27
+ continue;
28
+ }
29
+ const t = pickAnswerText(obj);
30
+ if (t)
31
+ chunks.push(t);
32
+ }
33
+ return chunks.join('');
34
+ }
35
+ export function extractFromJson(body) {
36
+ let obj;
37
+ try {
38
+ obj = JSON.parse(body);
39
+ }
40
+ catch {
41
+ return '';
42
+ }
43
+ return pickAnswerText(obj);
44
+ }
45
+ // Достаёт финальный текст ответа из узла, НЕ смешивая его с reasoning.
46
+ // Порядок: choices/messages -> delta/message -> content; reasoning_content
47
+ // намеренно не берём — это размышления модели, а не ответ.
48
+ function pickAnswerText(node) {
49
+ if (node == null)
50
+ return '';
51
+ if (typeof node === 'string')
52
+ return node;
53
+ if (Array.isArray(node)) {
54
+ return node.map(pickAnswerText).join('');
55
+ }
56
+ if (typeof node !== 'object')
57
+ return '';
58
+ const obj = node;
59
+ for (const key of ['choices', 'messages']) {
60
+ const v = obj[key];
61
+ if (Array.isArray(v) && v.length) {
62
+ const inner = v.map(pickAnswerText).join('');
63
+ if (inner)
64
+ return inner;
65
+ }
66
+ }
67
+ for (const key of ['content', 'text', 'answer', 'response']) {
68
+ const v = obj[key];
69
+ if (typeof v === 'string' && v)
70
+ return v;
71
+ }
72
+ for (const key of ['delta', 'message', 'data', 'payload', 'v']) {
73
+ const v = obj[key];
74
+ if (v && typeof v === 'object') {
75
+ const inner = pickAnswerText(v);
76
+ if (inner)
77
+ return inner;
78
+ }
79
+ }
80
+ return '';
81
+ }
82
+ // Универсальная попытка: сначала SSE, затем обычный JSON.
83
+ export function extractAnswer(body) {
84
+ const sse = extractFromSse(body);
85
+ if (sse)
86
+ return sse;
87
+ return extractFromJson(body);
88
+ }
89
+ // Сохраняет тело сетевого ответа DeepSeek на диск для разбора постфактум.
90
+ // Файлы лежат в ~/.zames/net-log — по ним видно реальный формат ответа.
91
+ export async function dumpNetBody(url, body) {
92
+ try {
93
+ const dir = path.join(os.homedir(), '.zames', 'net-log');
94
+ await fs.mkdir(dir, { recursive: true });
95
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
96
+ const safe = url.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 80);
97
+ await fs.writeFile(path.join(dir, stamp + '_' + safe + '.txt'), body, 'utf-8');
98
+ }
99
+ catch { }
100
+ }