@semalt-ai/code 1.4.3 → 1.4.4

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 (2) hide show
  1. package/index.js +245 -110
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -105,53 +105,184 @@ function boxLine(text, width) {
105
105
  return ` ${FG_DARK}${BOX_V}${RST} ${text}${' '.repeat(pad)}${FG_DARK}${BOX_V}${RST}`;
106
106
  }
107
107
 
108
- // ── Permission system ─────────────────────────────────────────────────────────
108
+ function dropLastChar(text) {
109
+ const chars = Array.from(text || '');
110
+ chars.pop();
111
+ return chars.join('');
112
+ }
109
113
 
110
- let AUTO_APPROVE_SHELL = false;
111
- let AUTO_APPROVE_FILE = false;
114
+ function insertCharAt(text, index, value) {
115
+ const chars = Array.from(text || '');
116
+ chars.splice(index, 0, value);
117
+ return chars.join('');
118
+ }
112
119
 
113
- function askPermissionLine(actionType) {
114
- return actionType === 'shell'
115
- ? ' 1. Yes 2. Yes, always for shell 3. No'
116
- : ' 1. Yes 2. Yes, always for files 3. No';
120
+ function removeCharAt(text, index) {
121
+ const chars = Array.from(text || '');
122
+ chars.splice(index, 1);
123
+ return chars.join('');
117
124
  }
118
125
 
119
- function readPermissionChoice() {
126
+ function isPrintableKey(str, key = {}) {
127
+ if (!str || key.ctrl || key.meta) return false;
128
+ if (key.name === 'return' || key.name === 'enter' || key.name === 'tab') return false;
129
+ if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'delete', 'backspace'].includes(key.name)) {
130
+ return false;
131
+ }
132
+ return !/[\x00-\x1f\x7f]/.test(str);
133
+ }
134
+
135
+ function readInteractiveInput(promptText, options = {}) {
136
+ const {
137
+ allowed = null,
138
+ immediate = false,
139
+ trim = false,
140
+ allowCursorNavigation = false,
141
+ } = options;
142
+
120
143
  return new Promise((resolve) => {
121
144
  if (!process.stdin.isTTY) {
122
145
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
123
- rl.question(` ${FG_YELLOW}?${RST} `, (answer) => {
146
+ rl.question(promptText, (answer) => {
124
147
  rl.close();
125
- resolve((answer || '').trim());
148
+ resolve({ type: 'submit', value: trim ? (answer || '').trim() : (answer || '') });
126
149
  });
127
150
  return;
128
151
  }
129
152
 
130
153
  const wasRaw = typeof process.stdin.isRaw === 'boolean' ? process.stdin.isRaw : false;
154
+ let buffer = '';
155
+ let cursor = 0;
156
+ let done = false;
157
+
131
158
  readline.emitKeypressEvents(process.stdin);
132
159
  process.stdin.setRawMode(true);
133
160
  process.stdin.resume();
134
- process.stdout.write(` ${FG_YELLOW}?${RST} `);
161
+
162
+ const render = () => {
163
+ readline.cursorTo(process.stdout, 0);
164
+ readline.clearLine(process.stdout, 0);
165
+ process.stdout.write(`${promptText}${buffer}`);
166
+ const promptWidth = stripAnsi(promptText).length;
167
+ readline.cursorTo(process.stdout, promptWidth + cursor);
168
+ };
169
+
170
+ const finish = (result, addNewline = true) => {
171
+ if (done) return;
172
+ done = true;
173
+ process.stdin.setRawMode(wasRaw);
174
+ process.stdin.removeListener('keypress', onKeypress);
175
+ if (addNewline) process.stdout.write('\n');
176
+ resolve(result);
177
+ };
135
178
 
136
179
  const onKeypress = (str, key = {}) => {
137
180
  if (key.ctrl && key.name === 'c') {
138
- process.stdin.setRawMode(wasRaw);
139
- process.stdin.removeListener('keypress', onKeypress);
140
- process.stdout.write('^C\n');
141
- process.kill(process.pid, 'SIGINT');
181
+ if (buffer) {
182
+ buffer = '';
183
+ cursor = 0;
184
+ render();
185
+ return;
186
+ }
187
+ finish({ type: 'sigint' }, false);
188
+ return;
189
+ }
190
+
191
+ if (key.ctrl && key.name === 'd') {
192
+ if (!buffer) {
193
+ finish({ type: 'eof' }, false);
194
+ }
142
195
  return;
143
196
  }
144
197
 
145
- const value = (str || '').trim();
146
- if (!value) return;
198
+ if (key.name === 'return' || key.name === 'enter') {
199
+ finish({ type: 'submit', value: trim ? buffer.trim() : buffer });
200
+ return;
201
+ }
147
202
 
148
- process.stdin.setRawMode(wasRaw);
149
- process.stdin.removeListener('keypress', onKeypress);
150
- process.stdout.write(`${value}\n`);
151
- resolve(value);
203
+ if (key.name === 'backspace' || key.name === 'delete') {
204
+ if (key.name === 'backspace' && cursor > 0) {
205
+ buffer = removeCharAt(buffer, cursor - 1);
206
+ cursor--;
207
+ render();
208
+ } else if (key.name === 'delete' && cursor < Array.from(buffer).length) {
209
+ buffer = removeCharAt(buffer, cursor);
210
+ render();
211
+ }
212
+ return;
213
+ }
214
+
215
+ if (allowCursorNavigation && key.name === 'left') {
216
+ if (cursor > 0) {
217
+ cursor--;
218
+ render();
219
+ }
220
+ return;
221
+ }
222
+
223
+ if (allowCursorNavigation && key.name === 'right') {
224
+ if (cursor < Array.from(buffer).length) {
225
+ cursor++;
226
+ render();
227
+ }
228
+ return;
229
+ }
230
+
231
+ if (allowCursorNavigation && key.name === 'home') {
232
+ cursor = 0;
233
+ render();
234
+ return;
235
+ }
236
+
237
+ if (allowCursorNavigation && key.name === 'end') {
238
+ cursor = Array.from(buffer).length;
239
+ render();
240
+ return;
241
+ }
242
+
243
+ if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'tab'].includes(key.name)) {
244
+ return;
245
+ }
246
+
247
+ if (!isPrintableKey(str, key)) return;
248
+
249
+ if (allowed && !allowed.includes(str)) return;
250
+
251
+ if (immediate) {
252
+ buffer = str;
253
+ cursor = Array.from(buffer).length;
254
+ render();
255
+ finish({ type: 'submit', value: str });
256
+ return;
257
+ }
258
+
259
+ buffer = insertCharAt(buffer, cursor, str);
260
+ cursor++;
261
+ render();
152
262
  };
153
263
 
154
264
  process.stdin.on('keypress', onKeypress);
265
+ render();
266
+ });
267
+ }
268
+
269
+ // ── Permission system ─────────────────────────────────────────────────────────
270
+
271
+ let AUTO_APPROVE_SHELL = false;
272
+ let AUTO_APPROVE_FILE = false;
273
+
274
+ function askPermissionLine(actionType) {
275
+ return actionType === 'shell'
276
+ ? ' 1. Yes 2. Yes, always for shell 3. No'
277
+ : ' 1. Yes 2. Yes, always for files 3. No';
278
+ }
279
+
280
+ function readPermissionChoice() {
281
+ return readInteractiveInput(` ${FG_YELLOW}?${RST} `, {
282
+ allowed: ['1', '2', '3'],
283
+ immediate: true,
284
+ onEmptyCtrlC: 'signal',
285
+ trim: true,
155
286
  });
156
287
  }
