nothumanallowed 13.5.56 → 13.5.58

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.58",
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';
4003
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');
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
@@ -4643,6 +4697,154 @@ REGOLE CRITICHE:
4643
4697
  return;
4644
4698
  }
4645
4699
 
4700
+ // POST /api/studio/webcraft/snapshot { projectName } → { ok, snapshot }
4701
+ // Creates a timestamped snapshot of all project files (excludes node_modules)
4702
+ if (pathname === '/api/studio/webcraft/snapshot' && method === 'POST') {
4703
+ const body = await parseBody(req);
4704
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4705
+ if (!projName) { sendJSON(res, 400, { error: 'projectName required' }); return; }
4706
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4707
+ const snapDir = path.join(projDir, '.snapshots');
4708
+ if (!fs.existsSync(snapDir)) fs.mkdirSync(snapDir, { recursive: true });
4709
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
4710
+ const snapPath = path.join(snapDir, ts + '.json');
4711
+ const snapshot = {};
4712
+ const walkSnap = (dir, base) => {
4713
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
4714
+ if (entry.name === 'node_modules' || entry.name === '.snapshots' || entry.name.startsWith('.')) continue;
4715
+ const rel = base ? base + '/' + entry.name : entry.name;
4716
+ if (entry.isDirectory()) { walkSnap(path.join(dir, entry.name), rel); }
4717
+ else {
4718
+ try { snapshot[rel] = fs.readFileSync(path.join(dir, entry.name), 'utf8'); } catch(_) {}
4719
+ }
4720
+ }
4721
+ };
4722
+ walkSnap(projDir, '');
4723
+ fs.writeFileSync(snapPath, JSON.stringify({ ts, files: snapshot }), 'utf8');
4724
+ // Keep only last 10 snapshots
4725
+ const allSnaps = fs.readdirSync(snapDir).filter(f => f.endsWith('.json')).sort();
4726
+ if (allSnaps.length > 10) {
4727
+ for (const old of allSnaps.slice(0, allSnaps.length - 10)) {
4728
+ try { fs.unlinkSync(path.join(snapDir, old)); } catch(_) {}
4729
+ }
4730
+ }
4731
+ sendJSON(res, 200, { ok: true, snapshot: ts, fileCount: Object.keys(snapshot).length });
4732
+ logRequest(method, pathname, 200, Date.now() - start);
4733
+ return;
4734
+ }
4735
+
4736
+ // GET /api/studio/webcraft/snapshots/:name → { snapshots: [{ts, fileCount}] }
4737
+ if (pathname.startsWith('/api/studio/webcraft/snapshots/') && method === 'GET') {
4738
+ const projName = decodeURIComponent(pathname.replace('/api/studio/webcraft/snapshots/', '')).replace(/[^a-zA-Z0-9_-]/g, '');
4739
+ const snapDir = path.join(os.homedir(), '.nha', 'webcraft', projName, '.snapshots');
4740
+ if (!fs.existsSync(snapDir)) { sendJSON(res, 200, { snapshots: [] }); return; }
4741
+ const snaps = fs.readdirSync(snapDir).filter(f => f.endsWith('.json')).sort().reverse().map(f => {
4742
+ try {
4743
+ const data = JSON.parse(fs.readFileSync(path.join(snapDir, f), 'utf8'));
4744
+ return { ts: data.ts, fileCount: Object.keys(data.files || {}).length };
4745
+ } catch(_) { return null; }
4746
+ }).filter(Boolean);
4747
+ sendJSON(res, 200, { snapshots: snaps });
4748
+ logRequest(method, pathname, 200, Date.now() - start);
4749
+ return;
4750
+ }
4751
+
4752
+ // POST /api/studio/webcraft/restore { projectName, ts } → { ok, restored }
4753
+ if (pathname === '/api/studio/webcraft/restore' && method === 'POST') {
4754
+ const body = await parseBody(req);
4755
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4756
+ const ts = (body.ts || '').replace(/[^0-9T\-]/g, '');
4757
+ if (!projName || !ts) { sendJSON(res, 400, { error: 'projectName and ts required' }); return; }
4758
+ const snapPath = path.join(os.homedir(), '.nha', 'webcraft', projName, '.snapshots', ts + '.json');
4759
+ if (!fs.existsSync(snapPath)) { sendJSON(res, 404, { error: 'snapshot not found' }); return; }
4760
+ const data = JSON.parse(fs.readFileSync(snapPath, 'utf8'));
4761
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4762
+ let restored = 0;
4763
+ for (const [rel, content] of Object.entries(data.files || {})) {
4764
+ const fp = path.join(projDir, rel);
4765
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
4766
+ fs.writeFileSync(fp, content, 'utf8');
4767
+ restored++;
4768
+ }
4769
+ sendJSON(res, 200, { ok: true, restored });
4770
+ logRequest(method, pathname, 200, Date.now() - start);
4771
+ return;
4772
+ }
4773
+
4774
+ // POST /api/studio/webcraft/syntax-check { projectName } → { results: [{file, ok, error}] }
4775
+ if (pathname === '/api/studio/webcraft/syntax-check' && method === 'POST') {
4776
+ const body = await parseBody(req);
4777
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4778
+ if (!projName) { sendJSON(res, 400, { error: 'projectName required' }); return; }
4779
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4780
+ const { execFile } = await import('child_process');
4781
+ const { promisify } = await import('util');
4782
+ const execFileAsync = promisify(execFile);
4783
+ const jsFiles = [];
4784
+ const walkJs = (dir, base) => {
4785
+ if (!fs.existsSync(dir)) return;
4786
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
4787
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
4788
+ const rel = base ? base + '/' + entry.name : entry.name;
4789
+ if (entry.isDirectory()) { walkJs(path.join(dir, entry.name), rel); }
4790
+ else if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) { jsFiles.push(rel); }
4791
+ }
4792
+ };
4793
+ walkJs(projDir, '');
4794
+ const results = [];
4795
+ for (const rel of jsFiles) {
4796
+ const fp = path.join(projDir, rel);
4797
+ try {
4798
+ await execFileAsync(process.execPath, ['--check', fp], { timeout: 5000 });
4799
+ results.push({ file: rel, ok: true });
4800
+ } catch(e) {
4801
+ const errMsg = (e.stderr || e.message || '').split('\n')[0].replace(fp, rel);
4802
+ results.push({ file: rel, ok: false, error: errMsg });
4803
+ }
4804
+ }
4805
+ sendJSON(res, 200, { results });
4806
+ logRequest(method, pathname, 200, Date.now() - start);
4807
+ return;
4808
+ }
4809
+
4810
+ // POST /api/studio/webcraft/grep { projectName, query } → { matches: [{file, line, lineNum, match}] }
4811
+ if (pathname === '/api/studio/webcraft/grep' && method === 'POST') {
4812
+ const body = await parseBody(req);
4813
+ const projName = (body.projectName || '').replace(/[^a-zA-Z0-9_-]/g, '');
4814
+ const query = (body.query || '').slice(0, 200);
4815
+ if (!projName || !query) { sendJSON(res, 400, { error: 'projectName and query required' }); return; }
4816
+ const projDir = path.join(os.homedir(), '.nha', 'webcraft', projName);
4817
+ const matches = [];
4818
+ let queryRe;
4819
+ try { queryRe = new RegExp(query, 'gi'); } catch(_) { queryRe = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); }
4820
+ const walkGrep = (dir, base) => {
4821
+ if (!fs.existsSync(dir)) return;
4822
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
4823
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
4824
+ const rel = base ? base + '/' + entry.name : entry.name;
4825
+ if (entry.isDirectory()) { walkGrep(path.join(dir, entry.name), rel); }
4826
+ else {
4827
+ const ext = entry.name.split('.').pop();
4828
+ if (!['js','mjs','ts','html','css','json','md','sql','env','conf'].includes(ext)) continue;
4829
+ try {
4830
+ const lines = fs.readFileSync(path.join(dir, entry.name), 'utf8').split('\n');
4831
+ lines.forEach((line, idx) => {
4832
+ queryRe.lastIndex = 0;
4833
+ if (queryRe.test(line)) {
4834
+ matches.push({ file: rel, lineNum: idx + 1, line: line.trim().slice(0, 200) });
4835
+ if (matches.length >= 100) return;
4836
+ }
4837
+ });
4838
+ } catch(_) {}
4839
+ }
4840
+ }
4841
+ };
4842
+ walkGrep(projDir, '');
4843
+ sendJSON(res, 200, { matches: matches.slice(0, 100) });
4844
+ logRequest(method, pathname, 200, Date.now() - start);
4845
+ return;
4846
+ }
4847
+
4646
4848
  // ── Studio: Parliament deliberation (SSE streaming) ──────────────────
4647
4849
  // Implements the Legion DeliberationEngine protocol adapted for Studio:
4648
4850
  // Round 1 outputs already exist (from normal workflow steps).
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.58';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11