zames_pro 2.5.1 → 2.7.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.
@@ -0,0 +1,231 @@
1
+ // Interactive settings menu for the /config command.
2
+ //
3
+ // The menu renders a list of parameters grouped by sections and lets you
4
+ // change them with the arrow keys: boolean/enum toggle in place, number/string
5
+ // prompt for input. All strings come from i18n (cfg.*), so the menu is
6
+ // fully localized.
7
+ //
8
+ // The module doesn't know about config.ts directly (except for the schema
9
+ // types): the caller reads and writes values through callbacks. This makes the
10
+ // menu easy to test and reuse.
11
+ import { theme } from './theme.js';
12
+ const ESC = String.fromCharCode(27);
13
+ // Strip ANSI sequences to compute the visible length of a line.
14
+ function stripAnsi(s) {
15
+ return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
16
+ }
17
+ function displayValue(field, value, t) {
18
+ if (value === undefined)
19
+ return t('cfg.menu.default');
20
+ if (typeof value === 'boolean')
21
+ return value ? t('common.on') : t('common.off');
22
+ return String(value);
23
+ }
24
+ function groupLabel(field, t) {
25
+ return t(field.groupKey);
26
+ }
27
+ /**
28
+ * Show the interactive menu. Returns a promise that resolves when the
29
+ * user exits (q/Esc/Ctrl+C). In a non-TTY it throws — the caller
30
+ * must show a text list.
31
+ */
32
+ export function runConfigMenu(opts) {
33
+ const input = opts.input || process.stdin;
34
+ const output = opts.output || process.stdout;
35
+ const { fields, t, get, set, reset } = opts;
36
+ if (!input.isTTY || !input.setRawMode) {
37
+ return Promise.reject(new Error(t('cfg.menu.notty')));
38
+ }
39
+ let cursor = 0;
40
+ let editing = false;
41
+ let editBuf = '';
42
+ let message = null;
43
+ let exited = false;
44
+ let resolveDone = null;
45
+ const wasRaw = input.isRaw;
46
+ input.setRawMode(true);
47
+ input.resume();
48
+ output.write(ESC + '[?25l'); // hide the cursor while rendering
49
+ // Menu rendering: lines go top to bottom, and we return the cursor to the
50
+ // start of the block via relative moves so we don't spawn blank lines.
51
+ let lastRows = 0;
52
+ const render = () => {
53
+ const lines = [];
54
+ lines.push(theme.bold(t('cfg.menu.title')));
55
+ let lastGroup = '';
56
+ for (let i = 0; i < fields.length; i++) {
57
+ const f = fields[i];
58
+ const g = groupLabel(f, t);
59
+ if (g !== lastGroup) {
60
+ lines.push(theme.system(' ' + g));
61
+ lastGroup = g;
62
+ }
63
+ const selected = i === cursor;
64
+ const marker = selected ? theme.prompt('\u276f ') : ' ';
65
+ const label = selected ? theme.prompt(f.labelKey ? t(f.labelKey) : f.path) : t(f.labelKey || f.path);
66
+ const val = displayValue(f, get(f.path), t);
67
+ const valText = selected ? theme.assistant(val) : theme.dim(val);
68
+ const pathText = selected ? theme.dim(' ' + f.path) : '';
69
+ lines.push(marker + label + theme.dim(' = ') + valText + pathText);
70
+ }
71
+ if (editing) {
72
+ lines.push('');
73
+ lines.push(theme.user(t('cfg.menu.edit_hint')));
74
+ lines.push(theme.prompt('\u276f ') + editBuf);
75
+ }
76
+ else if (message) {
77
+ lines.push('');
78
+ lines.push(theme.assistant(message));
79
+ }
80
+ else {
81
+ lines.push('');
82
+ lines.push(theme.dim(t('cfg.menu.hint')));
83
+ }
84
+ // Erase the previous block and print the new one. We count VISUAL lines
85
+ // (accounting for wrapping by terminal width), otherwise on narrow
86
+ // terminals you get "garbage" from unerased tails.
87
+ const cols = output.columns || 80;
88
+ const rowsOf = (s) => {
89
+ const len = stripAnsi(s).length;
90
+ return Math.max(1, Math.ceil(len / Math.max(1, cols)));
91
+ };
92
+ if (lastRows > 0) {
93
+ output.write(ESC + '[' + lastRows + 'A');
94
+ for (let i = 0; i < lastRows; i++) {
95
+ output.write(ESC + '[2K' + ESC + '[1B');
96
+ }
97
+ output.write(ESC + '[' + lastRows + 'A');
98
+ }
99
+ output.write(lines.join('\n') + '\n');
100
+ lastRows = lines.reduce((n, l) => n + rowsOf(l), 0);
101
+ };
102
+ const cleanup = () => {
103
+ if (exited)
104
+ return;
105
+ exited = true;
106
+ input.removeListener('data', onData);
107
+ if (input.setRawMode)
108
+ input.setRawMode(wasRaw || false);
109
+ output.write(ESC + '[?25h'); // restore the cursor
110
+ if (resolveDone)
111
+ resolveDone();
112
+ };
113
+ const commitEdit = () => {
114
+ const f = fields[cursor];
115
+ const raw = editBuf.trim();
116
+ editing = false;
117
+ editBuf = '';
118
+ if (!raw) {
119
+ message = null;
120
+ return;
121
+ }
122
+ try {
123
+ set(f, raw);
124
+ message = t('cfg.menu.saved', {
125
+ v: f.path,
126
+ value: displayValue(f, get(f.path), t),
127
+ });
128
+ }
129
+ catch (e) {
130
+ message = String(e.message || e);
131
+ }
132
+ };
133
+ const toggle = () => {
134
+ const f = fields[cursor];
135
+ if (f.type === 'boolean') {
136
+ const cur = get(f.path);
137
+ set(f, cur === true ? 'false' : 'true');
138
+ message = null;
139
+ return;
140
+ }
141
+ if (f.type === 'enum' && f.values && f.values.length) {
142
+ const cur = String(get(f.path) ?? '');
143
+ const idx = f.values.indexOf(cur);
144
+ const next = f.values[(idx + 1) % f.values.length];
145
+ set(f, next);
146
+ message = null;
147
+ return;
148
+ }
149
+ // number/string — prompt for input.
150
+ editing = true;
151
+ editBuf = get(f.path) === undefined ? '' : String(get(f.path));
152
+ };
153
+ const onData = (buf) => {
154
+ const s = buf.toString('utf-8');
155
+ if (editing) {
156
+ // Process input character by character: the terminal may send several
157
+ // bytes at once (paste, auto-repeat), Enter is 0x0d/0x0a, backspace is 0x7f.
158
+ for (const ch of s) {
159
+ if (ch === '\r' || ch === '\n') {
160
+ commitEdit();
161
+ break;
162
+ }
163
+ if (ch === ESC) {
164
+ editing = false;
165
+ editBuf = '';
166
+ break;
167
+ }
168
+ if (ch === '\x03') {
169
+ cleanup();
170
+ output.write('\n');
171
+ return;
172
+ }
173
+ if (ch === '\x7f' || ch === '\b') {
174
+ editBuf = editBuf.slice(0, -1);
175
+ }
176
+ else if (ch >= ' ') {
177
+ editBuf += ch;
178
+ }
179
+ }
180
+ render();
181
+ return;
182
+ }
183
+ // The terminal may send several keys in one packet (fast typing,
184
+ // auto-repeat, automation). We parse the buffer into tokens: first
185
+ // escape sequences (arrows), then single characters.
186
+ let i = 0;
187
+ while (i < s.length) {
188
+ if (s.startsWith('\x1b[A', i)) {
189
+ cursor = (cursor - 1 + fields.length) % fields.length;
190
+ message = null;
191
+ i += 3;
192
+ continue;
193
+ }
194
+ if (s.startsWith('\x1b[B', i)) {
195
+ cursor = (cursor + 1) % fields.length;
196
+ message = null;
197
+ i += 3;
198
+ continue;
199
+ }
200
+ const ch = s[i];
201
+ if (ch === 'q' || ch === 'Q' || ch === '\x03' || ch === ESC) {
202
+ cleanup();
203
+ output.write('\n');
204
+ return;
205
+ }
206
+ if (ch === 'k') {
207
+ cursor = (cursor - 1 + fields.length) % fields.length;
208
+ message = null;
209
+ }
210
+ else if (ch === 'j') {
211
+ cursor = (cursor + 1) % fields.length;
212
+ message = null;
213
+ }
214
+ else if (ch === '\r' || ch === '\n' || ch === ' ') {
215
+ toggle();
216
+ }
217
+ else if (ch === 'd' || ch === 'D') {
218
+ const f = fields[cursor];
219
+ reset(f);
220
+ message = t('cfg.reset', { v: f.path });
221
+ }
222
+ i++;
223
+ }
224
+ render();
225
+ };
226
+ return new Promise((resolve) => {
227
+ resolveDone = resolve;
228
+ input.on('data', onData);
229
+ render();
230
+ });
231
+ }
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { DEFAULT_LOCALE } from './i18n.js';
4
5
  const ZAMES_HOME = path.join(os.homedir(), '.zames');
