nothumanallowed 3.8.0 → 4.0.1

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": "3.8.0",
3
+ "version": "4.0.1",
4
4
  "description": "NotHumanAllowed — 38 AI agents for security, code, DevOps, data & daily ops. Ask agents directly, plan your day with 5 specialist agents, manage tasks, connect Gmail + Calendar.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -15,6 +15,7 @@ import { cmdOps } from './commands/ops.mjs';
15
15
  import { cmdChat } from './commands/chat.mjs';
16
16
  import { cmdUI } from './commands/ui.mjs';
17
17
  import { cmdGoogle } from './commands/google-auth.mjs';
18
+ import { cmdScan } from './commands/scan.mjs';
18
19
  import { banner, info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, M, B, R } from './ui.mjs';
19
20
 
20
21
  export async function main(argv) {
@@ -65,6 +66,9 @@ export async function main(argv) {
65
66
  case 'google':
66
67
  return cmdGoogle(args);
67
68
 
69
+ case 'scan':
70
+ return cmdScan(args);
71
+
68
72
  case 'pif':
69
73
  return cmdPif(args);
70
74
 
@@ -355,6 +359,8 @@ function cmdHelp() {
355
359
  console.log(` ask oracle "prompt" ${D}--provider openai${NC}`);
356
360
  console.log(` agents List all 38 specialized agents`);
357
361
  console.log(` agents info <name> Show agent capabilities & domain`);
362
+ console.log(` scan <path> Security scan a project with SABER + ZERO`);
363
+ console.log(` scan . ${D}--output report.md${NC} Save report to file`);
358
364
  console.log(` run "prompt" Multi-agent collaboration (server-routed)`);
359
365
  console.log(` run "prompt" ${D}--agents saber,zero${NC} Collaborate with specific agents\n`);
360
366
 
@@ -0,0 +1,187 @@
1
+ /**
2
+ * nha scan <path> — Scan a project with SABER + ZERO agents.
3
+ * Reads files, builds context, runs security + vulnerability analysis.
4
+ * Zero server. Direct LLM calls with your API key.
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { loadConfig } from '../config.mjs';
10
+ import { callAgent } from '../services/llm.mjs';
11
+ import { fail, info, ok, warn, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
12
+
13
+ const SCAN_EXTENSIONS = new Set([
14
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
15
+ '.py', '.rb', '.go', '.rs', '.java', '.kt',
16
+ '.php', '.c', '.cpp', '.h', '.cs', '.swift',
17
+ '.sh', '.bash', '.zsh',
18
+ '.json', '.yaml', '.yml', '.toml', '.ini', '.cfg',
19
+ '.env', '.env.example', '.env.local',
20
+ '.sql', '.prisma', '.graphql',
21
+ '.dockerfile', '.docker-compose.yml',
22
+ '.tf', '.hcl',
23
+ '.md', '.txt',
24
+ ]);
25
+
26
+ const IGNORE_DIRS = new Set([
27
+ 'node_modules', '.git', '.next', 'dist', 'build', 'out',
28
+ '__pycache__', '.venv', 'venv', 'vendor', 'target',
29
+ '.cache', '.turbo', 'coverage', '.nyc_output',
30
+ ]);
31
+
32
+ const MAX_FILE_SIZE = 100_000; // 100KB per file
33
+ const MAX_TOTAL_CHARS = 200_000; // 200KB total context
34
+
35
+ function scanDirectory(dirPath, files = [], depth = 0) {
36
+ if (depth > 8) return files;
37
+ try {
38
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
39
+ for (const entry of entries) {
40
+ if (entry.name.startsWith('.') && entry.name !== '.env' && entry.name !== '.env.example') continue;
41
+ if (IGNORE_DIRS.has(entry.name)) continue;
42
+
43
+ const fullPath = path.join(dirPath, entry.name);
44
+ if (entry.isDirectory()) {
45
+ scanDirectory(fullPath, files, depth + 1);
46
+ } else if (entry.isFile()) {
47
+ const ext = path.extname(entry.name).toLowerCase();
48
+ if (SCAN_EXTENSIONS.has(ext) || entry.name === 'Dockerfile' || entry.name === 'Makefile') {
49
+ try {
50
+ const stat = fs.statSync(fullPath);
51
+ if (stat.size <= MAX_FILE_SIZE && stat.size > 0) {
52
+ files.push({ path: fullPath, size: stat.size, ext });
53
+ }
54
+ } catch {}
55
+ }
56
+ }
57
+ }
58
+ } catch {}
59
+ return files;
60
+ }
61
+
62
+ function buildProjectContext(projectPath, files) {
63
+ let context = `Project: ${projectPath}\nFiles scanned: ${files.length}\n\n`;
64
+ let totalChars = context.length;
65
+
66
+ // Prioritize security-relevant files first
67
+ const prioritized = files.sort((a, b) => {
68
+ const securityFiles = ['.env', 'auth', 'login', 'password', 'secret', 'token', 'key', 'security', 'middleware'];
69
+ const aScore = securityFiles.some(s => a.path.toLowerCase().includes(s)) ? 0 : 1;
70
+ const bScore = securityFiles.some(s => b.path.toLowerCase().includes(s)) ? 0 : 1;
71
+ return aScore - bScore;
72
+ });
73
+
74
+ for (const file of prioritized) {
75
+ if (totalChars >= MAX_TOTAL_CHARS) break;
76
+ try {
77
+ const content = fs.readFileSync(file.path, 'utf-8');
78
+ const relative = path.relative(projectPath, file.path);
79
+ const entry = `\n--- ${relative} (${file.size} bytes) ---\n${content.slice(0, MAX_FILE_SIZE)}\n`;
80
+ if (totalChars + entry.length > MAX_TOTAL_CHARS) {
81
+ const remaining = MAX_TOTAL_CHARS - totalChars;
82
+ context += entry.slice(0, remaining) + '\n[... truncated ...]\n';
83
+ break;
84
+ }
85
+ context += entry;
86
+ totalChars += entry.length;
87
+ } catch {}
88
+ }
89
+
90
+ return context;
91
+ }
92
+
93
+ export async function cmdScan(args) {
94
+ const projectPath = args[0] ? path.resolve(args[0]) : process.cwd();
95
+
96
+ if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
97
+ fail(`Not a directory: ${projectPath}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ const config = loadConfig();
102
+ if (!config.llm.apiKey) {
103
+ fail('No API key configured. Run: nha config set key YOUR_KEY');
104
+ process.exit(1);
105
+ }
106
+
107
+ // Parse flags
108
+ let agents = ['saber', 'zero'];
109
+ let outputFile = null;
110
+ for (let i = 1; i < args.length; i++) {
111
+ if (args[i] === '--agents' && args[i + 1]) { agents = args[++i].split(','); continue; }
112
+ if (args[i] === '--output' && args[i + 1]) { outputFile = args[++i]; continue; }
113
+ if (args[i] === '-o' && args[i + 1]) { outputFile = args[++i]; continue; }
114
+ }
115
+
116
+ console.log(`\n ${BOLD}NHA Project Scanner${NC}`);
117
+ console.log(` ${D}Path: ${projectPath}${NC}\n`);
118
+
119
+ // Scan files
120
+ info('Scanning project files...');
121
+ const files = scanDirectory(projectPath);
122
+ ok(`Found ${files.length} scannable files`);
123
+
124
+ if (files.length === 0) {
125
+ warn('No source files found.');
126
+ return;
127
+ }
128
+
129
+ // Show file breakdown
130
+ const extCounts = {};
131
+ for (const f of files) {
132
+ extCounts[f.ext] = (extCounts[f.ext] || 0) + 1;
133
+ }
134
+ const breakdown = Object.entries(extCounts).sort((a, b) => b[1] - a[1]).slice(0, 8);
135
+ console.log(` ${D}${breakdown.map(([e, c]) => `${e}: ${c}`).join(' | ')}${NC}\n`);
136
+
137
+ // Build context
138
+ info('Building project context...');
139
+ const context = buildProjectContext(projectPath, files);
140
+ ok(`Context: ${(context.length / 1024).toFixed(0)} KB from ${files.length} files`);
141
+
142
+ // Run agents
143
+ const results = {};
144
+ for (const agentName of agents) {
145
+ info(`Running ${agentName.toUpperCase()}...`);
146
+ const startTime = Date.now();
147
+ try {
148
+ const result = await callAgent(config, agentName,
149
+ `Perform a thorough analysis of this project. Focus on:\n` +
150
+ `- Security vulnerabilities (OWASP Top 10, CWE references)\n` +
151
+ `- Hardcoded secrets, API keys, passwords\n` +
152
+ `- Dependency risks, outdated patterns\n` +
153
+ `- Authentication/authorization issues\n` +
154
+ `- Input validation, injection vectors\n` +
155
+ `- Configuration security\n` +
156
+ `- Code quality issues that impact security\n\n` +
157
+ `PROJECT FILES:\n${context}`
158
+ );
159
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
160
+ results[agentName] = result;
161
+ ok(`${agentName.toUpperCase()} complete (${elapsed}s)`);
162
+ } catch (e) {
163
+ warn(`${agentName.toUpperCase()} failed: ${e.message}`);
164
+ results[agentName] = `Error: ${e.message}`;
165
+ }
166
+ }
167
+
168
+ // Display results
169
+ console.log(`\n${'='.repeat(60)}`);
170
+ for (const [agent, result] of Object.entries(results)) {
171
+ console.log(`\n ${BOLD}${C}${agent.toUpperCase()} REPORT${NC}\n`);
172
+ console.log(result);
173
+ console.log(`\n${'─'.repeat(60)}`);
174
+ }
175
+
176
+ // Save to file if requested
177
+ if (outputFile) {
178
+ const report = Object.entries(results)
179
+ .map(([agent, result]) => `# ${agent.toUpperCase()} REPORT\n\n${result}`)
180
+ .join('\n\n---\n\n');
181
+ const header = `# NHA Security Scan Report\n\nProject: ${projectPath}\nDate: ${new Date().toISOString()}\nAgents: ${agents.join(', ')}\nFiles scanned: ${files.length}\n\n---\n\n`;
182
+ fs.writeFileSync(outputFile, header + report, 'utf-8');
183
+ ok(`Report saved to ${outputFile}`);
184
+ }
185
+
186
+ console.log('');
187
+ }
@@ -651,8 +651,20 @@ export async function cmdUI(args) {
651
651
  }
652
652
  } catch { /* context loading failed, proceed without it */ }
653
653
 
654
- const enrichedPrompt = body.prompt + (context
655
- ? '\n\nIMPORTANT: Below is the user\'s REAL personal data (emails, calendar, tasks). Use this actual data in your analysis — do NOT make up fictional examples.\n' + context
654
+ // Attach file content if provided
655
+ let fileContext = '';
656
+ if (body.fileContent && body.fileName) {
657
+ const maxChars = 100000;
658
+ const content = String(body.fileContent).slice(0, maxChars);
659
+ fileContext = '\n\n--- Attached file: ' + body.fileName + ' ---\n' + content;
660
+ if (body.fileContent.length > maxChars) fileContext += '\n[... truncated at 100KB ...]';
661
+ }
662
+
663
+ const enrichedPrompt = body.prompt + fileContext + (context
664
+ ? '\n\nIMPORTANT CONTEXT: The data below is from the user\'s OWN accounts (their Gmail, their Google Calendar, their tasks). The user is the OWNER of these accounts. ' +
665
+ 'Recent activity (npm publishes, Google login alerts, GitHub notifications) was done BY THE USER THEMSELVES as part of their normal work. ' +
666
+ 'Do NOT flag the user\'s own legitimate activity as suspicious or compromised. ' +
667
+ 'Only flag ACTUAL external threats: phishing emails from unknown senders, suspicious links, unauthorized access from unknown locations.\n' + context
656
668
  : '');
657
669
 
658
670
  const response = await callAgent(config, body.agent, enrichedPrompt);
@@ -122,12 +122,15 @@ input:focus,textarea:focus{border-color:var(--green3)}
122
122
  .event__location{color:var(--dim);font-size:11px}
123
123
 
124
124
  /* ---- AGENTS ---- */
125
- .agents-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px}
126
- @media(min-width:901px){.agents-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}}
127
- .agent-card{padding:12px;text-align:center;cursor:pointer;transition:border-color .15s}
128
- .agent-card:hover{border-color:var(--green3)}
129
- .agent-card__name{color:var(--green);font-weight:700;font-size:13px;margin-bottom:4px}
130
- .agent-card__cat{font-size:9px;color:var(--dim);text-transform:uppercase}
125
+ .agents-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
126
+ @media(min-width:600px){.agents-grid{grid-template-columns:repeat(3,1fr)}}
127
+ @media(min-width:901px){.agents-grid{grid-template-columns:repeat(4,1fr)}}
128
+ .agent-card{aspect-ratio:1;padding:14px;text-align:center;cursor:pointer;transition:border-color .15s,transform .15s;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px}
129
+ .agent-card:hover{border-color:var(--green3);transform:scale(1.03)}
130
+ .agent-card__icon{font-size:28px;line-height:1}
131
+ .agent-card__name{color:var(--green);font-weight:700;font-size:13px}
132
+ .agent-card__tagline{font-size:10px;color:var(--text);line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
133
+ .agent-card__cat{font-size:9px;color:var(--dim);text-transform:uppercase;margin-top:auto}
131
134
 
132
135
  /* ---- MODAL ---- */
133
136
  .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:300;align-items:center;justify-content:center}
@@ -475,32 +478,73 @@ function openDayDetail(dateStr){
475
478
  // Use the agent modal for day detail
476
479
  document.getElementById('modalName').textContent=dayLabel;
477
480
  document.getElementById('modalPrompt').style.display='none';
481
+ document.getElementById('fileDropZone').style.display='none';
482
+ document.getElementById('fileInfo').style.display='none';
478
483
  document.getElementById('modalResponse').style.display='block';
479
484
  document.getElementById('modalResponse').innerHTML=h;
480
485
  document.getElementById('agentModal').classList.add('modal-overlay--open');
481
- // Hide ask button
482
486
  var sendBtn=document.getElementById('agentModal').querySelector('.btn--primary');
483
487
  if(sendBtn)sendBtn.style.display='none';
484
488
  }
485
489
 
486
490
  // ---- AGENTS ----
491
+ var AGENT_ICONS = {
492
+ saber:'\\u{1F6E1}',zero:'\\u{1F50D}',veritas:'\\u2713',ade:'\\u{1F52C}',heimdall:'\\u{1F512}',
493
+ jarvis:'\\u{1F4BB}',forge:'\\u2699',pipe:'\\u{1F527}',shell:'\\u{1F4DF}',glitch:'\\u{1F41B}',
494
+ oracle:'\\u{1F4CA}',logos:'\\u{1F9EE}',atlas:'\\u{1F5FA}',cartographer:'\\u{1F30D}',
495
+ scheherazade:'\\u270D',quill:'\\u{1F4DD}',muse:'\\u{1F3A8}',murasaki:'\\u{1F58C}',
496
+ hermes:'\\u{1F517}',link:'\\u{1F50C}',mercury:'\\u{1F310}',
497
+ shogun:'\\u2638',flux:'\\u{1F504}',cron:'\\u23F0',
498
+ babel:'\\u{1F30E}',polyglot:'\\u{1F5E3}',herald:'\\u{1F4E2}',
499
+ echo:'\\u{1F4E1}',macro:'\\u26A1',
500
+ prometheus:'\\u{1F525}',cassandra:'\\u26A0',athena:'\\u{1F9E0}',sauron:'\\u{1F441}',conductor:'\\u{1F3BC}',
501
+ navi:'\\u{1F9ED}',edi:'\\u{1F4C8}',tempest:'\\u26C8',epicure:'\\u{1F37D}'
502
+ };
487
503
  function renderAgents(el){
488
504
  if(agentsList.length===0){el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading agents...</div></div>';loadAgents().then(function(){renderAgents(el)});return}
489
- var h='<div class="agents-grid">';
490
- agentsList.forEach(function(a){
491
- h+='<div class="card agent-card" onclick="openAgent(\\''+esc(a.name||a.agentName)+'\\',\\''+esc(a.displayName||a.name)+'\\')"><div class="agent-card__name">'+esc(a.displayName||a.name)+'</div><div class="agent-card__cat">'+esc(a.category||'')+'</div></div>';
505
+
506
+ // Category filter
507
+ var cats={};agentsList.forEach(function(a){var c=a.category||'other';cats[c]=(cats[c]||0)+1});
508
+ var h='<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px">';
509
+ h+='<button class="btn btn--secondary" style="font-size:11px" onclick="agentFilter=null;renderAgents(document.getElementById(\\x27content\\x27))">All ('+agentsList.length+')</button>';
510
+ Object.keys(cats).sort().forEach(function(c){
511
+ h+='<button class="btn btn--secondary" style="font-size:11px" onclick="agentFilter=\\x27'+esc(c)+'\\x27;renderAgents(document.getElementById(\\x27content\\x27))">'+esc(c)+' ('+cats[c]+')</button>';
512
+ });
513
+ h+='</div>';
514
+
515
+ var filtered=agentFilter?agentsList.filter(function(a){return a.category===agentFilter}):agentsList;
516
+
517
+ h+='<div class="agents-grid">';
518
+ filtered.forEach(function(a){
519
+ var name=a.name||a.agentName;
520
+ var display=a.displayName||name;
521
+ var icon=AGENT_ICONS[name.toLowerCase()]||'\\u{1F916}';
522
+ var tagline=a.tagline||a.description||'';
523
+ var cat=a.category||'';
524
+ h+='<div class="card agent-card" onclick="openAgent(\\''+esc(name)+'\\',\\''+esc(display)+'\\')">'+
525
+ '<div class="agent-card__icon">'+icon+'</div>'+
526
+ '<div class="agent-card__name">'+esc(display)+'</div>'+
527
+ '<div class="agent-card__tagline">'+esc(tagline.slice(0,60))+'</div>'+
528
+ '<div class="agent-card__cat">'+esc(cat)+'</div>'+
529
+ '</div>';
492
530
  });
493
531
  h+='</div>';
494
532
  el.innerHTML=h;
495
533
  }
534
+ var agentFilter=null;
496
535
  function openAgent(name,display){
497
536
  selectedAgent=name;
537
+ attachedFileContent=null;attachedFileName=null;
498
538
  document.getElementById('modalName').textContent=display||name;
499
539
  document.getElementById('modalPrompt').value='';
500
540
  document.getElementById('modalPrompt').style.display='';
501
541
  document.getElementById('modalResponse').style.display='none';
502
542
  document.getElementById('modalResponse').textContent='';
503
543
  document.getElementById('modalResponse').innerHTML='';
544
+ document.getElementById('fileInfo').style.display='none';
545
+ document.getElementById('fileDropZone').style.display='';
546
+ document.getElementById('fileDropZone').style.borderColor='var(--border2)';
547
+ document.getElementById('fileInput').value='';
504
548
  var sendBtn=document.getElementById('agentModal').querySelector('.btn--primary');
505
549
  if(sendBtn)sendBtn.style.display='';
506
550
  document.getElementById('agentModal').classList.add('modal-overlay--open');
@@ -508,12 +552,56 @@ function openAgent(name,display){
508
552
  function closeModal(){
509
553
  document.getElementById('agentModal').classList.remove('modal-overlay--open');
510
554
  }
555
+ var attachedFileContent = null;
556
+ var attachedFileName = null;
557
+
558
+ function handleFileDrop(e) {
559
+ var file = e.dataTransfer.files[0];
560
+ if (file) readFile(file);
561
+ }
562
+ function handleFileSelect(input) {
563
+ var file = input.files[0];
564
+ if (file) readFile(file);
565
+ }
566
+ function readFile(file) {
567
+ if (file.size > 500000) {
568
+ document.getElementById('fileInfo').style.display = 'block';
569
+ document.getElementById('fileInfo').textContent = 'File too large (max 500KB)';
570
+ document.getElementById('fileInfo').style.color = 'var(--red)';
571
+ return;
572
+ }
573
+ var reader = new FileReader();
574
+ reader.onload = function(e) {
575
+ attachedFileContent = e.target.result;
576
+ attachedFileName = file.name;
577
+ var info = document.getElementById('fileInfo');
578
+ info.style.display = 'block';
579
+ info.style.color = 'var(--cyan)';
580
+ info.textContent = 'Attached: ' + file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)';
581
+ document.getElementById('fileDropZone').style.borderColor = 'var(--green)';
582
+ };
583
+ reader.readAsText(file);
584
+ }
585
+
511
586
  function askAgent(){
512
587
  var p=document.getElementById('modalPrompt').value.trim();if(!p||!selectedAgent)return;
513
588
  var resp=document.getElementById('modalResponse');
514
589
  resp.style.display='block';resp.textContent='Thinking...';
515
- apiPost('/api/ask',{agent:selectedAgent,prompt:p}).then(function(r){
590
+
591
+ var payload = {agent:selectedAgent, prompt:p};
592
+ if (attachedFileContent) {
593
+ payload.fileContent = attachedFileContent;
594
+ payload.fileName = attachedFileName;
595
+ }
596
+
597
+ apiPost('/api/ask', payload).then(function(r){
516
598
  resp.textContent=(r&&r.response)||'Error: no response';
599
+ // Reset file after ask
600
+ attachedFileContent = null;
601
+ attachedFileName = null;
602
+ document.getElementById('fileInfo').style.display = 'none';
603
+ document.getElementById('fileDropZone').style.borderColor = 'var(--border2)';
604
+ document.getElementById('fileInput').value = '';
517
605
  });
518
606
  }
519
607
 
@@ -587,6 +675,11 @@ init();
587
675
  </div>
588
676
  <div class="modal__body">
589
677
  <textarea id="modalPrompt" placeholder="Ask this agent something..."></textarea>
678
+ <div id="fileDropZone" style="border:2px dashed var(--border2);border-radius:6px;padding:12px;text-align:center;color:var(--dim);font-size:11px;cursor:pointer;margin-bottom:10px;transition:border-color .2s" onclick="document.getElementById('fileInput').click()" ondragover="event.preventDefault();this.style.borderColor='var(--green)'" ondragleave="this.style.borderColor='var(--border2)'" ondrop="event.preventDefault();this.style.borderColor='var(--border2)';handleFileDrop(event)">
679
+ Drop a file here or click to attach
680
+ <input type="file" id="fileInput" style="display:none" onchange="handleFileSelect(this)">
681
+ </div>
682
+ <div id="fileInfo" style="display:none;font-size:10px;color:var(--cyan);margin-bottom:8px"></div>
590
683
  <div class="modal__response" id="modalResponse" style="display:none"></div>
591
684
  </div>
592
685
  <div class="modal__footer">