@semalt-ai/code 1.4.2 → 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 +272 -94
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -105,11 +105,187 @@ 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
+ function dropLastChar(text) {
109
+ const chars = Array.from(text || '');
110
+ chars.pop();
111
+ return chars.join('');
112
+ }
113
+
114
+ function insertCharAt(text, index, value) {
115
+ const chars = Array.from(text || '');
116
+ chars.splice(index, 0, value);
117
+ return chars.join('');
118
+ }
119
+
120
+ function removeCharAt(text, index) {
121
+ const chars = Array.from(text || '');
122
+ chars.splice(index, 1);
123
+ return chars.join('');
124
+ }
125
+
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
+
143
+ return new Promise((resolve) => {
144
+ if (!process.stdin.isTTY) {
145
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
146
+ rl.question(promptText, (answer) => {
147
+ rl.close();
148
+ resolve({ type: 'submit', value: trim ? (answer || '').trim() : (answer || '') });
149
+ });
150
+ return;
151
+ }
152
+
153
+ const wasRaw = typeof process.stdin.isRaw === 'boolean' ? process.stdin.isRaw : false;
154
+ let buffer = '';
155
+ let cursor = 0;
156
+ let done = false;
157
+
158
+ readline.emitKeypressEvents(process.stdin);
159
+ process.stdin.setRawMode(true);
160
+ process.stdin.resume();
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
+ };
178
+
179
+ const onKeypress = (str, key = {}) => {
180
+ if (key.ctrl && key.name === 'c') {
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
+ }
195
+ return;
196
+ }
197
+
198
+ if (key.name === 'return' || key.name === 'enter') {
199
+ finish({ type: 'submit', value: trim ? buffer.trim() : buffer });
200
+ return;
201
+ }
202
+
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();
262
+ };
263
+
264
+ process.stdin.on('keypress', onKeypress);
265
+ render();
266
+ });
267
+ }
268
+
108
269
  // ── Permission system ─────────────────────────────────────────────────────────
109
270
 
110
271
  let AUTO_APPROVE_SHELL = false;
111
272
  let AUTO_APPROVE_FILE = false;