5
6
  const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
6
7
  const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json');
@@ -41,6 +42,9 @@ export const DEFAULTS = {
41
42
  rateLimitWaitMs: 300000,
42
43
  maxRateLimitRetries: 6,
43
44
  },
45
+ ui: {
46
+ locale: DEFAULT_LOCALE,
47
+ },
44
48
  };
45
49
  export function loadConfig() {
46
50
  const merged = JSON.parse(JSON.stringify(DEFAULTS));
@@ -71,5 +75,144 @@ function deepMerge(target, source) {
71
75
  }
72
76
  }
73
77
  }
78
+ export const CONFIG_SCHEMA = [
79
+ { path: 'ui.locale', type: 'enum', values: ['ru', 'en'], labelKey: 'cfg.f.ui_locale', groupKey: 'cfg.group.ui' },
80
+ { path: 'maxIterations', type: 'number', min: 1, max: 1000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
81
+ { path: 'headless', type: 'boolean', labelKey: 'cfg.f.headless', groupKey: 'cfg.group.agent' },
82
+ { path: 'debug', type: 'boolean', labelKey: 'cfg.f.debug', groupKey: 'cfg.group.agent' },
83
+ { path: 'hotReload', type: 'boolean', labelKey: 'cfg.f.hotReload', groupKey: 'cfg.group.agent' },
84
+ { path: 'confirmation.write', type: 'boolean', labelKey: 'cfg.f.confirmation_write', groupKey: 'cfg.group.confirmation' },
85
+ { path: 'confirmation.edit', type: 'boolean', labelKey: 'cfg.f.confirmation_edit', groupKey: 'cfg.group.confirmation' },
86
+ { path: 'confirmation.bash', type: 'boolean', labelKey: 'cfg.f.confirmation_bash', groupKey: 'cfg.group.confirmation' },
87
+ { path: 'undo.enabled', type: 'boolean', labelKey: 'cfg.f.undo_enabled', groupKey: 'cfg.group.undo' },
88
+ { path: 'undo.maxBackups', type: 'number', min: 1, max: 100000, labelKey: 'cfg.f.undo_maxBackups', groupKey: 'cfg.group.undo' },
89
+ { path: 'transcript.enabled', type: 'boolean', labelKey: 'cfg.f.transcript_enabled', groupKey: 'cfg.group.transcript' },
90
+ { path: 'browser.answerTimeoutMs', type: 'number', min: 1000, max: 3600000, labelKey: 'cfg.f.browser_answerTimeoutMs', groupKey: 'cfg.group.browser' },
91
+ { path: 'browser.askRetries', type: 'number', min: 1, max: 100, labelKey: 'cfg.f.browser_askRetries', groupKey: 'cfg.group.browser' },
92
+ { path: 'browser.stabilityChecks', type: 'number', min: 1, max: 100, labelKey: 'cfg.f.browser_stabilityChecks', groupKey: 'cfg.group.browser' },
93
+ { path: 'browser.stabilityDelayMs', type: 'number', min: 0, max: 60000, labelKey: 'cfg.f.browser_stabilityDelayMs', groupKey: 'cfg.group.browser' },
94
+ { path: 'browser.minSendIntervalMs', type: 'number', min: 0, max: 600000, labelKey: 'cfg.f.browser_minSendIntervalMs', groupKey: 'cfg.group.browser' },
95
+ { path: 'browser.rateLimitWaitMs', type: 'number', min: 0, max: 3600000, labelKey: 'cfg.f.browser_rateLimitWaitMs', groupKey: 'cfg.group.browser' },
96
+ { path: 'browser.maxRateLimitRetries', type: 'number', min: 0, max: 100, labelKey: 'cfg.f.browser_maxRateLimitRetries', groupKey: 'cfg.group.browser' },
97
+ ];
98
+ export function getConfigField(path) {
99
+ return CONFIG_SCHEMA.find((f) => f.path === path);
100
+ }
101
+ export function getByPath(obj, path) {
102
+ const parts = path.split('.');
103
+ let cur = obj;
104
+ for (const p of parts) {
105
+ if (cur === null || cur === undefined || typeof cur !== 'object') {
106
+ return undefined;
107
+ }
108
+ cur = cur[p];
109
+ }
110
+ return cur;
111
+ }
112
+ export function setByPath(obj, path, value) {
113
+ const parts = path.split('.');
114
+ let cur = obj;
115
+ for (let i = 0; i < parts.length - 1; i++) {
116
+ const p = parts[i];
117
+ if (typeof cur[p] !== 'object' || cur[p] === null)
118
+ cur[p] = {};
119
+ cur = cur[p];
120
+ }
121
+ cur[parts[parts.length - 1]] = value;
122
+ }
123
+ function parseConfigValue(field, raw) {
124
+ switch (field.type) {
125
+ case 'boolean': {
126
+ const s = raw.trim().toLowerCase();
127
+ if (['true', '1', 'yes', 'y', 'on', 'да', 'вкл'].includes(s))
128
+ return true;
129
+ if (['false', '0', 'no', 'n', 'off', 'нет', 'выкл'].includes(s))
130
+ return false;
131
+ throw new Error('boolean');
132
+ }
133
+ case 'number': {
134
+ const n = Number(raw);
135
+ if (!Number.isFinite(n))
136
+ throw new Error('number');
137
+ if (field.min !== undefined && n < field.min)
138
+ throw new Error('number');
139
+ if (field.max !== undefined && n > field.max)
140
+ throw new Error('number');
141
+ return n;
142
+ }
143
+ case 'enum': {
144
+ const s = raw.trim().toLowerCase();
145
+ if (field.values && !field.values.includes(s))
146
+ throw new Error('enum');
147
+ return s;
148
+ }
149
+ default:
150
+ return raw;
151
+ }
152
+ }
153
+ export function validateConfigValue(field, raw) {
154
+ return parseConfigValue(field, raw);
155
+ }
156
+ export function configPathFor(scope) {
157
+ return scope === 'project' ? PROJECT_CONFIG : HOME_CONFIG;
158
+ }
159
+ /**
160
+ * Read only the specified config file (without merging with defaults), so that
161
+ * writing does not "freeze" all defaults into the user's file.
162
+ */
163
+ export function readConfigFile(file) {
164
+ try {
165
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
166
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
167
+ return data;
168
+ }
169
+ }
170
+ catch (e) {
171
+ const err = e;
172
+ if (err.code !== 'ENOENT') {
173
+ throw new Error(`не удалось прочитать ${file}: ${err.message}`);
174
+ }
175
+ }
176
+ return {};
177
+ }
178
+ /**
179
+ * Write a setting value to the file. Returns the final object that was
180
+ * written. The value is first validated against the schema.
181
+ */
182
+ export function writeConfigValue(scope, key, raw) {
183
+ const field = getConfigField(key);
184
+ if (!field)
185
+ throw new Error(`unknown setting: ${key}`);
186
+ const value = parseConfigValue(field, raw);
187
+ const file = configPathFor(scope);
188
+ const data = readConfigFile(file);
189
+ setByPath(data, key, value);
190
+ fs.mkdirSync(path.dirname(file), { recursive: true });
191
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf-8');
192
+ return { value, file };
193
+ }
194
+ /** Reset a setting to the default (remove the key from the file). */
195
+ export function resetConfigValue(scope, key) {
196
+ const field = getConfigField(key);
197
+ if (!field)
198
+ throw new Error(`unknown setting: ${key}`);
199
+ const file = configPathFor(scope);
200
+ const data = readConfigFile(file);
201
+ const parts = key.split('.');
202
+ let cur = data;
203
+ for (let i = 0; i < parts.length - 1; i++) {
204
+ const p = parts[i];
205
+ if (!cur || typeof cur[p] !== 'object' || cur[p] === null) {
206
+ cur = undefined;
207
+ break;
208
+ }
209
+ cur = cur[p];
210
+ }
211
+ if (cur)
212
+ delete cur[parts[parts.length - 1]];
213
+ fs.mkdirSync(path.dirname(file), { recursive: true });
214
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf-8');
215
+ return file;
216
+ }
74
217
  export const CONFIG_PATHS = { HOME_CONFIG, PROJECT_CONFIG };
