nothumanallowed 13.5.54 → 13.5.56

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.5.54",
3
+ "version": "13.5.56",
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": {
@@ -3913,11 +3913,99 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
3913
3913
  // Write meta
3914
3914
  const meta = { projectName: projName, description: body.description || '', files: (body.files || []).map(f => f.name), createdAt: new Date().toISOString() };
3915
3915
  fs.writeFileSync(path.join(projDir, 'webcraft-meta.json'), JSON.stringify(meta, null, 2), 'utf8');
3916
+ // Write agent memory file — always included as first context for WebCraft Agent
3917
+ const agentMd = [
3918
+ '# WebCraft Agent Memory — ' + projName,
3919
+ '> Auto-generated. Edit to give the agent persistent context about this project.',
3920
+ '',
3921
+ '## Progetto',
3922
+ '- **Nome**: ' + projName,
3923
+ '- **Descrizione**: ' + (body.description || ''),
3924
+ '- **Creato**: ' + new Date().toISOString(),
3925
+ '',
3926
+ '## Stack',
3927
+ '- Server: Express.js (Node.js)',
3928
+ '- Auth: JWT (access 15min, refresh 7d httpOnly cookie) + bcryptjs cost 12',
3929
+ '- DB: PostgreSQL (sandbox: in-memory shim in server/db.js)',
3930
+ '- CSS: BEM methodology',
3931
+ '- Security: helmet, express-rate-limit, custom sentinel WAF middleware',
3932
+ '',
3933
+ '## File principali',
3934
+ (body.files || []).map(f => '- ' + f.name).join('\n'),
3935
+ '',
3936
+ '## Note agente',
3937
+ '(aggiungi qui note per sessioni future — es. "usa Tailwind invece di BEM", "API key in .env.local")',
3938
+ ].join('\n');
3939
+ fs.writeFileSync(path.join(projDir, 'webcraft-agent.md'), agentMd, 'utf8');
3916
3940
  sendJSON(res, 200, { ok: true, dir: projDir });
3917
3941
  logRequest(method, pathname, 200, Date.now() - start);
3918
3942
  return;
3919
3943
  }
3920
3944
 
3945
+ // POST /api/studio/webcraft/projects/chat/save { projectName, chat[] }
3946
+ if (pathname === '/api/studio/webcraft/projects/chat/save' && method === 'POST') {
3947
+ const body = await parseBody(req, 2 * 1024 * 1024);
3948
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '-');
3949
+ if (!projName) { sendJSON(res, 400, { error: 'projectName required' }); return; }
3950
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
3951
+ fs.mkdirSync(projDir, { recursive: true });
3952
+ fs.writeFileSync(path.join(projDir, 'webcraft-chat.json'), JSON.stringify(body.chat || [], null, 2), 'utf8');
3953
+ sendJSON(res, 200, { ok: true });
3954
+ logRequest(method, pathname, 200, Date.now() - start);
3955
+ return;
3956
+ }
3957
+
3958
+ // GET /api/studio/webcraft/projects/chat/load/:name
3959
+ if (pathname.startsWith('/api/studio/webcraft/projects/chat/load/') && method === 'GET') {
3960
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/projects/chat/load/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3961
+ const chatPath = path.join(os.homedir(), '.nha', 'webcraft', projName, 'webcraft-chat.json');
3962
+ if (!fs.existsSync(chatPath)) { sendJSON(res, 200, { chat: [] }); return; }
3963
+ const chat = JSON.parse(fs.readFileSync(chatPath, 'utf8'));
3964
+ sendJSON(res, 200, { chat });
3965
+ logRequest(method, pathname, 200, Date.now() - start);
3966
+ return;
3967
+ }
3968
+
3969
+ // GET /api/studio/webcraft/skills/:name → { skills: [{name, content}] }
3970
+ if (pathname.startsWith('/api/studio/webcraft/skills/') && method === 'GET') {
3971
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/skills/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3972
+ const skillsDir = path.join(os.homedir(), '.nha', 'webcraft', projName, 'skills');
3973
+ const skills = [];
3974
+ if (fs.existsSync(skillsDir)) {
3975
+ for (const fname of fs.readdirSync(skillsDir)) {
3976
+ if (!fname.endsWith('.md')) continue;
3977
+ try {
3978
+ const content = fs.readFileSync(path.join(skillsDir, fname), 'utf8');
3979
+ skills.push({ name: fname.replace(/\.md$/, ''), content });
3980
+ } catch(_) {}
3981
+ }
3982
+ }
3983
+ sendJSON(res, 200, { skills });
3984
+ logRequest(method, pathname, 200, Date.now() - start);
3985
+ return;
3986
+ }
3987
+
3988
+ // POST /api/studio/webcraft/skills/:name { skills: [{name, content}] }
3989
+ if (pathname.startsWith('/api/studio/webcraft/skills/') && method === 'POST') {
3990
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/skills/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3991
+ const body = await parseBody(req);
3992
+ const skills = body.skills || [];
3993
+ const skillsDir = path.join(os.homedir(), '.nha', 'webcraft', projName, 'skills');
3994
+ if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
3995
+ // Remove all existing .md files, then write current set
3996
+ for (const fname of fs.readdirSync(skillsDir)) {
3997
+ if (fname.endsWith('.md')) fs.unlinkSync(path.join(skillsDir, fname));
3998
+ }
3999
+ for (const skill of skills) {
4000
+ if (!skill.name) continue;
4001
+ const safeName = skill.name.replace(/[^a-zA-Z0-9_-]/g, '_');
4002
+ fs.writeFileSync(path.join(skillsDir, safeName + '.md'), skill.content || '', 'utf8');
4003
+ }
4004
+ sendJSON(res, 200, { ok: true });
4005
+ logRequest(method, pathname, 200, Date.now() - start);
4006
+ return;
4007
+ }
4008
+
3921
4009
  // GET /api/studio/webcraft/projects/load/:name → { projectName, description, files[] }
