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 +1 -1
- package/src/cli.mjs +6 -0
- package/src/commands/scan.mjs +187 -0
- package/src/commands/ui.mjs +14 -2
- package/src/services/web-ui.mjs +104 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "
|
|
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
|
+
}
|
package/src/commands/ui.mjs
CHANGED
|
@@ -651,8 +651,20 @@ export async function cmdUI(args) {
|
|
|
651
651
|
}
|
|
652
652
|
} catch { /* context loading failed, proceed without it */ }
|
|
653
653
|
|
|
654
|
-
|
|
655
|
-
|
|
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);
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -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(
|
|
126
|
-
@media(min-width:
|
|
127
|
-
.
|
|
128
|
-
.agent-card:
|
|
129
|
-
.agent-
|
|
130
|
-
.agent-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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">
|