bortexcode 1.2.6 → 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 +152 -2
  2. package/package.json +1 -1
package/bin/bortex.js CHANGED
@@ -1209,6 +1209,20 @@ function extractNaturalFileContent(text, filePath) {
1209
1209
  return stripMatchingQuotes(content);
1210
1210
  }
1211
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
+
1212
1226
  function buildNaturalLocalFileActionCalls(text) {
1213
1227
  const raw = String(text || '').trim();
1214
1228
  const lower = raw.toLowerCase();
@@ -1216,6 +1230,18 @@ function buildNaturalLocalFileActionCalls(text) {
1216
1230
  const filePath = extractNaturalFilePath(raw);
1217
1231
  if (!filePath) return null;
1218
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
+
1219
1245
  if (/(?:^|\b)(leggi|mostra|apri|read|show|cat)(?:\b|$)/i.test(raw) && /(?:file|path|percorso|\/|\\|\.\.?[\\/])/.test(raw)) {
1220
1246
  return [{ tool: 'read', path: filePath }];
1221
1247
  }
@@ -2723,6 +2749,9 @@ async function executeStructuredBuiltinTool(opts, call) {
2723
2749
  error: res.ok ? null : `${label} exit ${res.code == null ? 'null' : res.code}`
2724
2750
  };
2725
2751
  };
2752
+ if (tool === 'applyPatch') {
2753
+ return runUnifiedPatchTool(opts, call);
2754
+ }
2726
2755
  if (tool === 'read') {
2727
2756
  const filePath = resolveCliPath(opts, call.path);
2728
2757
  try {
@@ -2767,6 +2796,39 @@ async function executeStructuredBuiltinTool(opts, call) {
2767
2796
  return { ok: false, stdout: null, stderr: err.message, data: { path: filePath, mode: tool }, error: err.message };
2768
2797
  }
2769
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
+ }
2770
2832
  if (tool === 'gitStatus') {
2771
2833
  const res = await runChild('git', ['status', '--short', ...(call.branch ? ['-b'] : [])], { cwd: opts.cwd, shell: false });
2772
2834
  const entries = parseGitStatusShortEntries(res.stdout || '');
@@ -3023,6 +3085,19 @@ function validateStructuredToolCall(raw) {
3023
3085
  if (typeof call.text !== 'string') return { ok: false, error: 'Campo `text` mancante o non valido.' };
3024
3086
  break;
3025
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;
3026
3101
  case 'diff':
3027
3102
  case 'gitDiff':
3028
3103
  if (call.mode && !['unstaged', 'staged', 'all'].includes(String(call.mode))) {
@@ -3084,6 +3159,8 @@ function structuredToolCallToLocalCommand(call) {
3084
3159
  case 'mkdir': return `/mkdir ${call.path || '.'}`;
3085
3160
  case 'write': return `/write ${call.path} ${call.text}`;
3086
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)`;
3087
3164
  case 'diff': return `/diff ${call.mode || 'unstaged'}`;
3088
3165
  case 'gitDiff': return `gitDiff(${call.mode || 'all'}${call.statOnly === false ? ',patch' : ',stat'})`;
3089
3166
  case 'search': return `search(${call.pattern}${call.path ? ` in ${call.path}` : ''}${call.glob ? ` glob=${call.glob}` : ''})`;
@@ -3107,7 +3184,8 @@ function structuredToolCallToLocalCommand(call) {
3107
3184
 
3108
3185
  function isRiskyStructuredToolCall(call) {
3109
3186
  const t = String(call.tool || '');
3110
- 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;
3111
3189
  if (t === 'git') return !isReadonlyGitArgs(call.args);
3112
3190
  return false;
3113
3191
  }
@@ -3484,7 +3562,7 @@ async function buildStructuredToolPlanWithLlm(opts, goal, llmOptions = {}) {
3484
3562
 
3485
3563
  const profile = detectWorkspaceProfile(opts);
3486
3564
  const supportedTools = [
3487
- 'project', 'pwd', 'ls', 'tree', 'glob', 'read', 'readMany', 'mkdir', 'write', 'append',
3565
+ 'project', 'pwd', 'ls', 'tree', 'glob', 'read', 'readMany', 'mkdir', 'write', 'append', 'replace', 'applyPatch',
3488
3566
  'diff', 'git', 'gitStatus', 'gitDiff', 'search', 'runTest', 'runBuild', 'runLint', 'sshStatus', 'systemStatus', 'processStatus', 'portStatus', 'sh', 'review', 'commitSuggest'
3489
3567
  ];
3490
3568
  const testCmd = profile.suggested.test?.[0] || '';
@@ -3577,6 +3655,8 @@ async function buildStructuredToolPlanWithLlm(opts, goal, llmOptions = {}) {
3577
3655
  'Regole:',
3578
3656
  '- Preferisci tool read-only prima dei tool rischiosi',
3579
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',
3580
3660
  '- Includi review e commitSuggest verso la fine',
3581
3661
  '- Se proponi `runTest`/`runBuild`/`runLint` preferiscili a `sh` quando possibile',
3582
3662
  '- Se proponi `sh`, usa comandi test/build/lint concreti se disponibili',
@@ -3803,6 +3883,7 @@ function printLocalHelp() {
3803
3883
  console.log(' /patch-apply <patch-file>');
3804
3884
  console.log(' /patch-check-inline incolla patch e valida (.end per terminare)');
3805
3885
  console.log(' /patch-apply-inline incolla patch e applica (.end per terminare)');
3886
+ console.log(' /tool {"tool":"applyPatch","patch":"...","checkOnly":true|false}');
3806
3887
  console.log(' /todo add|list|done|rm|clear ...');
3807
3888
  console.log(' /plan [goal]');
3808
3889
  console.log(' /agent-local <goal> planner locale rapido');
@@ -3934,6 +4015,75 @@ function printPlan(opts) {
3934
4015
  (opts.plan.steps || []).forEach((s, i) => console.log(` ${i + 1}. ${s}`));
3935
4016
  }
3936
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
+
3937
4087
  async function runGitApplyInline(opts, mode) {
3938
4088
  if (typeof opts._readMultiline !== 'function') {
3939
4089
  throw new Error('Patch inline disponibile solo in REPL interattiva.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bortexcode",
3
- "version": "1.2.6",
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",