nothumanallowed 14.1.81 → 14.2.1
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 +275 -125
- package/src/ui-dist/NHALogo.jpeg +0 -0
- package/src/ui-dist/apple-touch-icon.png +0 -0
- package/src/ui-dist/assets/{index-UUUprsdb.css → index-DJhcU-P3.css} +1 -1
- package/src/ui-dist/assets/index-DtqU6mO_.js +706 -0
- package/src/ui-dist/favicon-16x16.png +0 -0
- package/src/ui-dist/favicon-32x32.png +0 -0
- package/src/ui-dist/favicon.ico +0 -0
- package/src/ui-dist/favicon.svg +4 -6
- package/src/ui-dist/icon-192x192.png +0 -0
- package/src/ui-dist/index.html +2 -2
- package/src/ui-dist/assets/index-BPc0ZESP.js +0 -701
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "14.1
|
|
3
|
+
"version": "14.2.1",
|
|
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": {
|
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 = '14.1
|
|
8
|
+
export const VERSION = '14.2.1';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -475,168 +475,312 @@ const ChatStore = {
|
|
|
475
475
|
* Uses structured SSE events: { type: 'text', token } | { type: 'tool', ... } | { type: 'done', changed }
|
|
476
476
|
*/
|
|
477
477
|
async function runWebCraftAgent(config, projectName, message, attachments, emit) {
|
|
478
|
+
const MAX_STEPS = 8; // max agentic loop iterations
|
|
478
479
|
const dir = ProjectStore.dir(projectName);
|
|
479
480
|
if (!fs.existsSync(dir)) { emit({ type: 'error', msg: 'Project not found' }); return; }
|
|
480
481
|
|
|
481
|
-
// Ensure context files always exist
|
|
482
482
|
SkillStore.ensureDefaults(projectName, config);
|
|
483
483
|
|
|
484
|
-
const files = _listProjectFiles(dir);
|
|
485
|
-
const skillCtx = SkillStore.context(projectName);
|
|
486
|
-
|
|
487
|
-
const fileIndex = files.map((f) => `- ${f}`).join('\n');
|
|
488
484
|
const today = new Date().toISOString().slice(0, 10);
|
|
489
485
|
const LANG_MAP = { en:'English',it:'Italian',es:'Spanish',fr:'French',de:'German',pt:'Portuguese' };
|
|
490
486
|
const language = LANG_MAP[(config?.language||'it').slice(0,2)] || 'Italian';
|
|
491
487
|
|
|
492
|
-
// Build context: include files mentioned in the message + key project files
|
|
493
|
-
const mentionedFiles = files.filter((f) => message.toLowerCase().includes(f.toLowerCase().split('/').pop() ?? ''));
|
|
494
|
-
// Always include key structural files
|
|
495
|
-
const keyFiles = files.filter((f) => /^(server|app|index)\.(js|mjs|ts)$/.test(f) || f === 'package.json' || f.includes('routes/index'));
|
|
496
|
-
const contextFiles = [...new Set([...mentionedFiles, ...keyFiles, ...files.slice(0, 12)])].slice(0, 15);
|
|
497
|
-
const fileContents = contextFiles.map((rel) => {
|
|
498
|
-
try {
|
|
499
|
-
const content = fs.readFileSync(path.join(dir, rel), 'utf-8');
|
|
500
|
-
return `### FILE: ${rel}\n\`\`\`\n${content.slice(0, 8000)}\n\`\`\``;
|
|
501
|
-
} catch { return ''; }
|
|
502
|
-
}).filter(Boolean).join('\n\n');
|
|
503
|
-
|
|
504
488
|
const toolSpec = `
|
|
505
|
-
AVAILABLE TOOLS (use
|
|
489
|
+
AVAILABLE TOOLS (use exactly ONE tool per <tool> tag):
|
|
490
|
+
|
|
491
|
+
1. Read a file (to see its current content before editing):
|
|
492
|
+
<tool>{"op":"read","path":"relative/path.js"}</tool>
|
|
506
493
|
|
|
507
|
-
|
|
508
|
-
<tool>{"op":"
|
|
494
|
+
2. Edit a file (surgical replacement of exact code):
|
|
495
|
+
<tool>{"op":"edit","path":"relative/path.js","old":"EXACT_EXISTING_CODE","new":"REPLACEMENT_CODE"}</tool>
|
|
509
496
|
|
|
510
|
-
|
|
511
|
-
<tool>{"op":"
|
|
497
|
+
3. Write/create a file (complete content):
|
|
498
|
+
<tool>{"op":"write","path":"relative/path.js","content":"FULL_FILE_CONTENT"}</tool>
|
|
512
499
|
|
|
513
|
-
|
|
514
|
-
<tool>{"op":"
|
|
500
|
+
4. Check syntax of a JS file:
|
|
501
|
+
<tool>{"op":"check","path":"relative/path.js"}</tool>
|
|
502
|
+
|
|
503
|
+
WORKFLOW — follow this for every change:
|
|
504
|
+
1. Read the file(s) you need to modify
|
|
505
|
+
2. Make your changes with edit or write
|
|
506
|
+
3. Use check to verify JS files have no syntax errors
|
|
507
|
+
4. If check fails, read the file again and fix
|
|
508
|
+
5. When ALL changes are complete and verified, output: <done/>
|
|
515
509
|
|
|
516
510
|
RULES:
|
|
517
|
-
-
|
|
518
|
-
- Use
|
|
519
|
-
-
|
|
520
|
-
-
|
|
521
|
-
-
|
|
511
|
+
- "old" must be an EXACT verbatim copy from the file — copy-paste, no paraphrasing
|
|
512
|
+
- Use edit for targeted changes, write for new files or complete rewrites
|
|
513
|
+
- ALWAYS read before edit if you haven't seen the file content yet
|
|
514
|
+
- ALWAYS check JS files after modifications
|
|
515
|
+
- Output <done/> when you are completely finished — this is MANDATORY
|
|
522
516
|
`;
|
|
523
517
|
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
|
|
518
|
+
// Build system prompt with fresh file list each step
|
|
519
|
+
function buildSystemPrompt() {
|
|
520
|
+
const files = _listProjectFiles(dir);
|
|
521
|
+
const fileIndex = files.map((f) => `- ${f}`).join('\n');
|
|
522
|
+
const skillCtx = SkillStore.context(projectName);
|
|
523
|
+
const ctxDir = SkillStore.dir(projectName);
|
|
524
|
+
let skillContext = '';
|
|
525
|
+
try {
|
|
526
|
+
for (const name of ['memory.md', 'skills.md', `${config.llm?.provider || 'nha'}.md`]) {
|
|
527
|
+
const p = path.join(ctxDir, name);
|
|
528
|
+
if (fs.existsSync(p)) skillContext += `\n### ${name}:\n${fs.readFileSync(p, 'utf-8')}\n`;
|
|
529
|
+
}
|
|
530
|
+
} catch {}
|
|
531
531
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
532
|
+
// Key files: load full content for server, package.json, index
|
|
533
|
+
const keyFiles = files.filter((f) =>
|
|
534
|
+
/^(server|app|index)\.(js|mjs|ts)$/.test(f) || f === 'package.json' || f.includes('routes/index')
|
|
535
|
+
);
|
|
536
|
+
const mentionedFiles = files.filter((f) =>
|
|
537
|
+
message.toLowerCase().includes((f.split('/').pop() || '').toLowerCase())
|
|
538
|
+
);
|
|
539
|
+
const contextFiles = [...new Set([...mentionedFiles, ...keyFiles])].slice(0, 10);
|
|
540
|
+
const fileContents = contextFiles.map((rel) => {
|
|
541
|
+
try {
|
|
542
|
+
const content = fs.readFileSync(path.join(dir, rel), 'utf-8');
|
|
543
|
+
return `### FILE: ${rel}\n\`\`\`\n${content}\n\`\`\``;
|
|
544
|
+
} catch { return ''; }
|
|
545
|
+
}).filter(Boolean).join('\n\n');
|
|
546
|
+
|
|
547
|
+
return [
|
|
548
|
+
`You are WebCraft Agent — an elite AI coding assistant. Today: ${today}. Language: ${language}.`,
|
|
549
|
+
`\nYou control the project IDE. You MUST use tools to implement changes — never just explain.`,
|
|
550
|
+
`\nYour workflow: read → plan → edit/write → check → verify → done.`,
|
|
551
|
+
`\nAfter EVERY tool use, you will receive the result. Based on the result, decide what to do next.`,
|
|
552
|
+
`\nWhen finished, output <done/> to signal completion.`,
|
|
553
|
+
`\n\n## PROJECT: ${projectName}`,
|
|
554
|
+
`\n## FILE TREE:\n${fileIndex}`,
|
|
555
|
+
skillContext,
|
|
556
|
+
skillCtx ? `\n\n## PROJECT KNOWLEDGE:\n${skillCtx}` : '',
|
|
557
|
+
attachments?.length ? `\n\n## ATTACHMENTS: ${attachments.map((a) => a.name).join(', ')}` : '',
|
|
558
|
+
fileContents ? `\n\n## LOADED FILES:\n${fileContents}` : '',
|
|
559
|
+
`\n\n${toolSpec}`,
|
|
560
|
+
].join('');
|
|
561
|
+
}
|
|
545
562
|
|
|
546
|
-
|
|
547
|
-
`You are WebCraft Agent — a team of 200 senior developers working as one entity. Today is ${today}. Respond in ${language}.`,
|
|
548
|
-
`\nYou have FULL control of the project IDE. You read files, edit them surgically, and write new ones.`,
|
|
549
|
-
`\nYour edits MUST be enterprise-grade: security, error handling, responsive design, accessibility.`,
|
|
550
|
-
`\nWhen the user asks for changes, you MUST use tools to implement them — never just explain. ACT, don't talk.`,
|
|
551
|
-
`\nAfter each tool use, briefly explain what changed and why.`,
|
|
552
|
-
`\n\n## PROJECT: ${projectName}`,
|
|
553
|
-
`\n## FILES:\n${fileIndex}`,
|
|
554
|
-
skillContext,
|
|
555
|
-
skillCtx ? `\n\n## ADDITIONAL CONTEXT:\n${skillCtx}` : '',
|
|
556
|
-
attachments?.length ? `\n\n## ATTACHMENTS: ${attachments.map((a) => a.name).join(', ')}` : '',
|
|
557
|
-
`\n\n## CURRENT FILE CONTENTS:\n${fileContents}`,
|
|
558
|
-
`\n\n${toolSpec}`,
|
|
559
|
-
].join('');
|
|
560
|
-
|
|
561
|
-
// Prepare user content (text + images if any)
|
|
563
|
+
// Prepare user content
|
|
562
564
|
const userContent = attachments?.length
|
|
563
565
|
? _buildMultimodalContent(message, attachments)
|
|
564
566
|
: message;
|
|
565
567
|
|
|
566
|
-
|
|
568
|
+
// ── Agentic loop ───────────────────────────────────────────────────────────
|
|
567
569
|
let hasChanges = false;
|
|
570
|
+
const modifiedFiles = new Set();
|
|
571
|
+
let conversationHistory = [
|
|
572
|
+
{ role: 'user', content: userContent },
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
for (let step = 0; step < MAX_STEPS; step++) {
|
|
576
|
+
const systemPrompt = buildSystemPrompt();
|
|
577
|
+
emit({ type: 'step', step: step + 1, max: MAX_STEPS });
|
|
578
|
+
|
|
579
|
+
// Build user message from conversation history
|
|
580
|
+
// callLLMStream takes a single string, so we concatenate the conversation
|
|
581
|
+
let stepResponse = '';
|
|
582
|
+
const userMsg = conversationHistory.map((m) =>
|
|
583
|
+
m.role === 'user' ? m.content : `[ASSISTANT RESPONSE]\n${m.content.slice(0, 6000)}`
|
|
584
|
+
).join('\n\n---\n\n');
|
|
585
|
+
|
|
586
|
+
await callLLMStream(config, systemPrompt, userMsg, (token) => {
|
|
587
|
+
stepResponse += token;
|
|
588
|
+
// Stream visible text (suppress tool tags)
|
|
589
|
+
const visible = token.replace(/<tool>[\s\S]*?<\/tool>/g, '').replace(/<done\s*\/>/g, '');
|
|
590
|
+
if (visible) emit({ type: 'text', token: visible });
|
|
591
|
+
}, { max_tokens: 16384 });
|
|
592
|
+
|
|
593
|
+
// Check if agent signaled completion
|
|
594
|
+
const isDone = stepResponse.includes('<done/>') || stepResponse.includes('<done />');
|
|
595
|
+
|
|
596
|
+
// Extract and execute ALL tool calls from this step
|
|
597
|
+
const toolRegex = /<tool>([\s\S]*?)<\/tool>/g;
|
|
598
|
+
let match;
|
|
599
|
+
const toolResults = [];
|
|
600
|
+
|
|
601
|
+
while ((match = toolRegex.exec(stepResponse)) !== null) {
|
|
602
|
+
let toolCall;
|
|
603
|
+
try {
|
|
604
|
+
// Fix common JSON issues from LLM
|
|
605
|
+
let raw = match[1].trim();
|
|
606
|
+
raw = raw.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
|
|
607
|
+
toolCall = JSON.parse(raw);
|
|
608
|
+
} catch {
|
|
609
|
+
toolResults.push({ op: 'error', result: 'JSON parse failed' });
|
|
610
|
+
emit({ type: 'tool', op: 'parse_error', path: '', result: 'json_parse_failed' });
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
568
613
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// ── Execute all tool calls found in the response ───────────────────────────
|
|
577
|
-
const toolRegex = /<tool>([\s\S]*?)<\/tool>/g;
|
|
578
|
-
let match;
|
|
579
|
-
while ((match = toolRegex.exec(fullResponse)) !== null) {
|
|
580
|
-
let toolCall;
|
|
581
|
-
try { toolCall = JSON.parse(match[1].trim()); } catch {
|
|
582
|
-
emit({ type: 'tool', op: 'parse_error', path: '', result: 'json_parse_failed' });
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const { op, path: relPath, old: oldStr, new: newStr, content } = toolCall;
|
|
587
|
-
if (!relPath || !_isSafePath(relPath)) {
|
|
588
|
-
emit({ type: 'tool', op, path: relPath ?? '', result: 'unsafe_path' });
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (op === 'read') {
|
|
593
|
-
const src = ProjectStore.readFile(projectName, relPath);
|
|
594
|
-
emit({ type: 'tool', op: 'read', path: relPath, result: src !== null ? 'ok' : 'not_found' });
|
|
614
|
+
const { op, path: relPath, old: oldStr, new: newStr, content } = toolCall;
|
|
615
|
+
if (!relPath || !_isSafePath(relPath)) {
|
|
616
|
+
toolResults.push({ op, path: relPath, result: 'unsafe_path' });
|
|
617
|
+
emit({ type: 'tool', op, path: relPath ?? '', result: 'unsafe_path' });
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
595
620
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
621
|
+
if (op === 'read') {
|
|
622
|
+
const src = ProjectStore.readFile(projectName, relPath);
|
|
623
|
+
const result = src !== null ? 'ok' : 'not_found';
|
|
624
|
+
toolResults.push({ op: 'read', path: relPath, result, content: src?.slice(0, 12000) });
|
|
625
|
+
emit({ type: 'tool', op: 'read', path: relPath, result });
|
|
626
|
+
|
|
627
|
+
} else if (op === 'check') {
|
|
628
|
+
const src = ProjectStore.readFile(projectName, relPath);
|
|
629
|
+
let checkResult = 'ok';
|
|
630
|
+
if (!src) { checkResult = 'file_not_found'; }
|
|
631
|
+
else if (relPath.endsWith('.js') || relPath.endsWith('.mjs')) {
|
|
632
|
+
try { new Function(src); } catch (e) { checkResult = `syntax_error: ${e.message.replace(/\n.*/s, '')}`; }
|
|
633
|
+
} else if (relPath.endsWith('.json')) {
|
|
634
|
+
try { JSON.parse(src); } catch (e) { checkResult = `json_error: ${e.message}`; }
|
|
635
|
+
} else if (relPath.endsWith('.css')) {
|
|
636
|
+
// Basic CSS validation: balanced braces
|
|
637
|
+
const opens = (src.match(/\{/g) || []).length;
|
|
638
|
+
const closes = (src.match(/\}/g) || []).length;
|
|
639
|
+
if (opens !== closes) checkResult = `css_error: unbalanced braces (${opens} open, ${closes} close)`;
|
|
640
|
+
} else if (relPath.endsWith('.html')) {
|
|
641
|
+
checkResult = src.includes('</html>') ? 'ok' : 'missing_closing_html_tag';
|
|
642
|
+
}
|
|
643
|
+
toolResults.push({ op: 'check', path: relPath, result: checkResult });
|
|
644
|
+
emit({ type: 'tool', op: 'check', path: relPath, result: checkResult });
|
|
645
|
+
|
|
646
|
+
} else if (op === 'edit') {
|
|
647
|
+
const src = ProjectStore.readFile(projectName, relPath);
|
|
648
|
+
if (src === null) {
|
|
649
|
+
toolResults.push({ op: 'edit', path: relPath, result: 'file_not_found' });
|
|
650
|
+
emit({ type: 'tool', op: 'edit', path: relPath, result: 'file_not_found' });
|
|
651
|
+
} else if (!src.includes(oldStr)) {
|
|
652
|
+
const repaired = await _attemptEditRepair(config, relPath, src, oldStr, newStr);
|
|
653
|
+
if (repaired) {
|
|
654
|
+
ProjectStore.writeFile(projectName, relPath, repaired);
|
|
655
|
+
hasChanges = true;
|
|
656
|
+
modifiedFiles.add(relPath);
|
|
657
|
+
toolResults.push({ op: 'edit', path: relPath, result: 'ok_repaired' });
|
|
658
|
+
emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok_repaired', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr?.slice(0, 300) });
|
|
659
|
+
} else {
|
|
660
|
+
toolResults.push({ op: 'edit', path: relPath, result: 'old_not_found', hint: 'Use read to see current content, then retry with exact text' });
|
|
661
|
+
emit({ type: 'tool', op: 'edit', path: relPath, result: 'old_not_found', oldSnippet: oldStr.slice(0, 200) });
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
const newSrc = src.replace(oldStr, newStr ?? '');
|
|
665
|
+
ProjectStore.writeFile(projectName, relPath, newSrc);
|
|
605
666
|
hasChanges = true;
|
|
606
|
-
|
|
667
|
+
modifiedFiles.add(relPath);
|
|
668
|
+
toolResults.push({ op: 'edit', path: relPath, result: 'ok' });
|
|
669
|
+
emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr?.slice(0, 300) ?? '' });
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
} else if (op === 'write') {
|
|
673
|
+
if (content === undefined) {
|
|
674
|
+
toolResults.push({ op: 'write', path: relPath, result: 'missing_content' });
|
|
675
|
+
emit({ type: 'tool', op: 'write', path: relPath, result: 'missing_content' });
|
|
607
676
|
} else {
|
|
608
|
-
|
|
677
|
+
ProjectStore.writeFile(projectName, relPath, content);
|
|
678
|
+
hasChanges = true;
|
|
679
|
+
modifiedFiles.add(relPath);
|
|
680
|
+
toolResults.push({ op: 'write', path: relPath, result: 'ok' });
|
|
681
|
+
emit({ type: 'tool', op: 'write', path: relPath, result: 'ok' });
|
|
609
682
|
}
|
|
610
|
-
} else {
|
|
611
|
-
const newSrc = src.replace(oldStr, newStr ?? '');
|
|
612
|
-
ProjectStore.writeFile(projectName, relPath, newSrc);
|
|
613
|
-
hasChanges = true;
|
|
614
|
-
emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr?.slice(0, 300) ?? '' });
|
|
615
683
|
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// If agent said done or no tools were called, break
|
|
687
|
+
if (isDone || toolResults.length === 0) break;
|
|
688
|
+
|
|
689
|
+
// Build tool results feedback for next iteration
|
|
690
|
+
const feedbackParts = toolResults.map((r) => {
|
|
691
|
+
let msg = `[${r.op}] ${r.path || ''}: ${r.result}`;
|
|
692
|
+
if (r.op === 'read' && r.content) msg += `\n\`\`\`\n${r.content}\n\`\`\``;
|
|
693
|
+
if (r.hint) msg += ` — ${r.hint}`;
|
|
694
|
+
return msg;
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Add assistant response + tool results to conversation
|
|
698
|
+
conversationHistory.push({ role: 'assistant', content: stepResponse });
|
|
699
|
+
conversationHistory.push({ role: 'user', content: `TOOL RESULTS:\n${feedbackParts.join('\n\n')}\n\nContinue your work. When done, output <done/>` });
|
|
616
700
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
701
|
+
// Trim conversation to avoid context overflow (keep first user msg + last 4 exchanges)
|
|
702
|
+
if (conversationHistory.length > 10) {
|
|
703
|
+
conversationHistory = [conversationHistory[0], ...conversationHistory.slice(-6)];
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ── Post-edit: syntax check all modified JS files ──────────────────────────
|
|
708
|
+
const syntaxErrors = [];
|
|
709
|
+
for (const relPath of modifiedFiles) {
|
|
710
|
+
if (relPath.endsWith('.js') || relPath.endsWith('.mjs')) {
|
|
711
|
+
const src = ProjectStore.readFile(projectName, relPath);
|
|
712
|
+
if (src) {
|
|
713
|
+
try { new Function(src); } catch (e) {
|
|
714
|
+
syntaxErrors.push({ file: relPath, error: e.message.replace(/\n.*/s, '') });
|
|
715
|
+
}
|
|
624
716
|
}
|
|
625
717
|
}
|
|
626
718
|
}
|
|
719
|
+
if (syntaxErrors.length > 0) {
|
|
720
|
+
emit({ type: 'syntax_errors', errors: syntaxErrors });
|
|
721
|
+
}
|
|
627
722
|
|
|
628
|
-
// Log chat interaction
|
|
723
|
+
// Log chat interaction
|
|
629
724
|
if (hasChanges) {
|
|
630
725
|
try {
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
fs.writeFileSync(logFile, (fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf-8') : '') + logEntry, 'utf-8');
|
|
726
|
+
const logFile = path.join(SkillStore.dir(projectName), 'changes.log.md');
|
|
727
|
+
const ts = new Date().toISOString().slice(0, 16).replace('T', ' ');
|
|
728
|
+
const entry = `\n## ${ts} — Chat modification\n- User: ${message.slice(0, 100)}${message.length > 100 ? '...' : ''}\n- Files: ${[...modifiedFiles].join(', ')}\n`;
|
|
729
|
+
fs.writeFileSync(logFile, (fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf-8') : '') + entry, 'utf-8');
|
|
636
730
|
} catch {}
|
|
637
731
|
}
|
|
638
732
|
|
|
639
|
-
|
|
733
|
+
// Notify about modified files so frontend can reload them
|
|
734
|
+
emit({ type: 'files_changed', files: [...modifiedFiles] });
|
|
735
|
+
|
|
736
|
+
// ── Auto-restart sandbox if running and files changed ──────────────────────
|
|
737
|
+
if (hasChanges && sandbox.isRunning()) {
|
|
738
|
+
emit({ type: 'sandbox_restart', msg: 'Restarting sandbox to verify changes...' });
|
|
739
|
+
try {
|
|
740
|
+
await sandbox.stop();
|
|
741
|
+
const projectDir = ProjectStore.dir(projectName);
|
|
742
|
+
// Quick restart — capture stderr for 5s to detect crash
|
|
743
|
+
const port = await _findFreePort(4000, 4999);
|
|
744
|
+
if (port) {
|
|
745
|
+
const shimDir = path.join(projectDir, '.nha-shims');
|
|
746
|
+
ensureDir(shimDir);
|
|
747
|
+
_writeShims(shimDir);
|
|
748
|
+
const entryFile = _detectEntry(projectDir);
|
|
749
|
+
if (entryFile) {
|
|
750
|
+
const patchedEntry = _patchEntry(projectDir, entryFile, shimDir, port);
|
|
751
|
+
const proc = spawn('node', [patchedEntry], {
|
|
752
|
+
cwd: projectDir,
|
|
753
|
+
env: { ...process.env, PORT: String(port), NODE_ENV: 'development', NHA_SANDBOX: '1' },
|
|
754
|
+
detached: false,
|
|
755
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
756
|
+
});
|
|
757
|
+
sandbox._sandbox = { proc, port, projectName, startedAt: new Date(), healthy: false };
|
|
758
|
+
|
|
759
|
+
let crashErr = '';
|
|
760
|
+
proc.stderr.on('data', (d) => { crashErr += d.toString(); });
|
|
761
|
+
proc.stdout.on('data', (d) => {
|
|
762
|
+
if (/listen|running|started|ready|port/i.test(d.toString())) {
|
|
763
|
+
sandbox._sandbox.healthy = true;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const healthy = await _waitForPort(port, 8000);
|
|
768
|
+
if (healthy) {
|
|
769
|
+
sandbox._sandbox.healthy = true;
|
|
770
|
+
emit({ type: 'sandbox_ready', port });
|
|
771
|
+
} else if (crashErr) {
|
|
772
|
+
// Extract error for the user
|
|
773
|
+
const errLine = crashErr.split('\n').find((l) => l.includes('Error')) || crashErr.slice(0, 200);
|
|
774
|
+
emit({ type: 'sandbox_error', msg: errLine });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} catch (e) {
|
|
779
|
+
emit({ type: 'sandbox_error', msg: e.message?.slice(0, 200) });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
emit({ type: 'done', changed: hasChanges, syntaxErrors: syntaxErrors.length });
|
|
640
784
|
}
|
|
641
785
|
|
|
642
786
|
// ── Generation pipeline (SSE) ─────────────────────────────────────────────────
|
|
@@ -811,11 +955,12 @@ Design a COMPLETE production-ready file structure. Include ALL files needed for
|
|
|
811
955
|
const fileSpec = filePlan[fi];
|
|
812
956
|
emit({ type: 'file_start', name: fileSpec.name, fi: fi + 1, total: filePlan.length });
|
|
813
957
|
|
|
814
|
-
// Include last
|
|
815
|
-
const prevContext = generatedFiles.slice(-
|
|
958
|
+
// Include last 8 generated files — key files get full content
|
|
959
|
+
const prevContext = generatedFiles.slice(-8)
|
|
816
960
|
.map((f) => {
|
|
817
961
|
const ext = f.name.split('.').pop();
|
|
818
|
-
const
|
|
962
|
+
const isKey = /^(server|app|index)\.(js|mjs)$/.test(f.name) || f.name === 'package.json';
|
|
963
|
+
const maxSnippet = isKey ? 16000 : ext === 'json' ? 1200 : ext === 'css' ? 4000 : ext === 'html' ? 4000 : 3000;
|
|
819
964
|
const snippet = f.content.slice(0, maxSnippet);
|
|
820
965
|
return `### ${f.name}\n\`\`\`\n${snippet}${f.content.length > maxSnippet ? '\n... (truncated)' : ''}\n\`\`\``;
|
|
821
966
|
})
|
|
@@ -956,6 +1101,11 @@ Continue from here:`;
|
|
|
956
1101
|
if (f.name.endsWith('.json')) {
|
|
957
1102
|
try { JSON.parse(f.content); } catch { return true; }
|
|
958
1103
|
}
|
|
1104
|
+
if (f.name.endsWith('.css')) {
|
|
1105
|
+
const opens = (f.content.match(/\{/g) || []).length;
|
|
1106
|
+
const closes = (f.content.match(/\}/g) || []).length;
|
|
1107
|
+
if (opens !== closes) return true;
|
|
1108
|
+
}
|
|
959
1109
|
return isFileTruncated(f.content, f.name);
|
|
960
1110
|
});
|
|
961
1111
|
|
package/src/ui-dist/NHALogo.jpeg
CHANGED
|
Binary file
|
|
Binary file
|