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 +1 -1
- package/src/commands/ui.mjs +223 -21
- package/src/constants.mjs +1 -1
- package/src/services/web-ui.mjs +741 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.5.
|
|
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": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -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
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
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
|
-
//
|
|
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'))
|
|
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
|
-
|
|
4002
|
-
|
|
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
|
|
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
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
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
|
-
|
|
4462
|
-
|
|
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.
|
|
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
|
|