bortexcode 1.2.5 → 1.2.8

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/bin/bortex.js +289 -2
  2. package/package.json +1 -1
package/bin/bortex.js CHANGED
@@ -1167,6 +1167,111 @@ function classifyLocalPromptIntent(text) {
1167
1167
  return null;
1168
1168
  }
1169
1169
 
1170
+ function stripMatchingQuotes(value) {
1171
+ const s = String(value || '').trim();
1172
+ if (s.length >= 2 && ((s[0] === '"' && s[s.length - 1] === '"') || (s[0] === '\'' && s[s.length - 1] === '\'') || (s[0] === '`' && s[s.length - 1] === '`'))) {
1173
+ return s.slice(1, -1);
1174
+ }
1175
+ return s;
1176
+ }
1177
+
1178
+ function extractNaturalFilePath(text) {
1179
+ const raw = String(text || '');
1180
+ const quotedAfterFile = raw.match(/(?:file|path|percorso)\s+["'`]([^"'`]+)["'`]/i);
1181
+ if (quotedAfterFile?.[1]) return quotedAfterFile[1].trim();
1182
+ const tokenAfterFile = raw.match(/(?:file|path|percorso)\s+([^\s,;]+)/i);
1183
+ if (tokenAfterFile?.[1]) return stripMatchingQuotes(tokenAfterFile[1]);
1184
+ const absoluteOrRelative = raw.match(/["'`]?((?:[A-Za-z]:[\\/]|\/|~\/|\.{1,2}[\\/])[^"'`\s,;:]+)["'`]?/);
1185
+ if (absoluteOrRelative?.[1]) return absoluteOrRelative[1].trim();
1186
+ return '';
1187
+ }
1188
+
1189
+ function extractNaturalFileContent(text, filePath) {
1190
+ const raw = String(text || '').trim();
1191
+ const marker = raw.match(/(?:con\s+(?:contenuto|testo)|contenente|with\s+(?:content|text)|containing)\s+([\s\S]+)$/i);
1192
+ let content = marker?.[1] || '';
1193
+ if (!content && filePath) {
1194
+ const beforeFile = raw.slice(0, raw.toLowerCase().indexOf(filePath.toLowerCase())).trim();
1195
+ const writePrefix = beforeFile.match(/(?:scrivi|write|salva|metti)\s+([\s\S]+?)(?:\s+(?:nel|nella|in|su|to)\s+(?:file|path|percorso)?\s*)?$/i);
1196
+ if (writePrefix?.[1]) content = writePrefix[1];
1197
+ }
1198
+ content = String(content || '').trim();
1199
+ if (!content) return '';
1200
+ if ((content[0] === '"' || content[0] === '\'' || content[0] === '`')) {
1201
+ const quote = content[0];
1202
+ const end = content.indexOf(quote, 1);
1203
+ if (end > 0) return content.slice(1, end);
1204
+ }
1205
+ content = content
1206
+ .replace(/\s+(?:e|and|poi|then)\s+(?:rispondi|reply|dimmi|dillo|fammi sapere|tell me)\b[\s\S]*$/i, '')
1207
+ .replace(/\s+(?:rispondi|reply)\b[\s\S]*$/i, '')
1208
+ .trim();
1209
+ return stripMatchingQuotes(content);
1210
+ }
1211
+
1212
+ function extractNaturalReplaceParts(text) {
1213
+ const raw = String(text || '').trim();
1214
+ const quotedPair = raw.match(/(?:sostituisci|rimpiazza|replace)\s+(["'`])([\s\S]*?)\1\s+(?:con|with)\s+(["'`])([\s\S]*?)\3/i);
1215
+ if (quotedPair) {
1216
+ return { search: quotedPair[2], replacement: quotedPair[4] };
1217
+ }
1218
+ const plainPair = raw.match(/(?:sostituisci|rimpiazza|replace)\s+([\s\S]+?)\s+(?:con|with)\s+([\s\S]+?)(?:\s+(?:nel|nella|in|su)\s+(?:file|path|percorso)?\s|$)/i);
1219
+ if (!plainPair) return null;
1220
+ const search = stripMatchingQuotes(plainPair[1]);
1221
+ const replacement = stripMatchingQuotes(plainPair[2]);
1222
+ if (!search) return null;
1223
+ return { search, replacement };
1224
+ }
1225
+
1226
+ function buildNaturalLocalFileActionCalls(text) {
1227
+ const raw = String(text || '').trim();
1228
+ const lower = raw.toLowerCase();
1229
+ if (!raw) return null;
1230
+ const filePath = extractNaturalFilePath(raw);
1231
+ if (!filePath) return null;
1232
+
1233
+ const replaceParts = extractNaturalReplaceParts(raw);
1234
+ if (replaceParts) {
1235
+ return [{
1236
+ tool: 'replace',
1237
+ path: filePath,
1238
+ search: replaceParts.search,
1239
+ replacement: replaceParts.replacement,
1240
+ all: true,
1241
+ confirm: true
1242
+ }];
1243
+ }
1244
+
1245
+ if (/(?:^|\b)(leggi|mostra|apri|read|show|cat)(?:\b|$)/i.test(raw) && /(?:file|path|percorso|\/|\\|\.\.?[\\/])/.test(raw)) {
1246
+ return [{ tool: 'read', path: filePath }];
1247
+ }
1248
+
1249
+ const wantsAppend = /(?:^|\b)(appendi|accoda|aggiungi|append|add)(?:\b|$)/i.test(raw) && /(?:file|path|percorso|\/|\\|\.\.?[\\/])/.test(raw);
1250
+ const wantsWrite = /(?:^|\b)(crea|creare|scrivi|write|create|salva|metti)(?:\b|$)/i.test(raw) && /(?:file|path|percorso|\/|\\|\.\.?[\\/])/.test(raw);
1251
+ if (!wantsAppend && !wantsWrite) return null;
1252
+
1253
+ const textValue = extractNaturalFileContent(raw, filePath);
1254
+ if (!textValue && !/(?:^|\b)(crea|create)(?:\b|$)/i.test(raw)) return null;
1255
+ return [{
1256
+ tool: wantsAppend ? 'append' : 'write',
1257
+ path: filePath,
1258
+ text: textValue,
1259
+ confirm: true
1260
+ }];
1261
+ }
1262
+
1263
+ async function runNaturalLocalFileAction(opts, prompt) {
1264
+ const calls = buildNaturalLocalFileActionCalls(prompt);
1265
+ if (!calls) return { handled: false };
1266
+ opts.lastToolRunReport = await executeStructuredToolBatch(opts, calls, {
1267
+ continueOnError: true,
1268
+ goal: prompt,
1269
+ source: 'natural-local-file-action',
1270
+ reportLabel: 'natural-local-action'
1271
+ });
1272
+ return { handled: true, data: opts.lastToolRunReport };
1273
+ }
1274
+
1170
1275
  async function runSshStatusCommand(opts, { quiet = false } = {}) {
1171
1276
  const cwd = opts?.cwd || process.cwd();
1172
1277
  const platform = os.platform();
@@ -2644,6 +2749,86 @@ async function executeStructuredBuiltinTool(opts, call) {
2644
2749
  error: res.ok ? null : `${label} exit ${res.code == null ? 'null' : res.code}`
2645
2750
  };
2646
2751
  };
2752
+ if (tool === 'applyPatch') {
2753
+ return runUnifiedPatchTool(opts, call);
2754
+ }
2755
+ if (tool === 'read') {
2756
+ const filePath = resolveCliPath(opts, call.path);
2757
+ try {
2758
+ const text = fs.readFileSync(filePath, 'utf8');
2759
+ process.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
2760
+ return {
2761
+ ok: true,
2762
+ stdout: text,
2763
+ stderr: null,
2764
+ data: { path: filePath, bytes: Buffer.byteLength(text, 'utf8') }
2765
+ };
2766
+ } catch (err) {
2767
+ return { ok: false, stdout: null, stderr: err.message, data: { path: filePath }, error: err.message };
2768
+ }
2769
+ }
2770
+ if (tool === 'mkdir') {
2771
+ const dirPath = resolveCliPath(opts, call.path || '.');
2772
+ try {
2773
+ fs.mkdirSync(dirPath, { recursive: true });
2774
+ console.log(`Mkdir ok -> ${dirPath}`);
2775
+ return { ok: true, stdout: `Mkdir ok -> ${dirPath}\n`, stderr: null, data: { path: dirPath } };
2776
+ } catch (err) {
2777
+ return { ok: false, stdout: null, stderr: err.message, data: { path: dirPath }, error: err.message };
2778
+ }
2779
+ }
2780
+ if (tool === 'write' || tool === 'append') {
2781
+ const filePath = resolveCliPath(opts, call.path);
2782
+ const text = String(call.text ?? '');
2783
+ try {
2784
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
2785
+ if (tool === 'append') fs.appendFileSync(filePath, text, 'utf8');
2786
+ else fs.writeFileSync(filePath, text, 'utf8');
2787
+ const label = tool === 'append' ? 'Append' : 'Write';
2788
+ console.log(`${label} ok -> ${filePath}`);
2789
+ return {
2790
+ ok: true,
2791
+ stdout: `${label} ok -> ${filePath}\n`,
2792
+ stderr: null,
2793
+ data: { path: filePath, bytes: Buffer.byteLength(text, 'utf8'), mode: tool }
2794
+ };
2795
+ } catch (err) {
2796
+ return { ok: false, stdout: null, stderr: err.message, data: { path: filePath, mode: tool }, error: err.message };
2797
+ }
2798
+ }
2799
+ if (tool === 'replace') {
2800
+ const filePath = resolveCliPath(opts, call.path);
2801
+ const search = String(call.search ?? '');
2802
+ const replacement = String(call.replacement ?? '');
2803
+ const replaceAll = call.all !== false;
2804
+ try {
2805
+ if (!search) {
2806
+ return { ok: false, stdout: null, stderr: 'empty search string', data: { path: filePath }, error: 'empty search string' };
2807
+ }
2808
+ const before = fs.readFileSync(filePath, 'utf8');
2809
+ const count = before.split(search).length - 1;
2810
+ if (count <= 0) {
2811
+ const msg = `Replace no match -> ${filePath}`;
2812
+ console.log(msg);
2813
+ return { ok: false, stdout: `${msg}\n`, stderr: 'no matches', data: { path: filePath, search, count: 0 }, error: 'no matches' };
2814
+ }
2815
+ const after = replaceAll
2816
+ ? before.split(search).join(replacement)
2817
+ : before.replace(search, replacement);
2818
+ fs.writeFileSync(filePath, after, 'utf8');
2819
+ const applied = replaceAll ? count : 1;
2820
+ const msg = `Replace ok -> ${filePath} (${applied} occurrence${applied === 1 ? '' : 's'})`;
2821
+ console.log(msg);
2822
+ return {
2823
+ ok: true,
2824
+ stdout: `${msg}\n`,
2825
+ stderr: null,
2826
+ data: { path: filePath, search, replacement, count: applied, all: replaceAll, bytes: Buffer.byteLength(after, 'utf8') }
2827
+ };
2828
+ } catch (err) {
2829
+ return { ok: false, stdout: null, stderr: err.message, data: { path: filePath, search }, error: err.message };
2830
+ }
2831
+ }
2647
2832
  if (tool === 'gitStatus') {
2648
2833
  const res = await runChild('git', ['status', '--short', ...(call.branch ? ['-b'] : [])], { cwd: opts.cwd, shell: false });
2649
2834
  const entries = parseGitStatusShortEntries(res.stdout || '');
@@ -2900,6 +3085,19 @@ function validateStructuredToolCall(raw) {
2900
3085
  if (typeof call.text !== 'string') return { ok: false, error: 'Campo `text` mancante o non valido.' };
2901
3086
  break;
2902
3087
  }
3088
+ case 'replace': {
3089
+ const e1 = requireString('path');
3090
+ if (e1) return { ok: false, error: e1 };
3091
+ const e2 = requireString('search');
3092
+ if (e2) return { ok: false, error: e2 };
3093
+ if (typeof call.replacement !== 'string') return { ok: false, error: 'Campo `replacement` mancante o non valido.' };
3094
+ if (call.all != null && typeof call.all !== 'boolean') return { ok: false, error: '`all` deve essere boolean.' };
3095
+ break;
3096
+ }
3097
+ case 'applyPatch':
3098
+ if (typeof call.patch !== 'string' || !call.patch.trim()) return { ok: false, error: 'Campo `patch` mancante o non valido.' };
3099
+ if (call.checkOnly != null && typeof call.checkOnly !== 'boolean') return { ok: false, error: '`checkOnly` deve essere boolean.' };
3100
+ break;
2903
3101
  case 'diff':
2904
3102
  case 'gitDiff':
2905
3103
  if (call.mode && !['unstaged', 'staged', 'all'].includes(String(call.mode))) {
@@ -2961,6 +3159,8 @@ function structuredToolCallToLocalCommand(call) {
2961
3159
  case 'mkdir': return `/mkdir ${call.path || '.'}`;
2962
3160
  case 'write': return `/write ${call.path} ${call.text}`;
2963
3161
  case 'append': return `/append ${call.path} ${call.text}`;
3162
+ case 'replace': return `replace(${call.path}: ${call.search} -> ${call.replacement}${call.all === false ? ', first' : ', all'})`;
3163
+ case 'applyPatch': return `applyPatch(${call.checkOnly ? 'check' : 'apply'}, ${String(call.patch || '').length} chars)`;
2964
3164
  case 'diff': return `/diff ${call.mode || 'unstaged'}`;
2965
3165
  case 'gitDiff': return `gitDiff(${call.mode || 'all'}${call.statOnly === false ? ',patch' : ',stat'})`;
2966
3166
  case 'search': return `search(${call.pattern}${call.path ? ` in ${call.path}` : ''}${call.glob ? ` glob=${call.glob}` : ''})`;
@@ -2984,7 +3184,8 @@ function structuredToolCallToLocalCommand(call) {
2984
3184
 
2985
3185
  function isRiskyStructuredToolCall(call) {
2986
3186
  const t = String(call.tool || '');
2987
- if (['write', 'append', 'mkdir', 'sh', 'runTest', 'runBuild', 'runLint'].includes(t)) return true;
3187
+ if (['write', 'append', 'replace', 'mkdir', 'sh', 'runTest', 'runBuild', 'runLint'].includes(t)) return true;
3188
+ if (t === 'applyPatch') return call.checkOnly !== true;
2988
3189
  if (t === 'git') return !isReadonlyGitArgs(call.args);
2989
3190
  return false;
2990
3191
  }
@@ -3222,6 +3423,11 @@ function buildStructuredToolPlanFromGoal(opts, goal) {
3222
3423
  { tool: 'review', mode: 'all', fixSuggestions: true }
3223
3424
  ];
3224
3425
 
3426
+ const naturalFileCalls = buildNaturalLocalFileActionCalls(g);
3427
+ if (naturalFileCalls) {
3428
+ return naturalFileCalls;
3429
+ }
3430
+
3225
3431
  const wantsStatus = /(verifica|controlla|check|mostra|stato|status|attivo|active|running|enabled|on)/.test(lower);
3226
3432
  if (wantsStatus && /(\bssh\b|sshd|remote login)/.test(lower)) {
3227
3433
  return [{ tool: 'project' }, { tool: 'sshStatus' }];
@@ -3356,7 +3562,7 @@ async function buildStructuredToolPlanWithLlm(opts, goal, llmOptions = {}) {
3356
3562
 
3357
3563
  const profile = detectWorkspaceProfile(opts);
3358
3564
  const supportedTools = [
3359
- 'project', 'pwd', 'ls', 'tree', 'glob', 'read', 'readMany', 'mkdir', 'write', 'append',
3565
+ 'project', 'pwd', 'ls', 'tree', 'glob', 'read', 'readMany', 'mkdir', 'write', 'append', 'replace', 'applyPatch',
3360
3566
  'diff', 'git', 'gitStatus', 'gitDiff', 'search', 'runTest', 'runBuild', 'runLint', 'sshStatus', 'systemStatus', 'processStatus', 'portStatus', 'sh', 'review', 'commitSuggest'
3361
3567
  ];
3362
3568
  const testCmd = profile.suggested.test?.[0] || '';
@@ -3449,6 +3655,8 @@ async function buildStructuredToolPlanWithLlm(opts, goal, llmOptions = {}) {
3449
3655
  'Regole:',
3450
3656
  '- Preferisci tool read-only prima dei tool rischiosi',
3451
3657
  '- Per controlli di stato locale usa sshStatus/systemStatus/processStatus/portStatus invece di sh quando possibile',
3658
+ '- Per modifiche testuali puntuali usa replace invece di sh',
3659
+ '- Per modifiche multi-file o blocchi ampi usa applyPatch con una unified diff valida; usa checkOnly:true se devi solo validare',
3452
3660
  '- Includi review e commitSuggest verso la fine',
3453
3661
  '- Se proponi `runTest`/`runBuild`/`runLint` preferiscili a `sh` quando possibile',
3454
3662
  '- Se proponi `sh`, usa comandi test/build/lint concreti se disponibili',
@@ -3675,6 +3883,7 @@ function printLocalHelp() {
3675
3883
  console.log(' /patch-apply <patch-file>');
3676
3884
  console.log(' /patch-check-inline incolla patch e valida (.end per terminare)');
3677
3885
  console.log(' /patch-apply-inline incolla patch e applica (.end per terminare)');
3886
+ console.log(' /tool {"tool":"applyPatch","patch":"...","checkOnly":true|false}');
3678
3887
  console.log(' /todo add|list|done|rm|clear ...');
3679
3888
  console.log(' /plan [goal]');
3680
3889
  console.log(' /agent-local <goal> planner locale rapido');
@@ -3806,6 +4015,75 @@ function printPlan(opts) {
3806
4015
  (opts.plan.steps || []).forEach((s, i) => console.log(` ${i + 1}. ${s}`));
3807
4016
  }
3808
4017
 
4018
+ async function runUnifiedPatchTool(opts, call) {
4019
+ const patch = String(call.patch || '');
4020
+ const checkOnly = call.checkOnly === true;
4021
+ const data = {
4022
+ cwd: opts.cwd,
4023
+ patchChars: patch.length,
4024
+ checkOnly,
4025
+ checkOk: false,
4026
+ applied: false
4027
+ };
4028
+ if (!patch.trim()) {
4029
+ return { ok: false, stdout: null, stderr: 'empty patch', data, error: 'empty patch' };
4030
+ }
4031
+
4032
+ const tmp = path.join(os.tmpdir(), `bortex-tool-${Date.now()}-${Math.random().toString(16).slice(2)}.patch`);
4033
+ fs.writeFileSync(tmp, patch, 'utf8');
4034
+ try {
4035
+ const check = await runChild('git', ['apply', '--check', '--verbose', tmp], { cwd: opts.cwd, shell: false });
4036
+ const checkStdout = String(check.stdout || '');
4037
+ const checkStderr = String(check.stderr || '');
4038
+ if (checkStdout) process.stdout.write(checkStdout.endsWith('\n') ? checkStdout : `${checkStdout}\n`);
4039
+ if (checkStderr) process.stderr.write(checkStderr.endsWith('\n') ? checkStderr : `${checkStderr}\n`);
4040
+ data.checkOk = !!check.ok;
4041
+ data.checkExitCode = check.code == null ? null : check.code;
4042
+
4043
+ if (!check.ok) {
4044
+ const msg = 'Patch check failed';
4045
+ console.log(msg);
4046
+ return {
4047
+ ok: false,
4048
+ stdout: checkStdout || null,
4049
+ stderr: checkStderr || msg,
4050
+ data,
4051
+ error: msg
4052
+ };
4053
+ }
4054
+
4055
+ if (checkOnly) {
4056
+ const msg = 'Patch check ok';
4057
+ console.log(msg);
4058
+ return {
4059
+ ok: true,
4060
+ stdout: [checkStdout, `${msg}\n`].filter(Boolean).join(''),
4061
+ stderr: checkStderr || null,
4062
+ data
4063
+ };
4064
+ }
4065
+
4066
+ const applied = await runChild('git', ['apply', '--verbose', '--whitespace=nowarn', tmp], { cwd: opts.cwd, shell: false });
4067
+ const applyStdout = String(applied.stdout || '');
4068
+ const applyStderr = String(applied.stderr || '');
4069
+ if (applyStdout) process.stdout.write(applyStdout.endsWith('\n') ? applyStdout : `${applyStdout}\n`);
4070
+ if (applyStderr) process.stderr.write(applyStderr.endsWith('\n') ? applyStderr : `${applyStderr}\n`);
4071
+ data.applied = !!applied.ok;
4072
+ data.applyExitCode = applied.code == null ? null : applied.code;
4073
+ const msg = applied.ok ? 'Patch apply ok' : 'Patch apply failed';
4074
+ console.log(msg);
4075
+ return {
4076
+ ok: !!applied.ok,
4077
+ stdout: [checkStdout, applyStdout, `${msg}\n`].filter(Boolean).join(''),
4078
+ stderr: [checkStderr, applyStderr].filter(Boolean).join('\n') || null,
4079
+ data,
4080
+ error: applied.ok ? null : msg
4081
+ };
4082
+ } finally {
4083
+ try { fs.unlinkSync(tmp); } catch (_) {}
4084
+ }
4085
+ }
4086
+
3809
4087
  async function runGitApplyInline(opts, mode) {
3810
4088
  if (typeof opts._readMultiline !== 'function') {
3811
4089
  throw new Error('Patch inline disponibile solo in REPL interattiva.');
@@ -6397,6 +6675,8 @@ async function runSinglePrompt(opts) {
6397
6675
  const localResult = await handleLocalCommand(opts, opts.prompt);
6398
6676
  if (localResult.handled) return;
6399
6677
  }
6678
+ const naturalFileAction = await runNaturalLocalFileAction(opts, opts.prompt);
6679
+ if (naturalFileAction.handled) return;
6400
6680
  const localIntent = classifyLocalPromptIntent(opts.prompt);
6401
6681
  if (localIntent?.command) {
6402
6682
  const localResult = await handleLocalCommand(opts, localIntent.command);
@@ -6495,6 +6775,13 @@ async function runRepl(opts) {
6495
6775
  continue;
6496
6776
  }
6497
6777
  }
6778
+ try {
6779
+ const naturalFileAction = await runNaturalLocalFileAction(opts, line);
6780
+ if (naturalFileAction.handled) continue;
6781
+ } catch (err) {
6782
+ console.error(`Local tool error: ${err.message}`);
6783
+ continue;
6784
+ }
6498
6785
  if (opts.offline) {
6499
6786
  console.log('Offline mode: use /help for local tools or restart without --offline to use the server.');
6500
6787
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bortexcode",
3
- "version": "1.2.5",
3
+ "version": "1.2.8",
4
4
  "description": "Bortex Code CLI - AI coding assistant powered by bortex.site",
5
5
  "homepage": "https://bortex.site",
6
6
  "license": "UNLICENSED",