nothumanallowed 13.5.55 → 13.5.57

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.55",
3
+ "version": "13.5.57",
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": {
@@ -3937,6 +3937,13 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
3937
3937
  '(aggiungi qui note per sessioni future — es. "usa Tailwind invece di BEM", "API key in .env.local")',
3938
3938
  ].join('\n');
3939
3939
  fs.writeFileSync(path.join(projDir, 'webcraft-agent.md'), agentMd, 'utf8');
3940
+ // Create default context files (skills, memory, provider) if not present
3941
+ const ctxDir = path.join(projDir, 'skills');
3942
+ if (!fs.existsSync(ctxDir)) fs.mkdirSync(ctxDir, { recursive: true });
3943
+ for (const def of ['memory.md', 'liara.md', 'skills.md']) {
3944
+ const fp = path.join(ctxDir, def);
3945
+ if (!fs.existsSync(fp)) fs.writeFileSync(fp, '', 'utf8');
3946
+ }
3940
3947
  sendJSON(res, 200, { ok: true, dir: projDir });
3941
3948
  logRequest(method, pathname, 200, Date.now() - start);
3942
3949
  return;
@@ -3966,6 +3973,81 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
3966
3973
  return;
3967
3974
  }
3968
3975
 
3976
+ // GET /api/studio/webcraft/skills/:name → { skills: [{name, content, type}] }
3977
+ if (pathname.startsWith('/api/studio/webcraft/skills/') && method === 'GET') {
3978
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/skills/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3979
+ const skillsDir = path.join(os.homedir(), '.nha', 'webcraft', projName, 'skills');
3980
+ // Ensure the 3 default files always exist on disk
3981
+ const WC_DEFAULTS = [
3982
+ { name: 'memory.md', type: 'memory' },
3983
+ { name: 'liara.md', type: 'provider' },
3984
+ { name: 'skills.md', type: 'skill' },
3985
+ ];
3986
+ if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
3987
+ for (const def of WC_DEFAULTS) {
3988
+ const fp = path.join(skillsDir, def.name);
3989
+ if (!fs.existsSync(fp)) fs.writeFileSync(fp, '', 'utf8');
3990
+ }
3991
+ // Load type index
3992
+ const indexPath = path.join(skillsDir, '_index.json');
3993
+ let typeIndex = {};
3994
+ if (fs.existsSync(indexPath)) {
3995
+ try { typeIndex = JSON.parse(fs.readFileSync(indexPath, 'utf8')); } catch(_) {}
3996
+ }
3997
+ // Default types for well-known filenames
3998
+ const defaultType = (n) => n === 'memory.md' ? 'memory' : n === 'liara.md' ? 'provider' : 'skill';
3999
+ const skills = [];
4000
+ for (const fname of fs.readdirSync(skillsDir)) {
4001
+ if (!fname.endsWith('.md')) continue;
4002
+ try {
4003
+ const content = fs.readFileSync(path.join(skillsDir, fname), 'utf8');
4004
+ const type = typeIndex[fname] || defaultType(fname);
4005
+ skills.push({ name: fname, content, type });
4006
+ } catch(_) {}
4007
+ }
4008
+ sendJSON(res, 200, { skills });
4009
+ logRequest(method, pathname, 200, Date.now() - start);
4010
+ return;
4011
+ }
4012
+
4013
+ // POST /api/studio/webcraft/skills/:name { skills: [{name, content, type}] }
4014
+ if (pathname.startsWith('/api/studio/webcraft/skills/') && method === 'POST') {
4015
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/skills/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
4016
+ const body = await parseBody(req);
4017
+ const skills = body.skills || [];
4018
+ const skillsDir = path.join(os.homedir(), '.nha', 'webcraft', projName, 'skills');
4019
+ if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
4020
+ // Build set of incoming names (normalised)
4021
+ const incoming = new Set(skills.map(s => {
4022
+ let n = (s.name || '').replace(/[^a-zA-Z0-9_.-]/g, '_');
4023
+ if (!n.endsWith('.md')) n += '.md';
4024
+ return n;
4025
+ }));
4026
+ // Remove .md files not in incoming set (but never remove the 3 defaults — only clear them)
4027
+ const WC_KEEP = new Set(['memory.md', 'liara.md', 'skills.md']);
4028
+ for (const fname of fs.readdirSync(skillsDir)) {
4029
+ if (!fname.endsWith('.md')) continue;
4030
+ if (!incoming.has(fname) && !WC_KEEP.has(fname)) fs.unlinkSync(path.join(skillsDir, fname));
4031
+ }
4032
+ // Write/update all skills; always keep defaults even if not in incoming (write empty)
4033
+ const typeIndex = {};
4034
+ for (const skill of skills) {
4035
+ if (!skill.name) continue;
4036
+ let safeName = skill.name.replace(/[^a-zA-Z0-9_.-]/g, '_');
4037
+ if (!safeName.endsWith('.md')) safeName += '.md';
4038
+ fs.writeFileSync(path.join(skillsDir, safeName), skill.content || '', 'utf8');
4039
+ typeIndex[safeName] = skill.type || 'skill';
4040
+ }
4041
+ // Ensure defaults exist
4042
+ for (const def of ['memory.md', 'liara.md', 'skills.md']) {
4043
+ if (!fs.existsSync(path.join(skillsDir, def))) fs.writeFileSync(path.join(skillsDir, def), '', 'utf8');
4044
+ }
4045
+ fs.writeFileSync(path.join(skillsDir, '_index.json'), JSON.stringify(typeIndex), 'utf8');
4046
+ sendJSON(res, 200, { ok: true });
4047
+ logRequest(method, pathname, 200, Date.now() - start);
4048
+ return;
4049
+ }
4050
+
3969
4051
  // GET /api/studio/webcraft/projects/load/:name → { projectName, description, files[] }