75
218
  export { ZAMES_HOME };
package/dist/gitTools.js CHANGED
@@ -36,8 +36,8 @@ export function runGit(cmd, cwd, timeout = 15_000) {
36
36
  });
37
37
  });
38
38
  }
39
- // Безопасно квотит пути/аргументы для передачи в shell.
40
- // Строка или массив -> строка с двойными кавычками и экранированием.
39
+ // Safely quotes paths/arguments for passing to the shell.
40
+ // String or array -> a string with double quotes and escaping.
41
41
  function quoteArgs(input) {
42
42
  const list = Array.isArray(input) ? input : [input];
43
43
  return list
@@ -46,7 +46,7 @@ function quoteArgs(input) {
46
46
  .map((v) => JSON.stringify(v))
47
47
  .join(' ');
48
48
  }
49
- // Проверяет, является ли директория git-репозиторием, и собирает контекст.
49
+ // Checks whether the directory is a git repository and gathers the context.
50
50
  export async function getGitContext(workdir) {
51
51
  const probe = await runGit('git rev-parse --is-inside-work-tree', workdir, 5000);
52
52
  if (probe.trim() !== 'true')
@@ -175,7 +175,7 @@ export function createGitTools(workdir) {
175
175
  if (!msg.trim()) {
176
176
  return 'Ошибка: message пустой.';
177
177
  }
178
- // Индексируем всё только если в индексе пусто (как обещает описание).
178
+ // We stage everything only if the index is empty (as the description promises).
179
179
  const staged = await runGit('git diff --cached --name-only', workdir, 10_000);
180
180
  const hasStaged = !/^\s*/.test(staged) &&
181
181
  staged.trim() !== '(команда выполнена, вывода нет)';