3922
4010
  if (pathname.startsWith('/api/studio/webcraft/projects/load/') && method === 'GET') {
3923
4011
  const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/projects/load/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
@@ -4147,6 +4235,13 @@ module.exports = { get, set, del, exists };
4147
4235
  [/require\(['"]\.\/config\/database['"]\)/g, "require('./db')"],
4148
4236
  [/require\(['"]\.\.\/config\/db['"]\)/g, "require('../db')"],
4149
4237
  [/require\(['"]\.\/config\/db['"]\)/g, "require('./db')"],
4238
+ // email utils: LLM puts utils/email but file is in services/email
4239
+ [/require\(['"]\.\.\/utils\/email['"]\)/g, "require('../services/email')"],
4240
+ [/require\(['"]\.\/utils\/email['"]\)/g, "require('./services/email')"],
4241
+ // config module: LLM generates require('../../config') or require('../config')
4242
+ [/require\(['"]\.\.\/\.\.\/config['"]\)/g, "{env:process.env}"],
4243
+ [/require\(['"]\.\.\/config['"]\)/g, "{env:process.env}"],
4244
+ [/require\(['"]\.\/config['"]\)/g, "{env:process.env}"],
4150
4245
  ];
4151
4246
  function patchJsFiles(dir) {
4152
4247
  if (!fs.existsSync(dir)) return;
@@ -4350,6 +4445,24 @@ module.exports = { get, set, del, exists };
4350
4445
  const sendEv = (data) => { try { res.write(`data: ${JSON.stringify(data)}\n\n`); } catch {} };
4351
4446
 
4352
4447
  try {
4448
+ // Always read agent memory file first if present
4449
+ const agentMemoryPath = path.join(sandboxDir, 'webcraft-agent.md');
4450
+ const agentMemory = fs.existsSync(agentMemoryPath) ? fs.readFileSync(agentMemoryPath, 'utf8') : '';
4451
+
4452
+ // Load skills files from skills/ subfolder
4453
+ const skillsDir = path.join(sandboxDir, 'skills');
4454
+ let skillsContext = '';
4455
+ if (fs.existsSync(skillsDir)) {
4456
+ const skillFiles = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md'));
4457
+ if (skillFiles.length > 0) {
4458
+ skillsContext = '\nSKILLS DEL PROGETTO:\n' + skillFiles.map(fname => {
4459
+ const skillName = fname.replace(/\.md$/, '');
4460
+ const content = fs.readFileSync(path.join(skillsDir, fname), 'utf8');
4461
+ return `--- SKILL: ${skillName} ---\n${content}`;
4462
+ }).join('\n\n') + '\n';
4463
+ }
4464
+ }
4465
+
4353
4466
  // Read all project files to give agent full context
4354
4467
  const allFiles = [];
4355
4468
  if (fs.existsSync(sandboxDir)) {
@@ -4407,8 +4520,7 @@ Il tuo lavoro e di correggere, migliorare ed espandere il codice del progetto sa
4407
4520
 
4408
4521
  PROGETTO ATTIVO: ${projectName}
4409
4522
  PERCORSO: ${sandboxDir}
4410
-
4411
- FILE DISPONIBILI:
4523
+ ${agentMemory ? '\nMEMORIA PROGETTO:\n' + agentMemory + '\n' : ''}${skillsContext}FILE DISPONIBILI:
4412
4524
  ${fileList}
4413
4525
 
4414
4526
  STRUMENTI A TUA DISPOSIZIONE (rispondi SOLO con JSON valido per le operazioni):
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 = '13.5.54';
8
+ export const VERSION = '13.5.56';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -6252,6 +6252,10 @@ var wcChatRunning = false;
6252
6252
  var wcChatAttachments = []; // [{name, mimeType, base64, size}]
6253
6253
  var _wcAutoFixAttempts = 0;
6254
6254
  var _wcAutoFixTimer = null;
6255
+ // Skills state
6256
+ var wcSkills = []; // [{name, content}] loaded from disk
6257
+ var wcSkillModal = null; // null | {mode:'edit'|'new', idx:number|null, name, content, generating}
6258
+ var _wcSkillsLoaded = false;
6255
6259
 
6256
6260
  function wcEsc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
6257
6261
 
@@ -6336,6 +6340,7 @@ function renderWebCraft(el) {
6336
6340
  '<div id="wcFieldsList">'+authFieldsHtml+'</div>' +
6337
6341
  '<div style="font-size:9px;color:var(--dim);margin-top:4px">'+t('wc_required_hint')+'</div>' +
6338
6342
  '</div>' +
6343
+ wcSkillsPanelHtml() +
6339
6344
  (wcState.running ?
6340
6345
  '<div style="width:100%;padding:11px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--dim);font-size:12px;text-align:center">&#9203; '+t('wc_generating')+'...</div>'
6341
6346
  : '') +
@@ -6361,7 +6366,8 @@ function renderWebCraft(el) {
6361
6366
  (wcMainTab === 'projects' ? wcProjectsPanelHtml() : editorHtml) +
6362
6367
  '</div>' +
6363
6368
  wcChatPanelHtml() +
6364
- '</div>';
6369
+ '</div>' +
6370
+ wcSkillModalHtml();
6365
6371
  }
6366
6372
 
6367
6373
  function wcPickExample(i) {
@@ -6383,6 +6389,159 @@ function wcTabFiles() { wcRightTab = 'files'; renderWebCraft(document.getElement
6383
6389
  function wcTabPreview() { wcRightTab = 'preview'; renderWebCraft(document.getElementById('content')); }
6384
6390
  function wcOpenSandbox() { if (wcState.sandbox.port) window.open('http://127.0.0.1:' + wcState.sandbox.port, '_blank'); }
6385
6391
 
6392
+ // ── WebCraft Skills ───────────────────────────────────────────────────────────
6393
+ function wcSkillsPanelHtml() {
6394
+ var hasProj = wcState.projectName && wcState.generatedFiles.length > 0;
6395
+ // Load skills from server on first render if project active
6396
+ if (hasProj && !_wcSkillsLoaded) {
6397
+ _wcSkillsLoaded = true;
6398
+ fetch(API + '/api/studio/webcraft/skills/' + encodeURIComponent(wcState.projectName))
6399
+ .then(function(r){ return r.json(); })
6400
+ .then(function(d){ wcSkills = d.skills || []; renderWebCraft(document.getElementById('content')); })
6401
+ .catch(function(){});
6402
+ }
6403
+ var rows = wcSkills.map(function(s, si) {
6404
+ return '<div style="display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px solid var(--border)">' +
6405
+ '<span style="font-size:11px;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">&#128203; '+wcEsc(s.name)+'</span>' +
6406
+ '<button onclick="wcOpenSkill('+si+')" title="Modifica" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:12px;padding:2px 4px">&#9998;</button>' +
6407
+ '<button onclick="wcDeleteSkill('+si+')" title="Elimina" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:12px;padding:2px 4px">&times;</button>' +
6408
+ '</div>';
6409
+ }).join('');
6410
+ return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6411
+ '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
6412
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px">&#128218; Skills & Memoria</div>' +
6413
+ '<button onclick="wcNewSkill()" style="font-size:10px;padding:3px 8px;background:var(--bg3);border:1px solid var(--border2);border-radius:5px;color:var(--green);cursor:pointer">+ Aggiungi</button>' +
6414
+ '</div>' +
6415
+ (wcSkills.length > 0
6416
+ ? '<div style="max-height:120px;overflow-y:auto">' + rows + '</div>'
6417
+ : '<div style="font-size:10px;color:var(--dim);font-style:italic">Nessuna skill. Aggiungine una per dare istruzioni persistenti.</div>'
6418
+ ) +
6419
+ '</div>';
6420
+ }
6421
+
6422
+ function wcSkillModalHtml() {
6423
+ if (!wcSkillModal) return '';
6424
+ var m = wcSkillModal;
6425
+ var isNew = m.mode === 'new';
6426
+ return '<div onclick="wcCloseSkillModal(event)" style="position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;display:flex;align-items:center;justify-content:center">' +
6427
+ '<div onclick="event.stopPropagation()" style="background:var(--bg2);border:1px solid var(--border);border-radius:14px;width:560px;max-width:95vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden">' +
6428
+ '<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px">' +
6429
+ '<span style="font-size:14px;font-weight:700;color:var(--text);flex:1">'+(isNew?'&#128218; Nuova Skill':'&#128218; Modifica Skill')+'</span>' +
6430
+ '<button onclick="wcCloseSkillModal()" style="background:none;border:none;color:var(--dim);font-size:18px;cursor:pointer;line-height:1">&times;</button>' +
6431
+ '</div>' +
6432
+ '<div style="padding:16px 20px;display:flex;flex-direction:column;gap:10px;flex:1;overflow-y:auto">' +
6433
+ '<div>' +
6434
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:4px">NOME FILE (es. stripe.md, email-templates.md)</div>' +
6435
+ '<input id="wcSkillName" value="'+wcEsc(m.name||'')+'" placeholder="nome-skill.md" style="width:100%;padding:7px 10px;font-size:12px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);box-sizing:border-box;font-family:var(--mono)">' +
6436
+ '</div>' +
6437
+ '<div style="background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:10px">' +
6438
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:6px">&#129302; GENERA CON AI — descrivi cosa deve contenere la skill</div>' +
6439
+ '<div style="display:flex;gap:8px">' +
6440
+ '<textarea id="wcSkillAiDesc" rows="2" placeholder="es. Istruzioni per integrare Stripe Checkout con Express. Includi pattern per webhook, error handling e test mode." style="flex:1;padding:7px 10px;font-size:11px;border-radius:6px;border:1px solid var(--border2);background:var(--bg2);color:var(--text);resize:none;font-family:inherit"></textarea>' +
6441
+ '<button onclick="wcGenerateSkill()" '+(m.generating?'disabled':'')+' style="padding:8px 12px;background:var(--green3);border:none;border-radius:6px;color:var(--bg);font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap;align-self:flex-end">'+(m.generating?'&#9203;':'&#9654; Genera')+'</button>' +
6442
+ '</div>' +
6443
+ '</div>' +
6444
+ '<div>' +
6445
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:4px">CONTENUTO (markdown)</div>' +
6446
+ '<textarea id="wcSkillContent" rows="12" placeholder="# Nome Skill'+String.fromCharCode(10)+String.fromCharCode(10)+'Scrivi qui le istruzioni, pattern o snippet..." style="width:100%;padding:8px 10px;font-size:11px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text);resize:vertical;box-sizing:border-box;font-family:var(--mono);line-height:1.6">'+wcEsc(m.content||'')+'</textarea>' +
6447
+ '</div>' +
6448
+ '</div>' +
6449
+ '<div style="padding:12px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px">' +
6450
+ '<button onclick="wcCloseSkillModal()" style="padding:8px 16px;background:var(--bg3);border:1px solid var(--border2);border-radius:7px;color:var(--dim);font-size:12px;cursor:pointer">Annulla</button>' +
6451
+ '<button onclick="wcSaveSkill()" style="padding:8px 18px;background:var(--green3);border:none;border-radius:7px;color:var(--bg);font-size:12px;font-weight:700;cursor:pointer">&#10003; Salva</button>' +
6452
+ '</div>' +
6453
+ '</div>' +
6454
+ '</div>';
6455
+ }
6456
+
6457
+ function wcNewSkill() {
6458
+ wcSkillModal = { mode: 'new', idx: null, name: '', content: '', generating: false };
6459
+ renderWebCraft(document.getElementById('content'));
6460
+ }
6461
+
6462
+ function wcOpenSkill(si) {
6463
+ var s = wcSkills[si];
6464
+ if (!s) return;
6465
+ wcSkillModal = { mode: 'edit', idx: si, name: s.name, content: s.content, generating: false };
6466
+ renderWebCraft(document.getElementById('content'));
6467
+ }
6468
+
6469
+ function wcCloseSkillModal(e) {
6470
+ if (e && e.target !== e.currentTarget) return;
6471
+ wcSkillModal = null;
6472
+ renderWebCraft(document.getElementById('content'));
6473
+ }
6474
+
6475
+ async function wcGenerateSkill() {
6476
+ var descEl = document.getElementById('wcSkillAiDesc');
6477
+ var nameEl = document.getElementById('wcSkillName');
6478
+ var desc = (descEl ? descEl.value : '').trim();
6479
+ if (!desc) { alert('Descrivi prima cosa deve contenere la skill.'); return; }
6480
+ wcSkillModal.generating = true;
6481
+ wcSkillModal.name = nameEl ? nameEl.value : wcSkillModal.name;
6482
+ renderWebCraft(document.getElementById('content'));
6483
+ try {
6484
+ var r = await fetch(API + '/api/studio/webcraft', {
6485
+ method: 'POST',
6486
+ headers: {'Content-Type':'application/json'},
6487
+ body: JSON.stringify({
6488
+ system: 'Sei un esperto di sviluppo web fullstack. Genera un file Markdown di "skill" per il WebCraft Agent di NotHumanAllowed. Il file deve contenere istruzioni, pattern di codice, best practice e snippet pronti per essere usati dal modello AI come contesto persistente. Scrivi SOLO il contenuto Markdown, senza spiegazioni esterne.',
6489
+ user: 'Progetto: ' + wcState.projectName + String.fromCharCode(10) + 'Stack: Express.js, PostgreSQL, JWT auth, BEM CSS' + String.fromCharCode(10) + String.fromCharCode(10) + 'Genera la skill: ' + desc,
6490
+ max_tokens: 2048
6491
+ })
6492
+ });
6493
+ if (r.ok) {
6494
+ var d = await r.json();
6495
+ wcSkillModal.content = d.text || '';
6496
+ wcSkillModal.generating = false;
6497
+ // Auto-suggest name if empty
6498
+ if (!wcSkillModal.name && desc.length > 0) {
6499
+ wcSkillModal.name = desc.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) + '.md';
6500
+ }
6501
+ }
6502
+ } catch(e) {
6503
+ wcSkillModal.generating = false;
6504
+ }
6505
+ renderWebCraft(document.getElementById('content'));
6506
+ }
6507
+
6508
+ async function wcSaveSkill() {
6509
+ var nameEl = document.getElementById('wcSkillName');
6510
+ var contentEl = document.getElementById('wcSkillContent');
6511
+ var name = (nameEl ? nameEl.value : '').trim();
6512
+ var content = contentEl ? contentEl.value : '';
6513
+ if (!name) { alert('Inserisci un nome per la skill.'); return; }
6514
+ if (!name.endsWith('.md')) name = name + '.md';
6515
+ var skill = { name: name, content: content };
6516
+ if (wcSkillModal.mode === 'edit' && wcSkillModal.idx !== null) {
6517
+ wcSkills[wcSkillModal.idx] = skill;
6518
+ } else {
6519
+ wcSkills.push(skill);
6520
+ }
6521
+ wcSkillModal = null;
6522
+ // Persist to server
6523
+ await wcPersistSkills();
6524
+ renderWebCraft(document.getElementById('content'));
6525
+ }
6526
+
6527
+ async function wcDeleteSkill(si) {
6528
+ if (!confirm('Eliminare la skill "' + wcSkills[si].name + '"?')) return;
6529
+ wcSkills.splice(si, 1);
6530
+ await wcPersistSkills();
6531
+ renderWebCraft(document.getElementById('content'));
6532
+ }
6533
+
6534
+ async function wcPersistSkills() {
6535
+ if (!wcState.projectName) return;
6536
+ try {
6537
+ await fetch(API + '/api/studio/webcraft/skills/' + encodeURIComponent(wcState.projectName), {
6538
+ method: 'POST',
6539
+ headers: {'Content-Type':'application/json'},
6540
+ body: JSON.stringify({ skills: wcSkills })
6541
+ });
6542
+ } catch(_) {}
6543
+ }
6544
+
6386
6545
  // ── WebCraft Agent Chat Panel ─────────────────────────────────────────────
6387
6546
  function wcChatPanelHtml() {
6388
6547
  var hasProject = wcState.projectName && wcState.generatedFiles.length > 0;
@@ -6572,10 +6731,13 @@ async function wcChatSend() {
6572
6731
  }
6573
6732
  } else if (ev.type === 'done') {
6574
6733
  wcChatRunning = false;
6575
- if (ev.changed) {
6576
- // Reload project files from disk
6577
- wcReloadProjectFiles();
6578
- }
6734
+ if (ev.changed) { wcReloadProjectFiles(); }
6735
+ // Persist chat to disk
6736
+ fetch(API + '/api/studio/webcraft/projects/chat/save', {
6737
+ method: 'POST',
6738
+ headers: {'Content-Type':'application/json'},
6739
+ body: JSON.stringify({ projectName: wcState.projectName, chat: wcChat })
6740
+ }).catch(function(){});
6579
6741
  } else if (ev.type === 'restart_sandbox') {
6580
6742
  wcStartSandbox();
6581
6743
  } else if (ev.type === 'error') {
@@ -6731,7 +6893,20 @@ async function wcLoadProject(pi) {
6731
6893
  wcState.activeFile = 0;
6732
6894
  wcMainTab = 'new';
6733
6895
  wcRightTab = 'files';
6896
+ // Load persisted chat history
6897
+ try {
6898
+ var cr = await fetch(API + '/api/studio/webcraft/projects/chat/load/' + encodeURIComponent(wcState.projectName));
6899
+ if (cr.ok) { var cd = await cr.json(); wcChat = cd.chat || []; }
6900
+ } catch(_) { wcChat = []; }
6901
+ // Load skills for this project
6902
+ _wcSkillsLoaded = false;
6903
+ wcSkills = [];
6904
+ try {
6905
+ var sr = await fetch(API + '/api/studio/webcraft/skills/' + encodeURIComponent(wcState.projectName));
6906
+ if (sr.ok) { var sd = await sr.json(); wcSkills = sd.skills || []; _wcSkillsLoaded = true; }
6907
+ } catch(_) {}
6734
6908
  renderWebCraft(document.getElementById('content'));
6909
+ wcScrollChatToBottom();
6735
6910
  }
6736
6911
 
6737
6912
  async function wcDeleteProject(pi) {