3970
4052
  if (pathname.startsWith('/api/studio/webcraft/projects/load/') && method === 'GET') {
3971
4053
  const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/projects/load/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
@@ -4409,6 +4491,32 @@ module.exports = { get, set, del, exists };
4409
4491
  const agentMemoryPath = path.join(sandboxDir, 'webcraft-agent.md');
4410
4492
  const agentMemory = fs.existsSync(agentMemoryPath) ? fs.readFileSync(agentMemoryPath, 'utf8') : '';
4411
4493
 
4494
+ // Load context files from skills/ subfolder (skills, memory, provider)
4495
+ const skillsDir = path.join(sandboxDir, 'skills');
4496
+ let skillsContext = '';
4497
+ if (fs.existsSync(skillsDir)) {
4498
+ const indexPath = path.join(skillsDir, '_index.json');
4499
+ let typeIndex = {};
4500
+ if (fs.existsSync(indexPath)) { try { typeIndex = JSON.parse(fs.readFileSync(indexPath, 'utf8')); } catch(_) {} }
4501
+ const defaultType = (n) => n === 'memory.md' ? 'memory' : n === 'liara.md' ? 'provider' : 'skill';
4502
+ const sections = { memory: [], provider: [], skill: [] };
4503
+ for (const fname of fs.readdirSync(skillsDir)) {
4504
+ if (!fname.endsWith('.md')) continue;
4505
+ try {
4506
+ const content = fs.readFileSync(path.join(skillsDir, fname), 'utf8');
4507
+ if (!content.trim()) continue; // skip empty files
4508
+ const type = typeIndex[fname] || defaultType(fname);
4509
+ sections[type] = sections[type] || [];
4510
+ sections[type].push(`### ${fname}\n${content}`);
4511
+ } catch(_) {}
4512
+ }
4513
+ const parts = [];
4514
+ if (sections.memory.length) parts.push('MEMORIA PROGETTO:\n' + sections.memory.join('\n\n'));
4515
+ if (sections.provider.length) parts.push('ISTRUZIONI MODELLO AI:\n' + sections.provider.join('\n\n'));
4516
+ if (sections.skill.length) parts.push('SKILLS & PATTERN:\n' + sections.skill.join('\n\n'));
4517
+ if (parts.length) skillsContext = '\n' + parts.join('\n\n') + '\n';
4518
+ }
4519
+
4412
4520
  // Read all project files to give agent full context
4413
4521
  const allFiles = [];
4414
4522
  if (fs.existsSync(sandboxDir)) {
@@ -4466,8 +4574,7 @@ Il tuo lavoro e di correggere, migliorare ed espandere il codice del progetto sa
4466
4574
 
4467
4575
  PROGETTO ATTIVO: ${projectName}
4468
4576
  PERCORSO: ${sandboxDir}
4469
- ${agentMemory ? '\nMEMORIA PROGETTO:\n' + agentMemory + '\n' : ''}
4470
- FILE DISPONIBILI:
4577
+ ${agentMemory ? '\nMEMORIA PROGETTO:\n' + agentMemory + '\n' : ''}${skillsContext}FILE DISPONIBILI:
4471
4578
  ${fileList}
4472
4579
 
4473
4580
  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.55';
8
+ export const VERSION = '13.5.57';
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,17 @@ 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, type}] type: 'skill'|'memory'|'provider'
6257
+ var wcSkillModal = null; // null | {mode:'edit'|'new', idx:number|null, name, content, type, generating}
6258
+ var _wcSkillsLoaded = false;
6259
+
6260
+ // Default 3 files always present in every project
6261
+ var WC_DEFAULT_FILES = [
6262
+ { name: 'memory.md', type: 'memory', content: '' },
6263
+ { name: 'liara.md', type: 'provider', content: '' },
6264
+ { name: 'skills.md', type: 'skill', content: '' }
6265
+ ];
6255
6266
 
