nothumanallowed 15.0.13 → 15.0.15
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/package.json +1 -1
- package/src/server/routes/webcraft.mjs +155 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.0.
|
|
3
|
+
"version": "15.0.15",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -731,22 +731,30 @@ WORKFLOW:
|
|
|
731
731
|
const isDone = stepResponse.includes('<done/>') || stepResponse.includes('<done />');
|
|
732
732
|
|
|
733
733
|
// Extract and execute ALL tool calls from this step
|
|
734
|
+
// First try matched pairs, then handle truncated (unclosed) tool tags
|
|
734
735
|
const toolRegex = /<tool>([\s\S]*?)<\/tool>/g;
|
|
735
736
|
let match;
|
|
736
737
|
const toolResults = [];
|
|
738
|
+
const matchedRanges = [];
|
|
737
739
|
|
|
738
740
|
while ((match = toolRegex.exec(stepResponse)) !== null) {
|
|
741
|
+
matchedRanges.push([match.index, match.index + match[0].length]);
|
|
739
742
|
let toolCall;
|
|
740
743
|
try {
|
|
741
|
-
// Fix common JSON issues from LLM
|
|
742
744
|
let raw = match[1].trim();
|
|
743
745
|
raw = raw.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
|
|
744
746
|
toolCall = JSON.parse(raw);
|
|
745
|
-
} catch
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
747
|
+
} catch {
|
|
748
|
+
// LLM often puts raw HTML/code inside JSON values without proper escaping.
|
|
749
|
+
// Extract fields manually using a robust regex-based parser.
|
|
750
|
+
try {
|
|
751
|
+
toolCall = _parseToolCallRobust(match[1].trim());
|
|
752
|
+
} catch (parseErr2) {
|
|
753
|
+
console.error('[TOOL-PARSE] JSON parse failed even after robust parse:', parseErr2.message, 'raw:', match[1].slice(0, 200));
|
|
754
|
+
toolResults.push({ op: 'error', result: 'JSON parse failed' });
|
|
755
|
+
emit({ type: 'tool', op: 'parse_error', path: '', result: 'json_parse_failed' });
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
750
758
|
}
|
|
751
759
|
|
|
752
760
|
const { op, path: relPath, old: oldStr, new: newStr, content, newPath, query, glob: globPat, cmd } = toolCall;
|
|
@@ -1067,6 +1075,60 @@ WORKFLOW:
|
|
|
1067
1075
|
}
|
|
1068
1076
|
}
|
|
1069
1077
|
|
|
1078
|
+
// Handle truncated tool call — <tool> without </tool> (response cut off by max_tokens)
|
|
1079
|
+
const lastToolOpen = stepResponse.lastIndexOf('<tool>');
|
|
1080
|
+
if (lastToolOpen >= 0) {
|
|
1081
|
+
const alreadyMatched = matchedRanges.some(([start, end]) => lastToolOpen >= start && lastToolOpen < end);
|
|
1082
|
+
if (!alreadyMatched) {
|
|
1083
|
+
const truncatedRaw = stepResponse.slice(lastToolOpen + 6).trim();
|
|
1084
|
+
if (truncatedRaw.length > 20) {
|
|
1085
|
+
console.log('[TOOL-TRUNCATED] Found unclosed <tool> tag, attempting robust parse. Length:', truncatedRaw.length);
|
|
1086
|
+
try {
|
|
1087
|
+
const toolCall = _parseToolCallRobust(truncatedRaw);
|
|
1088
|
+
// Execute the truncated tool call
|
|
1089
|
+
const { op, path: relPath, old: oldStr, new: newStr, content } = toolCall;
|
|
1090
|
+
if (op === 'edit' && relPath && oldStr) {
|
|
1091
|
+
const src = ProjectStore.readFile(projectName, relPath);
|
|
1092
|
+
if (src && src.includes(oldStr)) {
|
|
1093
|
+
const newSrc = src.replace(oldStr, newStr ?? '');
|
|
1094
|
+
ProjectStore.writeFile(projectName, relPath, newSrc);
|
|
1095
|
+
hasChanges = true;
|
|
1096
|
+
modifiedFiles.add(relPath);
|
|
1097
|
+
toolResults.push({ op: 'edit', path: relPath, result: 'ok' });
|
|
1098
|
+
emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 2000), newSnippet: newStr?.slice(0, 2000) ?? '' });
|
|
1099
|
+
console.log('[TOOL-TRUNCATED] Edit applied successfully from truncated tool call');
|
|
1100
|
+
} else if (src) {
|
|
1101
|
+
// Try fuzzy match
|
|
1102
|
+
const oldLines = oldStr.split('\n').map(l => l.trim());
|
|
1103
|
+
const srcLines = src.split('\n');
|
|
1104
|
+
let matchStart = -1;
|
|
1105
|
+
for (let i = 0; i <= srcLines.length - oldLines.length; i++) {
|
|
1106
|
+
let ok = true;
|
|
1107
|
+
for (let j = 0; j < oldLines.length; j++) {
|
|
1108
|
+
if (srcLines[i + j].trim() !== oldLines[j]) { ok = false; break; }
|
|
1109
|
+
}
|
|
1110
|
+
if (ok) { matchStart = i; break; }
|
|
1111
|
+
}
|
|
1112
|
+
if (matchStart >= 0) {
|
|
1113
|
+
const before = srcLines.slice(0, matchStart).join('\n');
|
|
1114
|
+
const after = srcLines.slice(matchStart + oldLines.length).join('\n');
|
|
1115
|
+
const result = (before ? before + '\n' : '') + (newStr ?? '') + (after ? '\n' + after : '');
|
|
1116
|
+
ProjectStore.writeFile(projectName, relPath, result);
|
|
1117
|
+
hasChanges = true;
|
|
1118
|
+
modifiedFiles.add(relPath);
|
|
1119
|
+
toolResults.push({ op: 'edit', path: relPath, result: 'ok' });
|
|
1120
|
+
emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 2000), newSnippet: newStr?.slice(0, 2000) ?? '' });
|
|
1121
|
+
console.log('[TOOL-TRUNCATED] Edit applied via fuzzy match from truncated tool call');
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
console.log('[TOOL-TRUNCATED] Failed to parse truncated tool:', e.message);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1070
1132
|
// If any edit failed, ignore <done/> — force retry
|
|
1071
1133
|
const hasFailedEdit = toolResults.some((r) => r.op === 'edit' && (r.result?.includes('not_found') || r.result?.includes('blocked')));
|
|
1072
1134
|
if (hasFailedEdit && step < MAX_STEPS - 1) {
|
|
@@ -2656,6 +2718,93 @@ function _safeName(name) {
|
|
|
2656
2718
|
return (name ?? '').replace(/[^a-zA-Z0-9_\-. ]/g, '_').trim() || 'unnamed';
|
|
2657
2719
|
}
|
|
2658
2720
|
|
|
2721
|
+
/**
|
|
2722
|
+
* Robust tool call parser for when JSON.parse fails.
|
|
2723
|
+
* The LLM often puts raw HTML/code with unescaped quotes, newlines, < > inside JSON values.
|
|
2724
|
+
* This extracts "op", "path", "old", "new", "content", "query", "cmd" etc. by finding
|
|
2725
|
+
* the key-value boundaries manually.
|
|
2726
|
+
*/
|
|
2727
|
+
function _parseToolCallRobust(raw) {
|
|
2728
|
+
const result = {};
|
|
2729
|
+
|
|
2730
|
+
// Extract "op" — always a simple string
|
|
2731
|
+
const opMatch = raw.match(/"op"\s*[:>]\s*"([^"]+)"/);
|
|
2732
|
+
if (opMatch) result.op = opMatch[1];
|
|
2733
|
+
|
|
2734
|
+
// Extract "path" — simple string
|
|
2735
|
+
const pathMatch = raw.match(/"path"\s*[:>]\s*"([^"]+)"/);
|
|
2736
|
+
if (pathMatch) result.path = pathMatch[1];
|
|
2737
|
+
|
|
2738
|
+
// Extract "newPath" — simple string
|
|
2739
|
+
const newPathMatch = raw.match(/"newPath"\s*[:>]\s*"([^"]+)"/);
|
|
2740
|
+
if (newPathMatch) result.newPath = newPathMatch[1];
|
|
2741
|
+
|
|
2742
|
+
// Extract "query" — simple string (fix \| to |)
|
|
2743
|
+
const queryMatch = raw.match(/"query"\s*[:>]\s*"([^"]+)"/);
|
|
2744
|
+
if (queryMatch) result.query = queryMatch[1].replace(/\\\|/g, '|');
|
|
2745
|
+
|
|
2746
|
+
// Extract "glob" — simple string
|
|
2747
|
+
const globMatch = raw.match(/"glob"\s*[:>]\s*"([^"]+)"/);
|
|
2748
|
+
if (globMatch) result.glob = globMatch[1];
|
|
2749
|
+
|
|
2750
|
+
// Extract "cmd" — simple string
|
|
2751
|
+
const cmdMatch = raw.match(/"cmd"\s*[:>]\s*"([^"]+)"/);
|
|
2752
|
+
if (cmdMatch) result.cmd = cmdMatch[1];
|
|
2753
|
+
|
|
2754
|
+
// For edit: extract "old" and "new" — these can be HUGE multiline strings with HTML
|
|
2755
|
+
// Strategy: find "old" key, then grab everything until we hit ","new" or the end
|
|
2756
|
+
if (result.op === 'edit') {
|
|
2757
|
+
const oldIdx = raw.indexOf('"old"');
|
|
2758
|
+
const newIdx = raw.indexOf('"new"');
|
|
2759
|
+
if (oldIdx >= 0 && newIdx > oldIdx) {
|
|
2760
|
+
// old value is between "old":"/>" and ","new"
|
|
2761
|
+
let oldStart = raw.indexOf('"', oldIdx + 5); // find opening quote after "old":
|
|
2762
|
+
if (oldStart < 0) oldStart = raw.indexOf('>', oldIdx + 5); // handle "old">
|
|
2763
|
+
if (oldStart >= 0) {
|
|
2764
|
+
oldStart++; // skip the opening quote/bracket
|
|
2765
|
+
// Find the end — look for ","new" pattern
|
|
2766
|
+
const oldEnd = raw.lastIndexOf('"', newIdx - 1);
|
|
2767
|
+
if (oldEnd > oldStart) {
|
|
2768
|
+
result.old = raw.slice(oldStart, oldEnd);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
// new value is after "new":"
|
|
2772
|
+
let newStart = raw.indexOf('"', newIdx + 5);
|
|
2773
|
+
if (newStart < 0) newStart = raw.indexOf('>', newIdx + 5);
|
|
2774
|
+
if (newStart >= 0) {
|
|
2775
|
+
newStart++;
|
|
2776
|
+
// Find end — last " before the closing }
|
|
2777
|
+
const lastBrace = raw.lastIndexOf('}');
|
|
2778
|
+
const newEnd = raw.lastIndexOf('"', lastBrace);
|
|
2779
|
+
if (newEnd > newStart) {
|
|
2780
|
+
result.new = raw.slice(newStart, newEnd);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// For write: extract "content"
|
|
2787
|
+
if (result.op === 'write') {
|
|
2788
|
+
const contentIdx = raw.indexOf('"content"');
|
|
2789
|
+
if (contentIdx >= 0) {
|
|
2790
|
+
let contentStart = raw.indexOf('"', contentIdx + 9);
|
|
2791
|
+
if (contentStart < 0) contentStart = raw.indexOf('>', contentIdx + 9);
|
|
2792
|
+
if (contentStart >= 0) {
|
|
2793
|
+
contentStart++;
|
|
2794
|
+
const lastBrace = raw.lastIndexOf('}');
|
|
2795
|
+
const contentEnd = raw.lastIndexOf('"', lastBrace);
|
|
2796
|
+
if (contentEnd > contentStart) {
|
|
2797
|
+
result.content = raw.slice(contentStart, contentEnd);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
if (!result.op) throw new Error('Could not extract op from tool call');
|
|
2804
|
+
console.log(`[TOOL-PARSE] Robust parse recovered: op=${result.op} path=${result.path} old.len=${result.old?.length ?? 0} new.len=${result.new?.length ?? 0}`);
|
|
2805
|
+
return result;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2659
2808
|
function _isSafePath(relPath) {
|
|
2660
2809
|
if (!relPath || typeof relPath !== 'string') return false;
|
|
2661
2810
|
if (relPath.includes('..') || path.isAbsolute(relPath)) return false;
|