112
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,
286
+ });
287
+ }
288
+
113
289
  function askPermission(actionType, description) {
114
290
  return new Promise((resolve) => {
115
291
  if (actionType === 'shell' && AUTO_APPROVE_SHELL) {
@@ -125,16 +301,20 @@ function askPermission(actionType, description) {
125
301
  console.log(` ${FG_YELLOW}${BOLD}⚠ Permission required${RST}`);
126
302
  console.log(` ${FG_GRAY}${actionType}: ${description}${RST}`);
127
303
  console.log();
128
- console.log(` ${FG_CYAN}[y]${RST} Yes ${FG_CYAN}[a]${RST} Yes, always ${FG_CYAN}[n]${RST} No`);
304
+ console.log(` ${FG_CYAN}${askPermissionLine(actionType)}${RST}`);
129
305
  console.log();
130
306
 
131
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
132
- rl.question(` ${FG_YELLOW}?${RST} `, (answer) => {
133
- rl.close();
134
- const choice = (answer || '').trim().toLowerCase();
135
- if (choice === 'y' || choice === 'yes') {
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();
315
+ if (choice === '1' || choice === 'y' || choice === 'yes') {
136
316
  resolve(true);
137
- } else if (choice === 'a' || choice === 'always') {
317
+ } else if (choice === '2' || choice === 'a' || choice === 'always') {
138
318
  if (actionType === 'shell') AUTO_APPROVE_SHELL = true;
139
319
  else AUTO_APPROVE_FILE = true;
140
320
  console.log(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for ${actionType} operations${RST}`);
@@ -869,38 +1049,35 @@ async function cmdChat(opts) {
869
1049
  let messages = [{ role: 'system', content: getSystemPrompt() }];
870
1050
  const cols = getCols();
871
1051
 
872
- const rl = readline.createInterface({
873
- input: process.stdin,
874
- output: process.stdout,
875
- terminal: true,
876
- });
1052
+ while (true) {
1053
+ const inputResult = await readInteractiveInput(` ${FG_TEAL}${BOLD}>${RST} `, {
1054
+ trim: false,
1055
+ allowCursorNavigation: true,
1056
+ });
877
1057
 
878
- rl.on('close', () => {
879
- console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
880
- process.exit(0);
881
- });
1058
+ if (inputResult.type === 'eof') {
1059
+ console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
1060
+ return;
1061
+ }
882
1062
 
883
- rl.on('SIGINT', () => {
884
- if (isRunningAgent) return;
885
- console.log(`\n ${FG_YELLOW}Use Ctrl+D or type exit to quit.${RST}`);
886
- rl.prompt(true);
887
- });
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
+ }
888
1069
 
889
- async function prompt() {
890
- rl.setPrompt(` ${FG_TEAL}${BOLD}>${RST} `);
891
- rl.question(rl.getPrompt(), async (input) => {
892
- const text = (input || '').trim();
1070
+ const text = (inputResult.value || '').trim();
893
1071
 
894
- if (!text) return prompt();
1072
+ if (!text) continue;
895
1073
 
896
- if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
897
- console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
898
- rl.close();
899
- return;
900
- }
1074
+ if (['exit', 'quit', '/exit', '/quit'].includes(text.toLowerCase())) {
1075
+ console.log(`\n ${FG_GRAY}Goodbye!${RST}\n`);
1076
+ return;
1077
+ }
901
1078
 
902
- if (text === '/help') {
903
- console.log(`
1079
+ if (text === '/help') {
1080
+ console.log(`
904
1081
  ${FG_BLUE}${BOLD}Commands:${RST}
905
1082
  ${FG_CYAN}/file <path>${RST} ${FG_GRAY}Load file or dir into context${RST}
906
1083
  ${FG_CYAN}/model${RST} ${FG_GRAY}Choose saved model profile${RST}
@@ -916,82 +1093,83 @@ async function cmdChat(opts) {
916
1093
 
917
1094
  ${FG_DARK}The AI can execute commands — you'll be asked to approve each one.${RST}
918
1095
  `);
919
- return prompt();
920
- }
1096
+ continue;
1097
+ }
921
1098
 
922
- if (text.startsWith('/file ')) {
923
- const fp = text.slice(6).trim();
924
- const ctx = readFileContext([fp]);
925
- if (ctx) messages.push({ role: 'user', content: `Here is the file context:\n${ctx}` });
926
- return prompt();
927
- }
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
+ }
928
1105
 
929
- 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
+ });
930
1113
  chooseSavedModelProfile(rl, currentModel, cwd, (nextModel) => {
931
1114
  currentModel = nextModel;
932
- prompt();
1115
+ rl.close();
1116
+ resolve();
933
1117
  });
934
- return;
935
- }
936
-
937
- if (text.startsWith('/model ')) {
938
- currentModel = text.slice(7).trim();
939
- console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model → ${currentModel}${RST}`);
940
- printStatusBar(currentModel, cwd);
941
- return prompt();
942
- }
1118
+ });
1119
+ continue;
1120
+ }
943
1121
 
944
- if (text === '/clear') {
945
- messages = [{ role: 'system', content: getSystemPrompt() }];
946
- AUTO_APPROVE_SHELL = false;
947
- AUTO_APPROVE_FILE = false;
948
- console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Conversation and approvals cleared${RST}\n`);
949
- return prompt();
950
- }
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
+ }
951
1128
 
952
- if (text === '/compact' || text === '/cost') {
953
- const total = messages.reduce((s, m) => s + estimateTokens(m.content), 0);
954
- console.log(` ${FG_GRAY}${messages.length - 1} messages · ~${total} tokens${RST}\n`);
955
- return prompt();
956
- }
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
+ }
957
1136
 
958
- if (text === '/config') {
959
- console.log(` ${FG_GRAY}${JSON.stringify(config, null, 2)}${RST}\n`);
960
- return prompt();
961
- }
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
+ }
962
1142
 
963
- if (text === '/approve') {
964
- AUTO_APPROVE_SHELL = !AUTO_APPROVE_SHELL;
965
- AUTO_APPROVE_FILE = !AUTO_APPROVE_FILE;
966
- const state = AUTO_APPROVE_SHELL ? 'ON' : 'OFF';
967
- const color = AUTO_APPROVE_SHELL ? FG_GREEN : FG_RED;
968
- console.log(` ${color}●${RST} ${FG_GRAY}Auto-approve: ${state}${RST}\n`);
969
- return prompt();
970
- }
1143
+ if (text === '/config') {
1144
+ console.log(` ${FG_GRAY}${JSON.stringify(config, null, 2)}${RST}\n`);
1145
+ continue;
1146
+ }
971
1147
 
972
- if (text.startsWith('/shell ') || text.startsWith('!')) {
973
- const cmd = text.startsWith('/shell ') ? text.slice(7).trim() : text.slice(1).trim();
974
- await agentExecShell(cmd);
975
- return prompt();
976
- }
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
+ }
977
1156
 
978
- messages.push({ role: 'user', content: text });
979
- 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
+ }
980
1162
 
981
- rl.pause();
982
- isRunningAgent = true;
983
- messages = await runAgentLoop(messages, currentModel);
984
- isRunningAgent = false;
985
- rl.resume();
1163
+ messages.push({ role: 'user', content: text });
1164
+ console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
986
1165
 
987
- console.log(` ${FG_DARK}${'━'.repeat(Math.min(cols, 70) - 4)}${RST}`);
988
- console.log();
1166
+ isRunningAgent = true;
1167
+ messages = await runAgentLoop(messages, currentModel);
1168
+ isRunningAgent = false;
989
1169
 
990
- prompt();
991
- });
1170
+ console.log(` ${FG_DARK}${'━'.repeat(Math.min(cols, 70) - 4)}${RST}`);
1171
+ console.log();
992
1172
  }
993
-
994
- prompt();
995
1173
  }
996
1174
 
997
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.2",
3
+ "version": "1.4.4",
4
4
  "description": "Self-hosted AI Coding Assistant CLI",
5
5
  "main": "index.js",
6
6
  "bin": {