6256
6267
  function wcEsc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
6257
6268
 
@@ -6336,6 +6347,7 @@ function renderWebCraft(el) {
6336
6347
  '<div id="wcFieldsList">'+authFieldsHtml+'</div>' +
6337
6348
  '<div style="font-size:9px;color:var(--dim);margin-top:4px">'+t('wc_required_hint')+'</div>' +
6338
6349
  '</div>' +
6350
+ wcSkillsPanelHtml() +
6339
6351
  (wcState.running ?
6340
6352
  '<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
6353
  : '') +
@@ -6361,7 +6373,8 @@ function renderWebCraft(el) {
6361
6373
  (wcMainTab === 'projects' ? wcProjectsPanelHtml() : editorHtml) +
6362
6374
  '</div>' +
6363
6375
  wcChatPanelHtml() +
6364
- '</div>';
6376
+ '</div>' +
6377
+ wcSkillModalHtml();
6365
6378
  }
6366
6379
 
6367
6380
  function wcPickExample(i) {
@@ -6383,6 +6396,258 @@ function wcTabFiles() { wcRightTab = 'files'; renderWebCraft(document.getElement
6383
6396
  function wcTabPreview() { wcRightTab = 'preview'; renderWebCraft(document.getElementById('content')); }
6384
6397
  function wcOpenSandbox() { if (wcState.sandbox.port) window.open('http://127.0.0.1:' + wcState.sandbox.port, '_blank'); }
6385
6398
 
6399
+ // ── WebCraft Context Files (Skills / Memory / Provider) ───────────────────────
6400
+
6401
+ function wcFileTypeIcon(type) {
6402
+ return type === 'memory' ? '&#129504;' : type === 'provider' ? '&#129302;' : '&#128203;';
6403
+ }
6404
+ function wcFileTypeBadge(type) {
6405
+ var colors = { memory: '#7c5cbf', provider: '#2a7fff', skill: '#1a7a4a' };
6406
+ var labels = { memory: 'memory', provider: 'provider', skill: 'skill' };
6407
+ return '<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:' + (colors[type]||'#444') + ';color:#fff;margin-left:4px;flex-shrink:0">' + (labels[type]||type) + '</span>';
6408
+ }
6409
+
6410
+ function wcSkillsPanelHtml() {
6411
+ var hasProj = wcState.projectName && wcState.generatedFiles.length > 0;
6412
+ // Load context files from server on first render if project active
6413
+ if (hasProj && !_wcSkillsLoaded) {
6414
+ _wcSkillsLoaded = true;
6415
+ fetch(API + '/api/studio/webcraft/skills/' + encodeURIComponent(wcState.projectName))
6416
+ .then(function(r){ return r.json(); })
6417
+ .then(function(d){
6418
+ wcSkills = d.skills || [];
6419
+ // Ensure the 3 default files always exist client-side
6420
+ WC_DEFAULT_FILES.forEach(function(def) {
6421
+ var exists = wcSkills.some(function(s){ return s.name === def.name; });
6422
+ if (!exists) wcSkills.unshift({ name: def.name, type: def.type, content: def.content });
6423
+ });
6424
+ renderWebCraft(document.getElementById('content'));
6425
+ })
6426
+ .catch(function(){});
6427
+ }
6428
+ // Can add new skill only (memory + provider are singletons already in defaults)
6429
+ var rows = wcSkills.map(function(s, si) {
6430
+ var isSingleton = s.type === 'memory' || s.type === 'provider';
6431
+ var isEmpty = !s.content || s.content.trim() === '';
6432
+ return '<div style="display:flex;align-items:center;gap:4px;padding:5px 0;border-bottom:1px solid var(--border)">' +
6433
+ '<span style="font-size:13px;flex-shrink:0">' + wcFileTypeIcon(s.type) + '</span>' +
6434
+ '<span style="font-size:11px;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + wcEsc(s.name) + '</span>' +
6435
+ wcFileTypeBadge(s.type) +
6436
+ (isEmpty ? '<span title="Vuoto" style="font-size:9px;color:#e09020;flex-shrink:0">&#9888;</span>' : '') +
6437
+ '<button onclick="wcOpenSkill('+si+')" title="Modifica" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:12px;padding:2px 4px;flex-shrink:0">&#9998;</button>' +
6438
+ (!isSingleton ? '<button onclick="wcClearSkill('+si+')" title="Svuota" style="background:none;border:none;color:var(--dim);cursor:pointer;font-size:11px;padding:2px 4px;flex-shrink:0">&#128465;</button>' : '') +
6439
+ '</div>';
6440
+ }).join('');
6441
+ return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6442
+ '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
6443
+ '<div style="font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.8px">&#128218; Contesto AI</div>' +
6444
+ (hasProj ? '<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">+ Skill</button>' : '') +
6445
+ '</div>' +
6446
+ (wcSkills.length > 0
6447
+ ? '<div style="max-height:160px;overflow-y:auto">' + rows + '</div>'
6448
+ : (hasProj
6449
+ ? '<div style="font-size:10px;color:var(--dim);font-style:italic">Caricamento...</div>'
6450
+ : '<div style="font-size:10px;color:var(--dim);font-style:italic">Genera un progetto per attivare i file di contesto.</div>'
6451
+ )
6452
+ ) +
6453
+ '</div>';
6454
+ }
6455
+
6456
+ function wcSkillModalHtml() {
6457
+ if (!wcSkillModal) return '';
6458
+ var m = wcSkillModal;
6459
+ var isNew = m.mode === 'new';
6460
+ var charCount = (m.content || '').length;
6461
+ // Length guidance per type
6462
+ var maxChars = m.type === 'skill' ? 6000 : 4000;
6463
+ var warnLen = charCount > maxChars;
6464
+ var typeHints = {
6465
+ skill: 'Istruzioni tecniche, snippet, pattern di codice specifici per una funzione (es. Stripe, email, auth). Puoi avere quante skill vuoi. Max consigliato: ~6000 caratteri.',
6466
+ memory: 'Note persistenti sul progetto: decisioni architetturali, preferenze, contesto generale. Solo UN file. Max consigliato: ~4000 caratteri.',
6467
+ provider: 'Istruzioni specifiche per il modello AI usato (Liara/Qwen3, Claude, GPT-4...). Es. tono, formato risposte, vincoli. Solo UN file. Max consigliato: ~4000 caratteri.'
6468
+ };
6469
+ var hint = typeHints[m.type] || '';
6470
+ var typeOptions = ['skill', 'memory', 'provider'].map(function(t) {
6471
+ var hasSingleton = (t === 'memory' || t === 'provider') && wcSkills.some(function(s){ return s.type === t && (m.mode !== 'edit' || wcSkills.indexOf(s) !== m.idx); });
6472
+ return '<option value="'+t+'"'+(m.type===t?' selected':'')+(hasSingleton?' disabled':'')+'>'+t+(hasSingleton?' (esiste già)':'')+'</option>';
6473
+ }).join('');
6474
+ // Suggested name based on type
6475
+ var namePlaceholder = m.type === 'memory' ? 'memory.md' : m.type === 'provider' ? 'liara.md' : 'nome-skill.md';
6476
+ return '<div onclick="wcCloseSkillModal(event)" style="position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9999;display:flex;align-items:center;justify-content:center">' +
6477
+ '<div onclick="event.stopPropagation()" style="background:var(--bg2);border:1px solid var(--border);border-radius:14px;width:600px;max-width:96vw;max-height:90vh;display:flex;flex-direction:column;overflow:hidden">' +
6478
+ '<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px">' +
6479
+ '<span style="font-size:14px;font-weight:700;color:var(--text);flex:1">' + wcFileTypeIcon(m.type) + ' ' + (isNew ? 'Nuovo file di contesto' : 'Modifica ' + wcEsc(m.name)) + '</span>' +
6480
+ '<button onclick="wcCloseSkillModal()" style="background:none;border:none;color:var(--dim);font-size:18px;cursor:pointer;line-height:1">&times;</button>' +
6481
+ '</div>' +
6482
+ '<div style="padding:16px 20px;display:flex;flex-direction:column;gap:12px;flex:1;overflow-y:auto">' +
6483
+ (isNew ? (
6484
+ '<div style="display:flex;gap:10px">' +
6485
+ '<div style="flex:1">' +
6486
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:4px">TIPO</div>' +
6487
+ '<select id="wcSkillType" onchange="wcSkillTypeChange(this.value)" style="width:100%;padding:7px 10px;font-size:12px;border-radius:6px;border:1px solid var(--border2);background:var(--bg3);color:var(--text)">' + typeOptions + '</select>' +
6488
+ '</div>' +
6489
+ '<div style="flex:2">' +
6490
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:4px">NOME FILE</div>' +
6491
+ '<input id="wcSkillName" value="'+wcEsc(m.name||'')+'" placeholder="'+namePlaceholder+'" 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)">' +
6492
+ '</div>' +
6493
+ '</div>'
6494
+ ) : (
6495
+ '<div style="font-size:11px;color:var(--dim);background:var(--bg3);padding:7px 10px;border-radius:6px">File: <code style="color:var(--text)">'+wcEsc(m.name)+'</code> '+wcFileTypeBadge(m.type)+'</div>'
6496
+ )) +
6497
+ '<div style="background:var(--bg3);border-radius:8px;padding:9px 11px;font-size:10px;color:var(--dim);line-height:1.5">' +
6498
+ '&#128161; ' + wcEsc(hint) +
6499
+ '</div>' +
6500
+ '<div style="background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:10px">' +
6501
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:6px">&#129302; GENERA CON AI</div>' +
6502
+ '<div style="display:flex;gap:8px">' +
6503
+ '<textarea id="wcSkillAiDesc" rows="2" placeholder="Descrivi cosa deve contenere questo file... (es. Istruzioni per integrare Stripe con Express, pattern webhook)" 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>' +
6504
+ '<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>' +
6505
+ '</div>' +
6506
+ '</div>' +
6507
+ '<div>' +
6508
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:4px;display:flex;justify-content:space-between">' +
6509
+ '<span>CONTENUTO (markdown)</span>' +
6510
+ '<span style="color:'+(warnLen?'#e05050':'var(--dim)')+'">'+charCount+' car.'+(warnLen?' &#9888; Troppo lungo, potrebbe ridurre la qualita del contesto':'')+'</span>' +
6511
+ '</div>' +
6512
+ '<textarea id="wcSkillContent" rows="14" oninput="wcSkillContentChange(this.value)" placeholder="# Titolo'+String.fromCharCode(10)+'Scrivi le istruzioni in Markdown..." style="width:100%;padding:8px 10px;font-size:11px;border-radius:6px;border:1px solid '+(warnLen?'#e05050':'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>' +
6513
+ '</div>' +
6514
+ '</div>' +
6515
+ '<div style="padding:12px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px">' +
6516
+ '<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>' +
6517
+ '<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>' +
6518
+ '</div>' +
6519
+ '</div>' +
6520
+ '</div>';
6521
+ }
6522
+
6523
+ function wcSkillTypeChange(newType) {
6524
+ if (!wcSkillModal) return;
6525
+ wcSkillModal.type = newType;
6526
+ // Auto-fill name for singletons
6527
+ if (newType === 'memory') wcSkillModal.name = 'memory.md';
6528
+ else if (newType === 'provider') wcSkillModal.name = 'liara.md';
6529
+ else wcSkillModal.name = '';
6530
+ renderWebCraft(document.getElementById('content'));
6531
+ }
6532
+
6533
+ function wcSkillContentChange(val) {
6534
+ if (!wcSkillModal) return;
6535
+ wcSkillModal.content = val;
6536
+ // Re-render only the char counter without full re-render
6537
+ var maxChars = wcSkillModal.type === 'skill' ? 6000 : 4000;
6538
+ var warnLen = val.length > maxChars;
6539
+ var el = document.querySelector('#wcSkillContent');
6540
+ if (el) el.style.borderColor = warnLen ? '#e05050' : 'var(--border2)';
6541
+ }
6542
+
6543
+ function wcNewSkill() {
6544
+ wcSkillModal = { mode: 'new', idx: null, name: '', content: '', type: 'skill', generating: false };
6545
+ renderWebCraft(document.getElementById('content'));
6546
+ }
6547
+
6548
+ function wcOpenSkill(si) {
6549
+ var s = wcSkills[si];
6550
+ if (!s) return;
6551
+ wcSkillModal = { mode: 'edit', idx: si, name: s.name, content: s.content, type: s.type || 'skill', generating: false };
6552
+ renderWebCraft(document.getElementById('content'));
6553
+ }
6554
+
6555
+ function wcCloseSkillModal(e) {
6556
+ if (e && e.target !== e.currentTarget) return;
6557
+ wcSkillModal = null;
6558
+ renderWebCraft(document.getElementById('content'));
6559
+ }
6560
+
6561
+ async function wcClearSkill(si) {
6562
+ var s = wcSkills[si];
6563
+ if (!s) return;
6564
+ if (!confirm('Svuotare il file "' + s.name + '"? Il file rimane ma il contenuto viene cancellato.')) return;
6565
+ wcSkills[si].content = '';
6566
+ await wcPersistSkills();
6567
+ renderWebCraft(document.getElementById('content'));
6568
+ }
6569
+
6570
+ async function wcGenerateSkill() {
6571
+ var descEl = document.getElementById('wcSkillAiDesc');
6572
+ var nameEl = document.getElementById('wcSkillName');
6573
+ var typeEl = document.getElementById('wcSkillType');
6574
+ var desc = (descEl ? descEl.value : '').trim();
6575
+ if (!desc) { alert('Descrivi prima cosa deve contenere il file.'); return; }
6576
+ wcSkillModal.generating = true;
6577
+ if (nameEl) wcSkillModal.name = nameEl.value;
6578
+ if (typeEl) wcSkillModal.type = typeEl.value;
6579
+ renderWebCraft(document.getElementById('content'));
6580
+ var systemByType = {
6581
+ skill: 'Sei un esperto di sviluppo web fullstack. Genera un file Markdown "skill" per il WebCraft Agent di NotHumanAllowed. Deve contenere istruzioni, pattern di codice, best practice e snippet pronti all uso come contesto persistente. Scrivi SOLO il contenuto Markdown, niente altro.',
6582
+ memory: 'Sei un assistente tecnico. Genera un file Markdown "memory" per il WebCraft Agent. Deve riassumere decisioni architetturali, preferenze dello sviluppatore e contesto generale del progetto. Scrivi SOLO il Markdown.',
6583
+ provider: 'Sei un esperto di prompt engineering. Genera un file Markdown con istruzioni specifiche per calibrare il comportamento del modello AI (tono, formato risposte, vincoli, preferenze). Scrivi SOLO il Markdown.'
6584
+ };
6585
+ try {
6586
+ var r = await fetch(API + '/api/studio/webcraft', {
6587
+ method: 'POST',
6588
+ headers: {'Content-Type':'application/json'},
6589
+ body: JSON.stringify({
6590
+ system: systemByType[wcSkillModal.type] || systemByType.skill,
6591
+ user: 'Progetto: ' + wcState.projectName + String.fromCharCode(10) + 'Stack: Express.js, PostgreSQL, JWT auth' + String.fromCharCode(10) + String.fromCharCode(10) + desc,
6592
+ max_tokens: 2048
6593
+ })
6594
+ });
6595
+ if (r.ok) {
6596
+ var d = await r.json();
6597
+ wcSkillModal.content = d.text || '';
6598
+ wcSkillModal.generating = false;
6599
+ // Auto-suggest name if empty
6600
+ if (!wcSkillModal.name && desc.length > 0) {
6601
+ if (wcSkillModal.type === 'memory') wcSkillModal.name = 'memory.md';
6602
+ else if (wcSkillModal.type === 'provider') wcSkillModal.name = 'liara.md';
6603
+ else wcSkillModal.name = desc.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) + '.md';
6604
+ }
6605
+ }
6606
+ } catch(e) {}
6607
+ wcSkillModal.generating = false;
6608
+ renderWebCraft(document.getElementById('content'));
6609
+ }
6610
+
6611
+ async function wcSaveSkill() {
6612
+ var nameEl = document.getElementById('wcSkillName');
6613
+ var contentEl = document.getElementById('wcSkillContent');
6614
+ var typeEl = document.getElementById('wcSkillType');
6615
+ var name = (nameEl ? nameEl.value : wcSkillModal.name).trim();
6616
+ var content = contentEl ? contentEl.value : (wcSkillModal.content || '');
6617
+ var type = (typeEl ? typeEl.value : wcSkillModal.type) || 'skill';
6618
+ if (!name) { alert('Inserisci un nome per il file.'); return; }
6619
+ if (!name.endsWith('.md')) name = name + '.md';
6620
+ // Enforce singleton: only one memory, one provider
6621
+ if ((type === 'memory' || type === 'provider') && wcSkillModal.mode === 'new') {
6622
+ var existing = wcSkills.findIndex(function(s){ return s.type === type; });
6623
+ if (existing >= 0) { alert('Esiste gia un file di tipo "' + type + '". Modificalo direttamente.'); return; }
6624
+ }
6625
+ var skill = { name: name, content: content, type: type };
6626
+ if (wcSkillModal.mode === 'edit' && wcSkillModal.idx !== null) {
6627
+ wcSkills[wcSkillModal.idx] = skill;
6628
+ } else {
6629
+ wcSkills.push(skill);
6630
+ }
6631
+ wcSkillModal = null;
6632
+ await wcPersistSkills();
6633
+ renderWebCraft(document.getElementById('content'));
6634
+ }
6635
+
6636
+ async function wcDeleteSkill(si) {
6637
+ // No-op: kept for potential future use. Skills are only cleared, not deleted.
6638
+ }
6639
+
6640
+ async function wcPersistSkills() {
6641
+ if (!wcState.projectName) return;
6642
+ try {
6643
+ await fetch(API + '/api/studio/webcraft/skills/' + encodeURIComponent(wcState.projectName), {
6644
+ method: 'POST',
6645
+ headers: {'Content-Type':'application/json'},
6646
+ body: JSON.stringify({ skills: wcSkills })
6647
+ });
6648
+ } catch(_) {}
6649
+ }
6650
+
6386
6651
  // ── WebCraft Agent Chat Panel ─────────────────────────────────────────────
6387
6652
  function wcChatPanelHtml() {
6388
6653
  var hasProject = wcState.projectName && wcState.generatedFiles.length > 0;
@@ -6739,6 +7004,13 @@ async function wcLoadProject(pi) {
6739
7004
  var cr = await fetch(API + '/api/studio/webcraft/projects/chat/load/' + encodeURIComponent(wcState.projectName));
6740
7005
  if (cr.ok) { var cd = await cr.json(); wcChat = cd.chat || []; }
6741
7006
  } catch(_) { wcChat = []; }
7007
+ // Load skills for this project
7008
+ _wcSkillsLoaded = false;
7009
+ wcSkills = [];
7010
+ try {
7011
+ var sr = await fetch(API + '/api/studio/webcraft/skills/' + encodeURIComponent(wcState.projectName));
7012
+ if (sr.ok) { var sd = await sr.json(); wcSkills = sd.skills || []; _wcSkillsLoaded = true; }
7013
+ } catch(_) {}
6742
7014
  renderWebCraft(document.getElementById('content'));
6743
7015
  wcScrollChatToBottom();
6744
7016
  }