nothumanallowed 13.5.56 → 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.56",
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,41 +3973,76 @@ ${completedHeadings ? `## SECTIONS ALREADY WRITTEN (headings only):\n${completed
3966
3973
  return;
3967
3974
  }
3968
3975
 
3969
- // GET /api/studio/webcraft/skills/:name → { skills: [{name, content}] }
3976
+ // GET /api/studio/webcraft/skills/:name → { skills: [{name, content, type}] }
3970
3977
  if (pathname.startsWith('/api/studio/webcraft/skills/') && method === 'GET') {
3971
3978
  const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/skills/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3972
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';
3973
3999
  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
- }
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(_) {}
3982
4007
  }
3983
4008
  sendJSON(res, 200, { skills });
3984
4009
  logRequest(method, pathname, 200, Date.now() - start);
3985
4010
  return;
3986
4011
  }
3987
4012
 
3988
- // POST /api/studio/webcraft/skills/:name { skills: [{name, content}] }
4013
+ // POST /api/studio/webcraft/skills/:name { skills: [{name, content, type}] }
3989
4014
  if (pathname.startsWith('/api/studio/webcraft/skills/') && method === 'POST') {
3990
4015
  const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/skills/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
3991
4016
  const body = await parseBody(req);
3992
4017
  const skills = body.skills || [];
3993
4018
  const skillsDir = path.join(os.homedir(), '.nha', 'webcraft', projName, 'skills');
3994
4019
  if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
3995
- // Remove all existing .md files, then write current set
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']);
3996
4028
  for (const fname of fs.readdirSync(skillsDir)) {
3997
- if (fname.endsWith('.md')) fs.unlinkSync(path.join(skillsDir, fname));
4029
+ if (!fname.endsWith('.md')) continue;
4030
+ if (!incoming.has(fname) && !WC_KEEP.has(fname)) fs.unlinkSync(path.join(skillsDir, fname));
3998
4031
  }
4032
+ // Write/update all skills; always keep defaults even if not in incoming (write empty)
4033
+ const typeIndex = {};
3999
4034
  for (const skill of skills) {
4000
4035
  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');
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');
4003
4044
  }
4045
+ fs.writeFileSync(path.join(skillsDir, '_index.json'), JSON.stringify(typeIndex), 'utf8');
4004
4046
  sendJSON(res, 200, { ok: true });
4005
4047
  logRequest(method, pathname, 200, Date.now() - start);
4006
4048
  return;
@@ -4449,18 +4491,30 @@ module.exports = { get, set, del, exists };
4449
4491
  const agentMemoryPath = path.join(sandboxDir, 'webcraft-agent.md');
4450
4492
  const agentMemory = fs.existsSync(agentMemoryPath) ? fs.readFileSync(agentMemoryPath, 'utf8') : '';
4451
4493
 
4452
- // Load skills files from skills/ subfolder
4494
+ // Load context files from skills/ subfolder (skills, memory, provider)
4453
4495
  const skillsDir = path.join(sandboxDir, 'skills');
4454
4496
  let skillsContext = '';
4455
4497
  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$/, '');
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 {
4460
4506
  const content = fs.readFileSync(path.join(skillsDir, fname), 'utf8');
4461
- return `--- SKILL: ${skillName} ---\n${content}`;
4462
- }).join('\n\n') + '\n';
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(_) {}
4463
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';
4464
4518
  }
4465
4519
 
4466
4520
  // Read all project files to give agent full context
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.56';
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
 
@@ -6253,10 +6253,17 @@ var wcChatAttachments = []; // [{name, mimeType, base64, size}]
6253
6253
  var _wcAutoFixAttempts = 0;
6254
6254
  var _wcAutoFixTimer = null;
6255
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}
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
6258
  var _wcSkillsLoaded = false;
6259
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
+ ];
6266
+
6260
6267
  function wcEsc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):''}
6261
6268
 
6262
6269
  function renderWebCraft(el) {
@@ -6389,32 +6396,59 @@ function wcTabFiles() { wcRightTab = 'files'; renderWebCraft(document.getElement
6389
6396
  function wcTabPreview() { wcRightTab = 'preview'; renderWebCraft(document.getElementById('content')); }
6390
6397
  function wcOpenSandbox() { if (wcState.sandbox.port) window.open('http://127.0.0.1:' + wcState.sandbox.port, '_blank'); }
6391
6398
 
6392
- // ── WebCraft Skills ───────────────────────────────────────────────────────────
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
+
6393
6410
  function wcSkillsPanelHtml() {
6394
6411
  var hasProj = wcState.projectName && wcState.generatedFiles.length > 0;
6395
- // Load skills from server on first render if project active
6412
+ // Load context files from server on first render if project active
6396
6413
  if (hasProj && !_wcSkillsLoaded) {
6397
6414
  _wcSkillsLoaded = true;
6398
6415
  fetch(API + '/api/studio/webcraft/skills/' + encodeURIComponent(wcState.projectName))
6399
6416
  .then(function(r){ return r.json(); })
6400
- .then(function(d){ wcSkills = d.skills || []; renderWebCraft(document.getElementById('content')); })
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
+ })
6401
6426
  .catch(function(){});
6402
6427
  }
6428
+ // Can add new skill only (memory + provider are singletons already in defaults)
6403
6429
  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>' +
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>' : '') +
6408
6439
  '</div>';
6409
6440
  }).join('');
6410
6441
  return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px">' +
6411
6442
  '<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>' +
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>' : '') +
6414
6445
  '</div>' +
6415
6446
  (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>'
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
+ )
6418
6452
  ) +
6419
6453
  '</div>';
6420
6454
  }
@@ -6423,27 +6457,59 @@ function wcSkillModalHtml() {
6423
6457
  if (!wcSkillModal) return '';
6424
6458
  var m = wcSkillModal;
6425
6459
  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">' +
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">' +
6428
6478
  '<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>' +
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>' +
6430
6480
  '<button onclick="wcCloseSkillModal()" style="background:none;border:none;color:var(--dim);font-size:18px;cursor:pointer;line-height:1">&times;</button>' +
6431
6481
  '</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)">' +
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) +
6436
6499
  '</div>' +
6437
6500
  '<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>' +
6501
+ '<div style="font-size:10px;color:var(--dim);margin-bottom:6px">&#129302; GENERA CON AI</div>' +
6439
6502
  '<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>' +
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>' +
6442
6505
  '</div>' +
6443
6506
  '</div>' +
6444
6507
  '<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>' +
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>' +
6447
6513
  '</div>' +
6448
6514
  '</div>' +
6449
6515
  '<div style="padding:12px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px">' +
@@ -6454,15 +6520,35 @@ function wcSkillModalHtml() {
6454
6520
  '</div>';
6455
6521
  }
6456
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
+
6457
6543
  function wcNewSkill() {
6458
- wcSkillModal = { mode: 'new', idx: null, name: '', content: '', generating: false };
6544
+ wcSkillModal = { mode: 'new', idx: null, name: '', content: '', type: 'skill', generating: false };
6459
6545
  renderWebCraft(document.getElementById('content'));
6460
6546
  }
6461
6547
 
6462
6548
  function wcOpenSkill(si) {
6463
6549
  var s = wcSkills[si];
6464
6550
  if (!s) return;
6465
- wcSkillModal = { mode: 'edit', idx: si, name: s.name, content: s.content, generating: false };
6551
+ wcSkillModal = { mode: 'edit', idx: si, name: s.name, content: s.content, type: s.type || 'skill', generating: false };
6466
6552
  renderWebCraft(document.getElementById('content'));
6467
6553
  }
6468
6554
 
@@ -6472,21 +6558,37 @@ function wcCloseSkillModal(e) {
6472
6558
  renderWebCraft(document.getElementById('content'));
6473
6559
  }
6474
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
+
6475
6570
  async function wcGenerateSkill() {
6476
6571
  var descEl = document.getElementById('wcSkillAiDesc');
6477
6572
  var nameEl = document.getElementById('wcSkillName');
6573
+ var typeEl = document.getElementById('wcSkillType');
6478
6574
  var desc = (descEl ? descEl.value : '').trim();
6479
- if (!desc) { alert('Descrivi prima cosa deve contenere la skill.'); return; }
6575
+ if (!desc) { alert('Descrivi prima cosa deve contenere il file.'); return; }
6480
6576
  wcSkillModal.generating = true;
6481
- wcSkillModal.name = nameEl ? nameEl.value : wcSkillModal.name;
6577
+ if (nameEl) wcSkillModal.name = nameEl.value;
6578
+ if (typeEl) wcSkillModal.type = typeEl.value;
6482
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
+ };
6483
6585
  try {
6484
6586
  var r = await fetch(API + '/api/studio/webcraft', {
6485
6587
  method: 'POST',
6486
6588
  headers: {'Content-Type':'application/json'},
6487
6589
  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,
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,
6490
6592
  max_tokens: 2048
6491
6593
  })
6492
6594
  });
@@ -6496,39 +6598,43 @@ async function wcGenerateSkill() {
6496
6598
  wcSkillModal.generating = false;
6497
6599
  // Auto-suggest name if empty
6498
6600
  if (!wcSkillModal.name && desc.length > 0) {
6499
- wcSkillModal.name = desc.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 30) + '.md';
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';
6500
6604
  }
6501
6605
  }
6502
- } catch(e) {
6503
- wcSkillModal.generating = false;
6504
- }
6606
+ } catch(e) {}
6607
+ wcSkillModal.generating = false;
6505
6608
  renderWebCraft(document.getElementById('content'));
6506
6609
  }
6507
6610
 
6508
6611
  async function wcSaveSkill() {
6509
6612
  var nameEl = document.getElementById('wcSkillName');
6510
6613
  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; }
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; }
6514
6619
  if (!name.endsWith('.md')) name = name + '.md';
6515
- var skill = { name: name, content: content };
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 };
6516
6626
  if (wcSkillModal.mode === 'edit' && wcSkillModal.idx !== null) {
6517
6627
  wcSkills[wcSkillModal.idx] = skill;
6518
6628
  } else {
6519
6629
  wcSkills.push(skill);
6520
6630
  }
6521
6631
  wcSkillModal = null;
6522
- // Persist to server
6523
6632
  await wcPersistSkills();
6524
6633
  renderWebCraft(document.getElementById('content'));
6525
6634
  }
6526
6635
 
6527
6636
  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'));
6637
+ // No-op: kept for potential future use. Skills are only cleared, not deleted.
6532
6638
  }
6533
6639
 
6534
6640
  async function wcPersistSkills() {