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.
- package/bin/bortex.js +152 -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.');
|