nothumanallowed 3.8.0 → 4.0.0

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.0",
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);
@@ -475,10 +475,11 @@ function openDayDetail(dateStr){
475
475
  // Use the agent modal for day detail
476
476
  document.getElementById('modalName').textContent=dayLabel;
477
477
  document.getElementById('modalPrompt').style.display='none';
478
+ document.getElementById('fileDropZone').style.display='none';
479
+ document.getElementById('fileInfo').style.display='none';
478
480
  document.getElementById('modalResponse').style.display='block';
479
481
  document.getElementById('modalResponse').innerHTML=h;
480
482
  document.getElementById('agentModal').classList.add('modal-overlay--open');
481
- // Hide ask button
482
483
  var sendBtn=document.getElementById('agentModal').querySelector('.btn--primary');
483
484
  if(sendBtn)sendBtn.style.display='none';
484
485
  }
@@ -495,12 +496,17 @@ function renderAgents(el){
495
496
  }
496
497
  function openAgent(name,display){
497
498
  selectedAgent=name;
499
+ attachedFileContent=null;attachedFileName=null;
498
500
  document.getElementById('modalName').textContent=display||name;
499
501
  document.getElementById('modalPrompt').value='';
500
502
  document.getElementById('modalPrompt').style.display='';
501
503
  document.getElementById('modalResponse').style.display='none';
502
504
  document.getElementById('modalResponse').textContent='';
503
505
  document.getElementById('modalResponse').innerHTML='';
506
+ document.getElementById('fileInfo').style.display='none';
507
+ document.getElementById('fileDropZone').style.display='';
508
+ document.getElementById('fileDropZone').style.borderColor='var(--border2)';
509
+ document.getElementById('fileInput').value='';
504
510
  var sendBtn=document.getElementById('agentModal').querySelector('.btn--primary');
505
511
  if(sendBtn)sendBtn.style.display='';
506
512
  document.getElementById('agentModal').classList.add('modal-overlay--open');
@@ -508,12 +514,56 @@ function openAgent(name,display){
508
514
  function closeModal(){
509
515
  document.getElementById('agentModal').classList.remove('modal-overlay--open');
510
516
  }
517
+ var attachedFileContent = null;
518
+ var attachedFileName = null;
519
+
520
+ function handleFileDrop(e) {
521
+ var file = e.dataTransfer.files[0];
522
+ if (file) readFile(file);
523
+ }
524
+ function handleFileSelect(input) {
525
+ var file = input.files[0];
526
+ if (file) readFile(file);
527
+ }
528
+ function readFile(file) {
529
+ if (file.size > 500000) {
530
+ document.getElementById('fileInfo').style.display = 'block';
531
+ document.getElementById('fileInfo').textContent = 'File too large (max 500KB)';
532
+ document.getElementById('fileInfo').style.color = 'var(--red)';
533
+ return;
534
+ }
535
+ var reader = new FileReader();
536
+ reader.onload = function(e) {
537
+ attachedFileContent = e.target.result;
538
+ attachedFileName = file.name;
539
+ var info = document.getElementById('fileInfo');
540
+ info.style.display = 'block';
541
+ info.style.color = 'var(--cyan)';
542
+ info.textContent = 'Attached: ' + file.name + ' (' + (file.size / 1024).toFixed(1) + ' KB)';
543
+ document.getElementById('fileDropZone').style.borderColor = 'var(--green)';
544
+ };
545
+ reader.readAsText(file);
546
+ }
547
+
511
548
  function askAgent(){
512
549
  var p=document.getElementById('modalPrompt').value.trim();if(!p||!selectedAgent)return;
513
550
  var resp=document.getElementById('modalResponse');
514
551
  resp.style.display='block';resp.textContent='Thinking...';
515
- apiPost('/api/ask',{agent:selectedAgent,prompt:p}).then(function(r){
552
+
553
+ var payload = {agent:selectedAgent, prompt:p};
554
+ if (attachedFileContent) {
555
+ payload.fileContent = attachedFileContent;
556
+ payload.fileName = attachedFileName;
557
+ }
558
+
559
+ apiPost('/api/ask', payload).then(function(r){
516
560
  resp.textContent=(r&&r.response)||'Error: no response';
561
+ // Reset file after ask
562
+ attachedFileContent = null;
563
+ attachedFileName = null;
564
+ document.getElementById('fileInfo').style.display = 'none';
565
+ document.getElementById('fileDropZone').style.borderColor = 'var(--border2)';
566
+ document.getElementById('fileInput').value = '';
517
567
  });
518
568
  }
519
569
 
@@ -587,6 +637,11 @@ init();
587
637
  </div>
588
638
  <div class="modal__body">
589
639
  <textarea id="modalPrompt" placeholder="Ask this agent something..."></textarea>
640
+ <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)">
641
+ Drop a file here or click to attach
642
+ <input type="file" id="fileInput" style="display:none" onchange="handleFileSelect(this)">
643
+ </div>
644
+ <div id="fileInfo" style="display:none;font-size:10px;color:var(--cyan);margin-bottom:8px"></div>
590
645
  <div class="modal__response" id="modalResponse" style="display:none"></div>
591
646
  </div>
592
647
  <div class="modal__footer">