cli-surf 0.11.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/cliUi.js ADDED
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Терминальный UI для Surf CLI — в духе Claude Code / Codex, в цветах мессенджера.
3
+ * Без внешних зависимостей: только ANSI + stdout. Цвета гаснут при NO_COLOR,
4
+ * pipe или не-TTY, чтобы вывод оставался пригодным для скриптов.
5
+ */
6
+ import { emitKeypressEvents } from 'node:readline';
7
+ let cliLanguage = 'ru';
8
+ export function setCliLanguage(value) {
9
+ cliLanguage = value === 'en' || value === 'English' ? 'en' : 'ru';
10
+ }
11
+ export function isRussian() {
12
+ return cliLanguage === 'ru';
13
+ }
14
+ /** Short UI string with the account language selected in the messenger. */
15
+ export function tr(ru, en) {
16
+ return isRussian() ? ru : en;
17
+ }
18
+ export const OPUS_MODEL_IDS = [
19
+ 'auto', 'fast', 'advanced', 'sol', 'terra', 'luna',
20
+ 'gpt54mini', 'gpt54nano', 'gpt5mini', 'gpt5nano',
21
+ 'o3', 'o3mini', 'o4mini',
22
+ 'gemini37flash', 'gemini35flashlite', 'gemini31pro', 'gemini25pro',
23
+ 'musespark13', 'musespark12',
24
+ 'grok46',
25
+ ];
26
+ /** Человеческие имена — один в один как в мессенджере (OPUS_MODELS). */
27
+ const MODEL_DISPLAY = {
28
+ auto: 'Auto',
29
+ fast: 'GPT-4o mini',
30
+ advanced: 'GPT-4o',
31
+ sol: 'GPT-5.6 Sol',
32
+ terra: 'GPT-5.6 Terra',
33
+ luna: 'GPT-5.6 Luna',
34
+ gpt54mini: 'GPT-5.4 Mini',
35
+ gpt54nano: 'GPT-5.4 Nano',
36
+ gpt5mini: 'GPT-5 Mini',
37
+ gpt5nano: 'GPT-5 Nano',
38
+ o3: 'o3',
39
+ o3mini: 'o3-mini',
40
+ o4mini: 'o4-mini',
41
+ gemini37flash: 'Gemini 3.7 Flash',
42
+ gemini35flashlite: 'Gemini 3.5 Flash-Lite',
43
+ gemini31pro: 'Gemini 3.1 Pro',
44
+ gemini25pro: 'Gemini 2.5 Pro',
45
+ musespark13: 'Muse Spark 1.3',
46
+ musespark12: 'Muse Spark 1.2',
47
+ grok46: 'Grok 4.6',
48
+ };
49
+ export function modelDisplayName(id) {
50
+ return MODEL_DISPLAY[id] ?? id;
51
+ }
52
+ /** Вручную выбираются настоящие модели. Скрыты только режимы:
53
+ * auto (дефолт), fast (дешёвая для free), advanced (legacy GPT-4o).
54
+ * Скрытые работают, если задать явно: /model fast, --model auto. */
55
+ const HIDDEN_ALIASES = new Set(['auto', 'fast', 'advanced']);
56
+ export const PICKABLE_MODEL_IDS = OPUS_MODEL_IDS.filter(id => !HIDDEN_ALIASES.has(id));
57
+ /** «GPT-5 Mini» → «gpt5mini»: регистр, пробелы, точки и дефисы не важны. */
58
+ export function normModelKey(raw) {
59
+ return raw.toLowerCase().replace(/[^a-z0-9]/g, '');
60
+ }
61
+ const ANSI = {
62
+ reset: '\x1b[0m',
63
+ bold: '\x1b[1m',
64
+ dim: '\x1b[2m',
65
+ italic: '\x1b[3m',
66
+ white: '\x1b[37m',
67
+ gray: '\x1b[90m',
68
+ peach: '\x1b[38;5;216m',
69
+ blue: '\x1b[38;5;75m',
70
+ green: '\x1b[38;5;42m',
71
+ red: '\x1b[38;5;203m',
72
+ yellow: '\x1b[38;5;221m',
73
+ codeBg: '\x1b[48;5;238m',
74
+ };
75
+ /** Персональный акцент как в мессенджере (settings.accentColor).
76
+ * Дефолт мессенджера — синий #3287FE; персиковый — только до входа. */
77
+ const ACCENT_FALLBACK = ANSI.peach;
78
+ const MESSENGER_DEFAULT_ACCENT = '#3287FE';
79
+ let accentRgb = null;
80
+ export function defaultAccentHex() {
81
+ return MESSENGER_DEFAULT_ACCENT;
82
+ }
83
+ /** Задать акцент (#RGB или #RRGGBB). null/мусор — сброс к фолбэку. */
84
+ export function setAccentColor(hex) {
85
+ const m = typeof hex === 'string' ? hex.trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i) : null;
86
+ if (!m) {
87
+ accentRgb = null;
88
+ return;
89
+ }
90
+ let h = m[1];
91
+ if (h.length === 3)
92
+ h = h.split('').map(c => c + c).join('');
93
+ accentRgb = [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
94
+ }
95
+ function accentEscape() {
96
+ if (!accentRgb)
97
+ return ACCENT_FALLBACK;
98
+ return `\x1b[38;2;${accentRgb[0]};${accentRgb[1]};${accentRgb[2]}m`;
99
+ }
100
+ export function uiEnabled(stream = process.stdout) {
101
+ if (process.env.NO_COLOR !== undefined)
102
+ return false;
103
+ if (process.env.FORCE_COLOR)
104
+ return true;
105
+ return !!stream.isTTY;
106
+ }
107
+ let colorsOn = uiEnabled();
108
+ /** Форсированно выключить цвета (например, для --json). */
109
+ export function setUiColors(on) {
110
+ colorsOn = on && uiEnabled();
111
+ }
112
+ export function paint(text, ...styles) {
113
+ if (!colorsOn || styles.length === 0)
114
+ return text;
115
+ const seq = styles.map(s => (s === 'accent' ? accentEscape() : ANSI[s])).join('');
116
+ return `${seq}${text}${ANSI.reset}`;
117
+ }
118
+ export function stripLine(text) {
119
+ const ESC = String.fromCharCode(0x1b);
120
+ let out = '';
121
+ let i = 0;
122
+ for (;;) {
123
+ const j = text.indexOf(ESC, i);
124
+ if (j < 0) {
125
+ out += text.slice(i);
126
+ break;
127
+ }
128
+ out += text.slice(i, j);
129
+ const m = /^\[[0-9;]*m/.exec(text.slice(j + 1));
130
+ i = m ? j + 1 + m[0].length : j + 1;
131
+ }
132
+ return out;
133
+ }
134
+ /** Подпись статуса стрима — те же смыслы, что в мессенджере. */
135
+ export function statusLabel(status) {
136
+ switch (status) {
137
+ case 'search': return tr('Ищу в вебе…', 'Searching the web…');
138
+ case 'chats': return tr('Читаю переписки…', 'Reading chats…');
139
+ case 'images': return tr('Подбираю фото…', 'Finding images…');
140
+ case 'map': return tr('Строю карту…', 'Building a map…');
141
+ case 'file': return tr('Собираю файл…', 'Creating a file…');
142
+ case 'github': return tr('Работаю с GitHub…', 'Working with GitHub…');
143
+ case 'gitlab': return tr('Работаю с GitLab…', 'Working with GitLab…');
144
+ case 'notion': return tr('Работаю с Notion…', 'Working with Notion…');
145
+ case 'google-calendar': return tr('Работаю с Google Calendar…', 'Working with Google Calendar…');
146
+ case '101': return tr('Работаю с 101…', 'Working with 101…');
147
+ case 'calling': return tr('Звоню…', 'Calling…');
148
+ default: return tr('Думаю…', 'Thinking…');
149
+ }
150
+ }
151
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
152
+ /** Спиннер в stderr — stdout остаётся чистым для pipe. */
153
+ export class Spinner {
154
+ timer = null;
155
+ frame = 0;
156
+ label = '';
157
+ active = false;
158
+ start(label) {
159
+ this.label = label;
160
+ if (!colorsOn || !process.stderr.isTTY)
161
+ return;
162
+ if (this.active)
163
+ return;
164
+ this.active = true;
165
+ this.timer = setInterval(() => {
166
+ const glyph = paint(SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length], 'accent');
167
+ process.stderr.write(`\r\x1b[K${glyph} ${paint(this.label, 'gray')}`);
168
+ this.frame += 1;
169
+ }, 80);
170
+ }
171
+ update(label) {
172
+ this.label = label;
173
+ }
174
+ stop() {
175
+ if (this.timer)
176
+ clearInterval(this.timer);
177
+ this.timer = null;
178
+ if (this.active)
179
+ process.stderr.write('\r\x1b[K');
180
+ this.active = false;
181
+ }
182
+ }
183
+ function renderInline(text) {
184
+ if (!colorsOn)
185
+ return text;
186
+ // Сначала прячем код, чтобы ** внутри кода не красились.
187
+ // Маркер из Private Use Area — не встречается в обычных ответах.
188
+ const MARK = '';
189
+ const codeSpans = [];
190
+ const hidden = text.replace(/`([^`\n]+)`/g, (_, code) => {
191
+ codeSpans.push(code);
192
+ return `${MARK}${codeSpans.length - 1}${MARK}`;
193
+ });
194
+ const styled = hidden
195
+ .replace(/\*\*([^*]+)\*\*/g, (_, b) => `${ANSI.bold}${b}${ANSI.reset}`)
196
+ .replace(/(^|[\s(])\*([^*\n]+)\*/g, (_, pre, it) => `${pre}${ANSI.italic}${it}${ANSI.reset}`);
197
+ const markRe = new RegExp(`${MARK}(\\d+)${MARK}`, 'g');
198
+ return styled.replace(markRe, (_, i) => {
199
+ const code = codeSpans[Number(i)] ?? '';
200
+ return `${ANSI.codeBg}${ANSI.white} ${code} ${ANSI.reset}`;
201
+ });
202
+ }
203
+ function renderLine(line, inFence) {
204
+ if (/^\s*```/.test(line)) {
205
+ return paint(line.trim() ? '```' + line.trim().slice(3) : '```', 'gray');
206
+ }
207
+ if (inFence)
208
+ return paint(line || ' ', 'gray');
209
+ const header = line.match(/^(#{1,4})\s+(.*)$/);
210
+ if (header)
211
+ return `${paint(header[2], 'bold', 'white')}`;
212
+ const quote = line.match(/^>\s?(.*)$/);
213
+ if (quote)
214
+ return `${paint('▏', 'accent')} ${paint(quote[1], 'gray')}`;
215
+ const list = line.match(/^(\s*)([-*]|\d+[.)])\s+(.*)$/);
216
+ if (list)
217
+ return `${list[1]}${paint('•', 'accent')} ${renderInline(list[3])}`;
218
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line))
219
+ return paint('─'.repeat(24), 'gray');
220
+ if (/^\s*\|.*\|\s*$/.test(line))
221
+ return paint(line, 'gray');
222
+ return renderInline(line);
223
+ }
224
+ /**
225
+ * Построчный markdown-рендер для стрима: копим неполную строку,
226
+ * готовые строки отдаём уже раскрашенными. Состояние ``` тянется между строк.
227
+ */
228
+ export class MdStream {
229
+ buf = '';
230
+ inFence = false;
231
+ push(chunk) {
232
+ this.buf += chunk;
233
+ const parts = this.buf.split('\n');
234
+ this.buf = parts.pop() ?? '';
235
+ let out = '';
236
+ for (const line of parts) {
237
+ out += this.render(line) + '\n';
238
+ }
239
+ return out;
240
+ }
241
+ flush() {
242
+ if (!this.buf)
243
+ return '';
244
+ const out = this.render(this.buf);
245
+ this.buf = '';
246
+ return out;
247
+ }
248
+ render(line) {
249
+ const rendered = renderLine(line, this.inFence);
250
+ if (/^\s*```/.test(line))
251
+ this.inFence = !this.inFence;
252
+ return rendered;
253
+ }
254
+ }
255
+ /** Полоса лимита как в OpusUsageCard: синяя used-часть, dim-остаток. */
256
+ export function progressBar(percentUsed, width = 24) {
257
+ const clamped = Math.max(0, Math.min(100, percentUsed));
258
+ const filled = Math.round((clamped / 100) * width);
259
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
260
+ if (!colorsOn)
261
+ return `[${bar}]`;
262
+ return `[${paint('█'.repeat(filled), 'blue')}${paint('░'.repeat(width - filled), 'gray')}]`;
263
+ }
264
+ function formatReset(resetsAt) {
265
+ const d = new Date(resetsAt);
266
+ if (Number.isNaN(d.getTime()))
267
+ return resetsAt;
268
+ return d.toLocaleDateString(isRussian() ? 'ru-RU' : 'en-US', { day: 'numeric', month: 'long' });
269
+ }
270
+ /** Карточка лимита — терминальная версия OpusUsageCard. */
271
+ export function usageCard(u) {
272
+ const head = `${paint(tr('Использовано', 'Used'), 'gray')} ${paint(`${u.usedCredits} / ${u.limitCredits}`, 'bold', 'white')} ${paint(tr('кредитов', 'credits'), 'gray')}`;
273
+ const bar = ` ${progressBar(u.percentUsed)} ${paint(`${u.percentUsed}%`, 'bold', 'white')}`;
274
+ const legend = ` ${paint('●', 'blue')} ${paint(tr(`осталось ${u.remainingCredits}`, `${u.remainingCredits} remaining`), 'white')} ${paint('○', 'gray')} ${paint(tr(`сброс ${formatReset(u.resetsAt)}`, `resets ${formatReset(u.resetsAt)}`), 'gray')}`;
275
+ const lines = ['', head, bar, legend];
276
+ if (u.creditBalance > 0) {
277
+ lines.push(` ${paint(tr('Доп. баланс:', 'Extra balance:'), 'gray')} ${paint(String(u.creditBalance), 'white')}`);
278
+ }
279
+ if (u.cheapModeOnly) {
280
+ lines.push(` ${paint(tr('⚠ cheap-mode: лимит исчерпан, отвечает только дешёвая модель', '⚠ cheap mode: the limit is exhausted; only the economy model is available'), 'yellow')}`);
281
+ }
282
+ return lines.join('\n');
283
+ }
284
+ /** Шапка чата: бренд + модель + остаток. */
285
+ export function chatBanner(opts) {
286
+ const title = `${paint('◆', 'accent')} ${paint('Surf', 'bold', 'white')} ${paint(`v${opts.version}`, 'gray')}`;
287
+ const modelLine = ` ${paint(tr('модель', 'model'), 'gray')} ${paint(opts.model, 'white')}`;
288
+ const info = opts.usage
289
+ ? ` ${paint(tr('лимит', 'limit'), 'gray')} ${paint(`${opts.usage.remainingCredits}/${opts.usage.limitCredits}`, opts.usage.remainingCredits <= 0 ? 'red' : 'white')} ${paint(`· ${opts.usage.plan}`, 'gray')}`
290
+ : '';
291
+ const hint = ` ${paint(tr('/help — команды · Ctrl+C — выход', '/help — commands · Ctrl+C — exit'), 'gray')}`;
292
+ return ['', title, modelLine, info, hint, ''].filter(l => l !== '').join('\n');
293
+ }
294
+ /** Строка после ответа: модель + остаток. */
295
+ export function answerFooter(model, usage) {
296
+ if (!usage)
297
+ return '';
298
+ return paint(tr(`— ${model} · осталось ${usage.remainingCredits}/${usage.limitCredits} · ${usage.plan}`, `— ${model} · ${usage.remainingCredits}/${usage.limitCredits} remaining · ${usage.plan}`), 'gray');
299
+ }
300
+ /** Заголовок ответа ассистента. */
301
+ export function assistantHead(model) {
302
+ return `${paint('◆', 'accent')} ${paint('Opus', 'bold', 'accent')} ${paint(`· ${model}`, 'gray')}`;
303
+ }
304
+ /** Автокомплит для readline: /команды и id моделей после /model. */
305
+ export function completer(line) {
306
+ const commands = ['/usage', '/model', '/permissions', '/clear', '/help', '/skills', '/logout', '/exit', '/quit'];
307
+ if (line.startsWith('/model ')) {
308
+ const frag = line.slice('/model '.length);
309
+ return [PICKABLE_MODEL_IDS.filter(m => m.startsWith(frag)).map(m => `/model ${m}`), line];
310
+ }
311
+ if (line.startsWith('/')) {
312
+ return [commands.filter(c => c.startsWith(line)), line];
313
+ }
314
+ return [[], line];
315
+ }
316
+ /** Пронумерованный список — fallback без TTY. Читает через очередь чата. */
317
+ export async function selectNumbered(title, options, readLine) {
318
+ if (options.length === 0)
319
+ return null;
320
+ console.log(paint(title, 'gray'));
321
+ options.forEach((o, i) => {
322
+ const num = paint(String(i + 1), 'white');
323
+ const name = paint(o.label ?? o.id, 'bold', 'white');
324
+ const hint = o.hint ? paint(` → ${o.hint}`, 'gray') : '';
325
+ const cur = o.current ? paint(tr(' (текущая)', ' (current)'), 'accent') : '';
326
+ console.log(` ${num} ${name}${hint}${cur}`);
327
+ });
328
+ process.stdout.write(paint(tr('Номер (Enter — отмена): ', 'Number (Enter — cancel): '), 'accent'));
329
+ const answer = ((await readLine()) ?? '').trim();
330
+ if (!answer)
331
+ return null;
332
+ const n = Number(answer);
333
+ if (!Number.isInteger(n) || n < 1 || n > options.length)
334
+ return null;
335
+ return n - 1;
336
+ }
337
+ /** Можно ли показать стрелочный пикер (нужен живой TTY). */
338
+ export function canUseArrows() {
339
+ return !!process.stdin.isTTY && !!process.stdout.isTTY && colorsOn;
340
+ }
341
+ /** Подсказка по командам чата. */
342
+ export function chatHelp() {
343
+ const rows = [
344
+ ['/usage', tr('общий лимит с мессенджером', 'shared messenger limit')],
345
+ ['/model [name]', tr('выбор стрелками, запоминается', 'choose with arrows; saved')],
346
+ ['/permissions', tr('режим подтверждений агента', 'agent confirmation mode')],
347
+ ['! text', tr('выполнить как задачу с файлами', 'run as a task with files')],
348
+ ['/clear', tr('очистить историю чата', 'clear chat history')],
349
+ ['/logout', tr('выйти из аккаунта', 'sign out')],
350
+ ['/skills', tr('навыки агента', 'agent skills')],
351
+ ['/[skill-name]', tr('вызвать навык', 'run a skill')],
352
+ ['/exit', tr('выйти', 'exit')],
353
+ ];
354
+ return rows.map(([cmd, desc]) => ` ${paint(cmd, 'accent')} ${paint(desc, 'gray')}`).join('\n');
355
+ }
356
+ let keypressHooked = false;
357
+ /**
358
+ * Удобный выбор из списка как в Claude Code: ↑↓ (или j/k), Enter — выбор,
359
+ * Esc — отмена. Возвращает индекс или null (отмена).
360
+ * Требование: в момент вызова НЕТ активного readline-интерфейса на stdin
361
+ * (чат его закрывает и пересоздаёт после пикера).
362
+ */
363
+ export async function selectArrows(title, options, footer) {
364
+ if (options.length === 0)
365
+ return null;
366
+ const stdin = process.stdin;
367
+ const stdout = process.stdout;
368
+ const emitter = stdin;
369
+ let selected = Math.max(0, options.findIndex(o => o.current));
370
+ const total = 1 + options.length + (footer ? 1 : 0);
371
+ const render = () => {
372
+ const lines = [paint(title, 'gray')];
373
+ options.forEach((o, i) => {
374
+ const active = i === selected;
375
+ const cursor = active ? paint('❯', 'accent') : ' ';
376
+ const label = o.label ?? o.id;
377
+ const name = active ? paint(label, 'bold', 'white') : paint(label, 'gray');
378
+ const hint = o.hint ? paint(` → ${o.hint}`, 'gray') : '';
379
+ const cur = o.current ? paint(' (текущая)', 'accent') : '';
380
+ lines.push(`${cursor} ${name}${hint}${cur}`);
381
+ });
382
+ if (footer)
383
+ lines.push(paint(footer, 'gray'));
384
+ stdout.write(`${lines.map(l => `\x1b[K${l}`).join('\n')}\n`);
385
+ };
386
+ render();
387
+ return new Promise(resolve => {
388
+ const done = (value) => {
389
+ emitter.removeListener('keypress', onKey);
390
+ try {
391
+ stdin.setRawMode(false);
392
+ }
393
+ catch { /* уже не raw */ }
394
+ stdin.pause();
395
+ stdout.write('\x1b[?25h');
396
+ // Стереть меню без разрыва в истории: вверх на высоту блока
397
+ // и очистить всё ниже курсора (блок — последние строки экрана).
398
+ stdout.write(`\x1b[${total}A`);
399
+ stdout.write('\x1b[J');
400
+ resolve(value);
401
+ };
402
+ const redraw = () => {
403
+ stdout.write(`\x1b[${total}A`);
404
+ render();
405
+ };
406
+ const onKey = (_str, key) => {
407
+ const name = key?.name ?? '';
408
+ if (name === 'up' || name === 'k') {
409
+ selected = (selected - 1 + options.length) % options.length;
410
+ redraw();
411
+ return;
412
+ }
413
+ if (name === 'down' || name === 'j') {
414
+ selected = (selected + 1) % options.length;
415
+ redraw();
416
+ return;
417
+ }
418
+ if (name === 'return') {
419
+ done(selected);
420
+ return;
421
+ }
422
+ if (name === 'escape' || name === 'q' || (name === 'c' && key?.ctrl)) {
423
+ done(null);
424
+ }
425
+ };
426
+ if (!keypressHooked) {
427
+ emitKeypressEvents(stdin);
428
+ keypressHooked = true;
429
+ }
430
+ emitter.on('keypress', onKey);
431
+ stdin.setRawMode(true);
432
+ stdin.resume();
433
+ stdout.write('\x1b[?25l');
434
+ });
435
+ }
436
+ //# sourceMappingURL=cliUi.js.map
package/opusModels.js ADDED
@@ -0,0 +1,83 @@
1
+ export function normalizeOpusModelId(value) {
2
+ if (value === 'auto'
3
+ || value === 'sol'
4
+ || value === 'terra'
5
+ || value === 'luna'
6
+ || value === 'gpt54mini'
7
+ || value === 'gpt54nano'
8
+ || value === 'gpt5mini'
9
+ || value === 'gpt5nano'
10
+ || value === 'o3'
11
+ || value === 'o3mini'
12
+ || value === 'o4mini'
13
+ || value === 'gemini37flash'
14
+ || value === 'gemini35flashlite'
15
+ || value === 'gemini31pro'
16
+ || value === 'gemini25pro'
17
+ || value === 'musespark13'
18
+ || value === 'musespark12'
19
+ || value === 'grok46')
20
+ return value;
21
+ if (value === 'fast' || value === 'advanced')
22
+ return value;
23
+ return 'auto';
24
+ }
25
+ export function opusProviderModel(model) {
26
+ if (model === 'auto')
27
+ return 'openai/gpt-5.6-terra';
28
+ if (model === 'sol')
29
+ return 'openai/gpt-5.6-sol';
30
+ if (model === 'terra')
31
+ return 'openai/gpt-5.6-terra';
32
+ if (model === 'luna')
33
+ return 'openai/gpt-5.6-luna';
34
+ if (model === 'gpt54mini')
35
+ return 'openai/gpt-5.4-mini';
36
+ if (model === 'gpt54nano')
37
+ return 'openai/gpt-5.4-nano';
38
+ if (model === 'gpt5mini')
39
+ return 'openai/gpt-5-mini';
40
+ if (model === 'gpt5nano')
41
+ return 'openai/gpt-5-nano';
42
+ if (model === 'o3')
43
+ return 'openai/o3';
44
+ if (model === 'o3mini')
45
+ return 'openai/o3-mini';
46
+ if (model === 'o4mini')
47
+ return 'openai/o4-mini';
48
+ if (model === 'gemini37flash')
49
+ return 'gemini/gemini-3.7-flash';
50
+ if (model === 'gemini35flashlite')
51
+ return 'gemini/gemini-3.5-flash-lite';
52
+ if (model === 'gemini31pro')
53
+ return 'gemini/gemini-3.1-pro-preview';
54
+ if (model === 'gemini25pro')
55
+ return 'gemini/gemini-2.5-pro';
56
+ if (model === 'musespark13')
57
+ return 'meta/muse-spark-1.3-contributor';
58
+ if (model === 'musespark12')
59
+ return 'meta/muse-spark-1.2-contributor';
60
+ if (model === 'grok46')
61
+ return 'x-ai/grok-4.6';
62
+ return model === 'advanced' ? 'openai/gpt-4o' : 'openai/gpt-4o-mini';
63
+ }
64
+ export function isReasoningProviderModel(model) {
65
+ return typeof model === 'string' && model.startsWith('openai/o');
66
+ }
67
+ export function normalizeReasoningEffort(value) {
68
+ if (value === 'low' || value === 'high')
69
+ return value;
70
+ return 'medium';
71
+ }
72
+ export function autoOpusModelForRequest(text, history, attachments) {
73
+ const prompt = typeof text === 'string' ? text.trim() : '';
74
+ const historyLength = Array.isArray(history) ? history.length : 0;
75
+ const attachmentCount = Array.isArray(attachments) ? attachments.length : 0;
76
+ const complexRequest = /(?:анализ|проанализ|исслед|сравни|документ|архитектур|код|программ|план|стратег|математ|доказ|analy[sz]|research|compare|document|architect|\bcode\b|program|strategy|math|prove)/i.test(prompt);
77
+ if (attachmentCount > 0 || prompt.length > 1000 || historyLength >= 8 || complexRequest)
78
+ return 'sol';
79
+ if (prompt.length < 180 && historyLength <= 2)
80
+ return 'luna';
81
+ return 'terra';
82
+ }
83
+ //# sourceMappingURL=opusModels.js.map
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "cli-surf",
3
+ "version": "0.11.0",
4
+ "description": "Surf Opus CLI — terminal client for Surf messenger with shared AI limits",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "surf": "cli.js",
9
+ "opus": "cli.js"
10
+ },
11
+ "files": [
12
+ "cli.js",
13
+ "cliUi.js",
14
+ "cliPrompt.js",
15
+ "cliTools.js",
16
+ "cliSkills.js",
17
+ "opusModels.js",
18
+ "skills/**/*",
19
+ "README.md"
20
+ ],
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "keywords": [
25
+ "surf",
26
+ "opus",
27
+ "cli",
28
+ "ai",
29
+ "chat"
30
+ ]
31
+ }
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: commit
3
+ description: Git-коммит по conventional commits — смотрит diff, пишет сообщение, коммитит. Пуш только если просят.
4
+ version: "1.0"
5
+ ---
6
+
7
+ # Git commit
8
+
9
+ Сделай коммит текущих изменений по conventional commits.
10
+
11
+ ## Как работать
12
+
13
+ 1. `git status` и `git diff` — пойми, что меняется. `git log --oneline -5` — подхвати стиль сообщений репозитория.
14
+ 2. Проверь, что не коммитишь мусор: секреты (.env, ключи), временные файлы, артефакты сборки, свои отладочные скрипты. Такое не добавлять, предупредить пользователя.
15
+ 3. Собери сообщение: `<type>: <кратко>`, типы — feat, fix, chore, refactor, docs, test. Тело — только если изменение неочевидное.
16
+ 4. Закоммить: `git add` только нужные файлы (не `git add -A` вслепую), затем `git commit -m`.
17
+ 5. НЕ пушить без прямой просьбы. НЕ менять конфиг git, НЕ форсить, НЕ amend без просьбы.
18
+
19
+ ## Ответ
20
+
21
+ Одной строкой: что закоммичено + хеш. Если что-то не стал добавлять — скажи что и почему.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: review
3
+ description: Ревью кода — баги, безопасность, производительность. Вызывай после правок или перед коммитом.
4
+ version: "1.0"
5
+ ---
6
+
7
+ # Code review
8
+
9
+ Проведи ревью последних изменений или указанного кода как строгий, но справедливый ревьюер.
10
+
11
+ ## Как работать
12
+
13
+ 1. Определи скоуп: `git status` и `git diff` (только чтение, без коммита). Если не git — спроси, что ревьюить, и прочитай файлы инструментами read/glob.
14
+ 2. Проверяй по чек-листу ниже. Хвалить не нужно, только проблемы и риски.
15
+ 3. Каждое замечание: файл и строка, что не так, почему это проблема, как исправить (конкретно, с кодом).
16
+
17
+ ## Чек-лист
18
+
19
+ - Баги: off-by-one, null/undefined, race conditions, необработанные ошибки, неверные условия.
20
+ - Безопасность: инъекции (SQL, shell, XSS), секреты в коде, path traversal, невалидированный ввод.
21
+ - Производительность: N+1, лишние аллокации в горячих путях, синхронные операции в async-коде.
22
+ - Читаемость: мёртвый код, дубли, магические числа, нейминг.
23
+ - Тесты: есть ли покрытие изменённого кода, граничные случаи.
24
+
25
+ ## Формат ответа
26
+
27
+ - Сначала вердикт одной строкой: OK / есть замечания / блокирующие проблемы.
28
+ - Затем замечания по важности: Blocker, Warning, Nit.
29
+ - В конце: что проверить руками (тесты, сборка, линтер) — и при желании запусти их сам через bash.