nothumanallowed 16.0.34 → 16.0.35
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/constants.mjs +1 -1
- package/src/server/routes/webcraft.mjs +218 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "16.0.
|
|
3
|
+
"version": "16.0.35",
|
|
4
4
|
"description": "Local AI assistant: 80 tools (Gmail, Calendar, Drive, GitHub, Slack, browser, code, files), 38 agents, visual workflows (Studio, AWF, WebCraft). Install with `npm i -g nothumanallowed`, run with `nha ui`. Free tier built-in (Liara), no API key required. Your data stays on your PC — OAuth tokens local, no cloud. Open-source MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '16.0.
|
|
8
|
+
export const VERSION = '16.0.35';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -1268,9 +1268,30 @@ RULES:
|
|
|
1268
1268
|
);
|
|
1269
1269
|
} else {
|
|
1270
1270
|
// Fallback for other providers — use old text-based <tool> system
|
|
1271
|
-
// (kept for backward compatibility with Gemini, DeepSeek, etc.)
|
|
1271
|
+
// (kept for backward compatibility with Gemini, DeepSeek, Liara, etc.)
|
|
1272
1272
|
let conversationHistory = [{ role: 'user', content: userContent }];
|
|
1273
1273
|
emit({ type: 'text', token: 'Note: Using text-based tools (native tool calling not available for this provider).\n' });
|
|
1274
|
+
// Inject text-based tool-format instructions into the user message so the
|
|
1275
|
+
// LLM emits calls in a format the parser recognizes. The parser now ALSO
|
|
1276
|
+
// accepts OpenAI/Anthropic-style nude JSON, but the wrapper form is more
|
|
1277
|
+
// robust against truncation.
|
|
1278
|
+
const textBasedFormat = `
|
|
1279
|
+
TOOL CALL FORMAT (CRITICAL):
|
|
1280
|
+
You MUST emit tool calls using THIS exact format (one per line, on its own line):
|
|
1281
|
+
|
|
1282
|
+
<tool>{"op": "read", "path": "public/index.html"}</tool>
|
|
1283
|
+
<tool>{"op": "edit", "path": "public/index.html", "old": "exact old text", "new": "new text"}</tool>
|
|
1284
|
+
<tool>{"op": "write", "path": "public/css/main.css", "content": "body { ... }"}</tool>
|
|
1285
|
+
<tool>{"op": "check", "path": "server.js"}</tool>
|
|
1286
|
+
|
|
1287
|
+
Valid ops: read, edit, write, delete, list, search, run, check, sandbox
|
|
1288
|
+
|
|
1289
|
+
DO NOT write things like {"tool": "read_file", "args": {...}} — that format is for
|
|
1290
|
+
OpenAI-native providers, not for you. Use <tool>{"op": "read", "path": "..."}</tool>.
|
|
1291
|
+
|
|
1292
|
+
After ALL fixes are done, emit <done/> on its own line.
|
|
1293
|
+
`;
|
|
1294
|
+
conversationHistory[0].content = (typeof conversationHistory[0].content === 'string' ? conversationHistory[0].content : userContent) + '\n\n' + textBasedFormat;
|
|
1274
1295
|
// Minimal old-style loop with <tool> tags — simplified
|
|
1275
1296
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
1276
1297
|
if (isAborted()) break;
|
|
@@ -1294,31 +1315,25 @@ RULES:
|
|
|
1294
1315
|
// Check if agent signaled completion
|
|
1295
1316
|
const isDone = stepResponse.includes('<done/>') || stepResponse.includes('<done />');
|
|
1296
1317
|
|
|
1297
|
-
// Extract and execute ALL tool calls from this step
|
|
1298
|
-
//
|
|
1299
|
-
|
|
1300
|
-
|
|
1318
|
+
// Extract and execute ALL tool calls from this step.
|
|
1319
|
+
// Accept THREE wire formats (LLM providers vary wildly):
|
|
1320
|
+
// 1. NHA native: <tool>{"op":"read","path":"..."}</tool>
|
|
1321
|
+
// 2. OpenAI-style: {"tool":"read_file","args":{"file_path":"..."}}
|
|
1322
|
+
// 3. Anthropic-style: <tool>{"name":"read_file","input":{"path":"..."}}</tool>
|
|
1323
|
+
// Normalize all three into the internal {op, path, ...} shape before dispatch.
|
|
1324
|
+
const toolCalls = _extractAllToolCalls(stepResponse);
|
|
1301
1325
|
const toolResults = [];
|
|
1302
|
-
const matchedRanges =
|
|
1326
|
+
const matchedRanges = toolCalls.matchedRanges;
|
|
1303
1327
|
|
|
1304
|
-
|
|
1305
|
-
matchedRanges.push([match.index, match.index + match[0].length]);
|
|
1328
|
+
for (const rawCall of toolCalls.calls) {
|
|
1306
1329
|
let toolCall;
|
|
1307
1330
|
try {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
try {
|
|
1315
|
-
toolCall = _parseToolCallRobust(match[1].trim());
|
|
1316
|
-
} catch (parseErr2) {
|
|
1317
|
-
console.error('[TOOL-PARSE] JSON parse failed even after robust parse:', parseErr2.message, 'raw:', match[1].slice(0, 200));
|
|
1318
|
-
toolResults.push({ op: 'error', result: 'JSON parse failed' });
|
|
1319
|
-
emit({ type: 'tool', op: 'parse_error', path: '', result: 'json_parse_failed' });
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1331
|
+
toolCall = _normalizeToolCall(rawCall);
|
|
1332
|
+
} catch (parseErr) {
|
|
1333
|
+
console.error('[TOOL-PARSE] failed:', parseErr.message, 'raw:', JSON.stringify(rawCall).slice(0, 200));
|
|
1334
|
+
toolResults.push({ op: 'error', result: 'parse_failed' });
|
|
1335
|
+
emit({ type: 'tool', op: 'parse_error', path: '', result: 'parse_failed' });
|
|
1336
|
+
continue;
|
|
1322
1337
|
}
|
|
1323
1338
|
|
|
1324
1339
|
const { op, path: relPath, old: oldStr, new: newStr, content, newPath, query, glob: globPat, cmd } = toolCall;
|
|
@@ -3396,6 +3411,187 @@ function _safeName(name) {
|
|
|
3396
3411
|
* This extracts "op", "path", "old", "new", "content", "query", "cmd" etc. by finding
|
|
3397
3412
|
* the key-value boundaries manually.
|
|
3398
3413
|
*/
|
|
3414
|
+
// Map native tool names → internal NHA "op" codes. Both directions: the LLM
|
|
3415
|
+
// might emit either based on which provider's docs it was trained on.
|
|
3416
|
+
const _TOOL_NAME_TO_OP = new Map([
|
|
3417
|
+
['read_file', 'read'], ['read', 'read'],
|
|
3418
|
+
['edit_file', 'edit'], ['edit', 'edit'],
|
|
3419
|
+
['create_file', 'write'], ['write', 'write'], ['write_file', 'write'],
|
|
3420
|
+
['delete_file', 'delete'], ['delete', 'delete'],
|
|
3421
|
+
['list_files', 'list'], ['list', 'list'],
|
|
3422
|
+
['search_files', 'search'], ['search', 'search'],
|
|
3423
|
+
['run_command', 'run'], ['run', 'run'], ['shell', 'run'],
|
|
3424
|
+
['check_syntax', 'check'], ['check', 'check'],
|
|
3425
|
+
['restart_sandbox', 'sandbox'], ['sandbox', 'sandbox'],
|
|
3426
|
+
]);
|
|
3427
|
+
|
|
3428
|
+
// Map common param-name variants used across LLM providers.
|
|
3429
|
+
const _PARAM_ALIASES = {
|
|
3430
|
+
path: ['path', 'file_path', 'filepath', 'filename', 'file'],
|
|
3431
|
+
old: ['old', 'old_text', 'old_string', 'oldText'],
|
|
3432
|
+
new: ['new', 'new_text', 'new_string', 'newText', 'replacement'],
|
|
3433
|
+
content: ['content', 'text', 'body', 'file_content'],
|
|
3434
|
+
query: ['query', 'pattern', 'search', 'q'],
|
|
3435
|
+
glob: ['glob', 'pattern', 'file_pattern'],
|
|
3436
|
+
cmd: ['cmd', 'command', 'shell_command'],
|
|
3437
|
+
};
|
|
3438
|
+
|
|
3439
|
+
function _pickParam(obj, kind) {
|
|
3440
|
+
for (const alt of _PARAM_ALIASES[kind] || [kind]) {
|
|
3441
|
+
if (obj && Object.prototype.hasOwnProperty.call(obj, alt) && obj[alt] !== undefined) {
|
|
3442
|
+
return obj[alt];
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
return undefined;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
/**
|
|
3449
|
+
* Extract every tool-call-like JSON blob from an LLM response. Handles:
|
|
3450
|
+
* - <tool>{...}</tool> — NHA native wrapper
|
|
3451
|
+
* - {"tool": "...", "args": {...}} — OpenAI-style nude JSON
|
|
3452
|
+
* - {"name": "...", "input": {...}} — Anthropic-style nude JSON
|
|
3453
|
+
* - ```json\n{...}\n``` — markdown-fenced JSON blocks
|
|
3454
|
+
*
|
|
3455
|
+
* Returns { calls: [raw objects], matchedRanges: [[start,end], ...] } so the
|
|
3456
|
+
* caller can blank out the consumed regions from the visible text stream.
|
|
3457
|
+
*/
|
|
3458
|
+
export function _extractAllToolCalls(text) {
|
|
3459
|
+
const calls = [];
|
|
3460
|
+
const matchedRanges = [];
|
|
3461
|
+
|
|
3462
|
+
// Pass 1: <tool>...</tool> wrappers
|
|
3463
|
+
const wrapRe = /<tool>([\s\S]*?)<\/tool>/g;
|
|
3464
|
+
let m;
|
|
3465
|
+
while ((m = wrapRe.exec(text)) !== null) {
|
|
3466
|
+
matchedRanges.push([m.index, m.index + m[0].length]);
|
|
3467
|
+
try {
|
|
3468
|
+
let raw = m[1].trim().replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
|
|
3469
|
+
calls.push(JSON.parse(raw));
|
|
3470
|
+
} catch {
|
|
3471
|
+
try { calls.push(_parseToolCallRobust(m[1].trim())); } catch {}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// Pass 2: markdown-fenced JSON blocks ```json {...} ```
|
|
3476
|
+
const fenceRe = /```(?:json)?\s*\n([\s\S]*?)\n```/g;
|
|
3477
|
+
while ((m = fenceRe.exec(text)) !== null) {
|
|
3478
|
+
// Skip if already inside a <tool> match
|
|
3479
|
+
if (matchedRanges.some(([s, e]) => m.index >= s && m.index < e)) continue;
|
|
3480
|
+
const body = m[1].trim();
|
|
3481
|
+
if (!/^\s*\{/.test(body)) continue;
|
|
3482
|
+
try {
|
|
3483
|
+
const parsed = JSON.parse(body.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']'));
|
|
3484
|
+
if (_looksLikeToolCall(parsed)) {
|
|
3485
|
+
calls.push(parsed);
|
|
3486
|
+
matchedRanges.push([m.index, m.index + m[0].length]);
|
|
3487
|
+
}
|
|
3488
|
+
} catch {}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// Pass 3: bare JSON objects with balanced braces — scan top-level.
|
|
3492
|
+
// Find every '{' candidate not already consumed, then walk forward to find
|
|
3493
|
+
// the matching '}' while respecting nested braces AND string literals.
|
|
3494
|
+
for (let i = 0; i < text.length; i++) {
|
|
3495
|
+
if (text[i] !== '{') continue;
|
|
3496
|
+
if (matchedRanges.some(([s, e]) => i >= s && i < e)) continue;
|
|
3497
|
+
// Must be at line start or after whitespace (avoid inline {expr}/object literals in code)
|
|
3498
|
+
let prev = i - 1;
|
|
3499
|
+
while (prev >= 0 && (text[prev] === ' ' || text[prev] === '\t')) prev--;
|
|
3500
|
+
if (prev >= 0 && text[prev] !== '\n') continue;
|
|
3501
|
+
|
|
3502
|
+
const end = _findMatchingBrace(text, i);
|
|
3503
|
+
if (end < 0) continue;
|
|
3504
|
+
const candidate = text.slice(i, end + 1);
|
|
3505
|
+
try {
|
|
3506
|
+
const parsed = JSON.parse(candidate.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']'));
|
|
3507
|
+
if (_looksLikeToolCall(parsed)) {
|
|
3508
|
+
calls.push(parsed);
|
|
3509
|
+
matchedRanges.push([i, end + 1]);
|
|
3510
|
+
i = end;
|
|
3511
|
+
}
|
|
3512
|
+
} catch {}
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
return { calls, matchedRanges };
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
// Walk forward from openIdx (which must be '{') to find the matching '}'.
|
|
3519
|
+
// Respects nested braces, string literals (single & double quote), and escapes.
|
|
3520
|
+
// Returns -1 if no match found (unclosed).
|
|
3521
|
+
function _findMatchingBrace(text, openIdx) {
|
|
3522
|
+
let depth = 0;
|
|
3523
|
+
let inStr = false;
|
|
3524
|
+
let strCh = '';
|
|
3525
|
+
for (let i = openIdx; i < text.length; i++) {
|
|
3526
|
+
const c = text[i];
|
|
3527
|
+
if (inStr) {
|
|
3528
|
+
if (c === '\\') { i++; continue; }
|
|
3529
|
+
if (c === strCh) inStr = false;
|
|
3530
|
+
continue;
|
|
3531
|
+
}
|
|
3532
|
+
if (c === '"' || c === "'") { inStr = true; strCh = c; continue; }
|
|
3533
|
+
if (c === '{') depth++;
|
|
3534
|
+
else if (c === '}') {
|
|
3535
|
+
depth--;
|
|
3536
|
+
if (depth === 0) return i;
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return -1;
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
function _looksLikeToolCall(obj) {
|
|
3543
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
3544
|
+
return (
|
|
3545
|
+
'op' in obj ||
|
|
3546
|
+
'tool' in obj ||
|
|
3547
|
+
'name' in obj && ('input' in obj || 'arguments' in obj || 'args' in obj) ||
|
|
3548
|
+
'function_call' in obj
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
/**
|
|
3553
|
+
* Normalize a parsed tool-call object to NHA's internal shape:
|
|
3554
|
+
* { op, path, old, new, content, newPath, query, glob, cmd }
|
|
3555
|
+
*
|
|
3556
|
+
* Accepts:
|
|
3557
|
+
* - {op, path, ...} — already normalized
|
|
3558
|
+
* - {tool, args: {file_path, old_text, ...}} — OpenAI-style
|
|
3559
|
+
* - {name, input: {...}} / {name, arguments} — Anthropic-style
|
|
3560
|
+
*/
|
|
3561
|
+
export function _normalizeToolCall(raw) {
|
|
3562
|
+
if (!raw || typeof raw !== 'object') throw new Error('not an object');
|
|
3563
|
+
|
|
3564
|
+
// Already in NHA native shape
|
|
3565
|
+
if ('op' in raw && typeof raw.op === 'string') {
|
|
3566
|
+
return raw;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// Extract tool name from various wire formats
|
|
3570
|
+
const toolName = raw.tool || raw.name || raw.function_call?.name;
|
|
3571
|
+
if (!toolName) throw new Error('no tool/op/name field');
|
|
3572
|
+
|
|
3573
|
+
const op = _TOOL_NAME_TO_OP.get(toolName);
|
|
3574
|
+
if (!op) throw new Error(`unknown tool name: ${toolName}`);
|
|
3575
|
+
|
|
3576
|
+
// Extract args payload (OpenAI uses "args", Anthropic uses "input" or "arguments")
|
|
3577
|
+
let args = raw.args || raw.input || raw.arguments || raw.parameters || {};
|
|
3578
|
+
if (typeof args === 'string') {
|
|
3579
|
+
try { args = JSON.parse(args); } catch { args = {}; }
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
return {
|
|
3583
|
+
op,
|
|
3584
|
+
path: _pickParam(args, 'path'),
|
|
3585
|
+
old: _pickParam(args, 'old'),
|
|
3586
|
+
new: _pickParam(args, 'new'),
|
|
3587
|
+
content: _pickParam(args, 'content'),
|
|
3588
|
+
query: _pickParam(args, 'query'),
|
|
3589
|
+
glob: _pickParam(args, 'glob'),
|
|
3590
|
+
cmd: _pickParam(args, 'cmd'),
|
|
3591
|
+
newPath: args.newPath || args.new_path,
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3399
3595
|
function _parseToolCallRobust(raw) {
|
|
3400
3596
|
const result = {};
|
|
3401
3597
|
|