157
288
 
@@ -173,8 +304,14 @@ function askPermission(actionType, description) {
173
304
  console.log(` ${FG_CYAN}${askPermissionLine(actionType)}${RST}`);
174
305
  console.log();
175
306
 
176
- readPermissionChoice().then((answer) => {
177
- const choice = (answer || '').trim().toLowerCase();
307
+ readPermissionChoice().then((result) => {
308
+ if (result.type === 'sigint' || result.type === 'eof') {
309
+ console.log(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
310
+ resolve(false);
311
+ return;
312
+ }
313
+
314
+ const choice = (result.value || '').trim().toLowerCase();
178
315
  if (choice === '1' || choice === 'y' || choice === 'yes') {
179
316
  resolve(true);
180
317
  } else if (choice === '2' || choice === 'a' || choice === 'always') {
@@ -912,38 +1049,35 @@ async function cmdChat(opts) {
912
1049
  let messages = [{ role: 'system', content: getSystemPrompt() }];
913
1050
  const cols = getCols();
914
1051
 
915
- const rl = readline.createInterface({
916
- input: process.stdin,
917
- output: process.stdout,
918
- terminal: true,
919
- });
1052
+ while (true) {
1053
+ const inputResult = await readInteractiveInput(` ${FG_TEAL}${BOLD}>${RST} `, {
1054
+ trim: false,
1055
+ allowCursorNavigation: true,
1056
+ });
920
1057
 
921
- rl.on('close', () => {
922
- console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
923
- process.exit(0);
924
- });
1058
+ if (inputResult.type === 'eof') {
1059
+ console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
1060
+ return;
1061
+ }
925
1062
 
926
- rl.on('SIGINT', () => {
927
- if (isRunningAgent) return;
928
- console.log(`\n ${FG_YELLOW}Use Ctrl+D or type exit to quit.${RST}`);
929
- rl.prompt(true);
930
- });
1063
+ if (inputResult.type === 'sigint') {
1064
+ if (!isRunningAgent) {
1065
+ console.log(`\n ${FG_YELLOW}Use Ctrl+D or type exit to quit.${RST}`);
1066
+ }
1067
+ continue;
1068
+ }
931
1069
 
932
- async function prompt() {
933
- rl.setPrompt(` ${FG_TEAL}${BOLD}>${RST} `);
934
- rl.question(rl.getPrompt(), async (input) => {
935
- const text = (input || '').trim();
1070
+ const text = (inputResult.value || '').trim();
936
1071
 
937
- if (!text) return prompt();
1072
+ if (!text) continue;
938
1073
 
939
- if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
940
- console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
941
- rl.close();
942
- return;
943
- }
1074
+ if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
1075
+ console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
1076
+ return;
1077
+ }
944
1078
 
945
- if (text === '/help') {
946
- console.log(`
1079
+ if (text === '/help') {
1080
+ console.log(`
947
1081
  ${FG_BLUE}${BOLD}Commands:${RST}
948
1082
  ${FG_CYAN}/file <path>${RST} ${FG_GRAY}Load file or dir into context${RST}
949
1083
  ${FG_CYAN}/model${RST} ${FG_GRAY}Choose saved model profile${RST}
@@ -959,82 +1093,83 @@ async function cmdChat(opts) {
959
1093
 
960
1094
  ${FG_DARK}The AI can execute commands — you'll be asked to approve each one.${RST}
961
1095
  `);
962
- return prompt();
963
- }
1096
+ continue;
1097
+ }
964
1098
 
965
- if (text.startsWith('/file ')) {
966
- const fp = text.slice(6).trim();
967
- const ctx = readFileContext([fp]);
968
- if (ctx) messages.push({ role: 'user', content: `Here is the file context:\n${ctx}` });
969
- return prompt();
970
- }
1099
+ if (text.startsWith('/file ')) {
1100
+ const fp = text.slice(6).trim();
1101
+ const ctx = readFileContext([fp]);
1102
+ if (ctx) messages.push({ role: 'user', content: `Here is the file context:\n${ctx}` });
1103
+ continue;
1104
+ }
971
1105
 
972
- if (text === '/model' || text === '/models') {
1106
+ if (text === '/model' || text === '/models') {
1107
+ await new Promise((resolve) => {
1108
+ const rl = readline.createInterface({
1109
+ input: process.stdin,
1110
+ output: process.stdout,
1111
+ terminal: true,
1112
+ });
973
1113
  chooseSavedModelProfile(rl, currentModel, cwd, (nextModel) => {
974
1114
  currentModel = nextModel;
975
- prompt();
1115
+ rl.close();
1116
+ resolve();
976
1117
  });
977
- return;
978
- }
979
-
980
- if (text.startsWith('/model ')) {
981
- currentModel = text.slice(7).trim();
982
- console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model → ${currentModel}${RST}`);
983
- printStatusBar(currentModel, cwd);
984
- return prompt();
985
- }
1118
+ });
1119
+ continue;
1120
+ }
986
1121
 
987
- if (text === '/clear') {
988
- messages = [{ role: 'system', content: getSystemPrompt() }];
989
- AUTO_APPROVE_SHELL = false;
990
- AUTO_APPROVE_FILE = false;
991
- console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Conversation and approvals cleared${RST}\n`);
992
- return prompt();
993
- }
1122
+ if (text.startsWith('/model ')) {
1123
+ currentModel = text.slice(7).trim();
1124
+ console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model → ${currentModel}${RST}`);
1125
+ printStatusBar(currentModel, cwd);
1126
+ continue;
1127
+ }
994
1128
 
995
- if (text === '/compact' || text === '/cost') {
996
- const total = messages.reduce((s, m) => s + estimateTokens(m.content), 0);
997
- console.log(` ${FG_GRAY}${messages.length - 1} messages · ~${total} tokens${RST}\n`);
998
- return prompt();
999
- }
1129
+ if (text === '/clear') {
1130
+ messages = [{ role: 'system', content: getSystemPrompt() }];
1131
+ AUTO_APPROVE_SHELL = false;
1132
+ AUTO_APPROVE_FILE = false;
1133
+ console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Conversation and approvals cleared${RST}\n`);
1134
+ continue;
1135
+ }
1000
1136
 
1001
- if (text === '/config') {
1002
- console.log(` ${FG_GRAY}${JSON.stringify(config, null, 2)}${RST}\n`);
1003
- return prompt();
1004
- }
1137
+ if (text === '/compact' || text === '/cost') {
1138
+ const total = messages.reduce((s, m) => s + estimateTokens(m.content), 0);
1139
+ console.log(` ${FG_GRAY}${messages.length - 1} messages · ~${total} tokens${RST}\n`);
1140
+ continue;
1141
+ }
1005
1142
 
1006
- if (text === '/approve') {
1007
- AUTO_APPROVE_SHELL = !AUTO_APPROVE_SHELL;
1008
- AUTO_APPROVE_FILE = !AUTO_APPROVE_FILE;
1009
- const state = AUTO_APPROVE_SHELL ? 'ON' : 'OFF';
1010
- const color = AUTO_APPROVE_SHELL ? FG_GREEN : FG_RED;
1011
- console.log(` ${color}●${RST} ${FG_GRAY}Auto-approve: ${state}${RST}\n`);
1012
- return prompt();
1013
- }
1143
+ if (text === '/config') {
1144
+ console.log(` ${FG_GRAY}${JSON.stringify(config, null, 2)}${RST}\n`);
1145
+ continue;
1146
+ }
1014
1147
 
1015
- if (text.startsWith('/shell ') || text.startsWith('!')) {
1016
- const cmd = text.startsWith('/shell ') ? text.slice(7).trim() : text.slice(1).trim();
1017
- await agentExecShell(cmd);
1018
- return prompt();
1019
- }
1148
+ if (text === '/approve') {
1149
+ AUTO_APPROVE_SHELL = !AUTO_APPROVE_SHELL;
1150
+ AUTO_APPROVE_FILE = !AUTO_APPROVE_FILE;
1151
+ const state = AUTO_APPROVE_SHELL ? 'ON' : 'OFF';
1152
+ const color = AUTO_APPROVE_SHELL ? FG_GREEN : FG_RED;
1153
+ console.log(` ${color}●${RST} ${FG_GRAY}Auto-approve: ${state}${RST}\n`);
1154
+ continue;
1155
+ }
1020
1156
 
1021
- messages.push({ role: 'user', content: text });
1022
- console.log(` ${FG_DARK}${''.repeat(Math.min(cols, 70) - 4)}${RST}`);
1157
+ if (text.startsWith('/shell ') || text.startsWith('!')) {
1158
+ const cmd = text.startsWith('/shell ') ? text.slice(7).trim() : text.slice(1).trim();
1159
+ await agentExecShell(cmd);
1160
+ continue;
1161
+ }
1023
1162
 
1024
- rl.pause();
1025
- isRunningAgent = true;
1026
- messages = await runAgentLoop(messages, currentModel);
1027
- isRunningAgent = false;
1028
- rl.resume();
1163
+ messages.push({ role: 'user', content: text });
1164
+ console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
1029
1165
 
1030
- console.log(` ${FG_DARK}${'━'.repeat(Math.min(cols, 70) - 4)}${RST}`);
1031
- console.log();
1166
+ isRunningAgent = true;
1167
+ messages = await runAgentLoop(messages, currentModel);
1168
+ isRunningAgent = false;
1032
1169
 
1033
- prompt();
1034
- });
1170
+ console.log(` ${FG_DARK}${'━'.repeat(Math.min(cols, 70) - 4)}${RST}`);
1171
+ console.log();
1035
1172
  }
1036
-
1037
- prompt();
1038
1173
  }
1039
1174
 
1040
1175
  async function cmdCode(opts, promptArgs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semalt-ai/code",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "Self-hosted AI Coding Assistant CLI",
5
5
  "main": "index.js",
6
6
  "bin": {