nothumanallowed 2.1.0 → 3.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": "2.1.0",
3
+ "version": "3.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
@@ -13,6 +13,7 @@ import { cmdPlan } from './commands/plan.mjs';
13
13
  import { cmdTasks } from './commands/tasks.mjs';
14
14
  import { cmdOps } from './commands/ops.mjs';
15
15
  import { cmdChat } from './commands/chat.mjs';
16
+ import { cmdUI } from './commands/ui.mjs';
16
17
  import { cmdGoogle } from './commands/google-auth.mjs';
17
18
  import { banner, info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, M, B, R } from './ui.mjs';
18
19
 
@@ -58,6 +59,9 @@ export async function main(argv) {
58
59
  case 'chat':
59
60
  return cmdChat(args);
60
61
 
62
+ case 'ui':
63
+ return cmdUI(args);
64
+
61
65
  case 'google':
62
66
  return cmdGoogle(args);
63
67
 
@@ -355,6 +359,8 @@ function cmdHelp() {
355
359
  console.log(` run "prompt" ${D}--agents saber,zero${NC} Collaborate with specific agents\n`);
356
360
 
357
361
  console.log(` ${C}Daily Operations${NC} ${D}(Gmail + Calendar + Tasks)${NC}`);
362
+ console.log(` ui Open local web dashboard (http://127.0.0.1:3847)`);
363
+ console.log(` ui --port=4000 Custom port ui --no-browser Don't auto-open`);
358
364
  console.log(` chat Interactive chat — manage email/calendar/tasks naturally`);
359
365
  console.log(` plan Generate daily plan (5 agents analyze your day)`);
360
366
  console.log(` plan --refresh Regenerate today's plan`);
@@ -0,0 +1,632 @@
1
+ /**
2
+ * nha ui — Local web interface for NHA operations.
3
+ *
4
+ * Starts a zero-dependency HTTP server on localhost:3847 serving a single-page
5
+ * operations console with REST API endpoints that reuse existing services.
6
+ *
7
+ * Zero npm dependencies — Node.js 22 native http module only.
8
+ */
9
+
10
+ import http from 'http';
11
+ import { exec } from 'child_process';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { loadConfig } from '../config.mjs';
15
+ import { callLLM, callAgent, parseAgentFile } from '../services/llm.mjs';
16
+ import { getUnreadImportant } from '../services/google-gmail.mjs';
17
+ import { getTodayEvents } from '../services/google-calendar.mjs';
18
+ import {
19
+ getTasks,
20
+ addTask,
21
+ completeTask,
22
+ getDayStats,
23
+ } from '../services/task-store.mjs';
24
+ import { runPlanningPipeline } from '../services/ops-pipeline.mjs';
25
+ import { AGENTS, AGENTS_DIR, NHA_DIR, VERSION } from '../constants.mjs';
26
+ import { getHTML } from '../services/web-ui.mjs';
27
+ import { info, ok, fail, warn, C, G, D, NC, BOLD } from '../ui.mjs';
28
+
29
+ // ── Constants ──────────────────────────────────────────────────────────────
30
+
31
+ const DEFAULT_PORT = 3847;
32
+ const HOST = '127.0.0.1';
33
+
34
+ // ── Chat system prompt (reused from chat.mjs) ──────────────────────────────
35
+
36
+ const TOOL_DEFINITIONS = `
37
+ You have access to the following tools. When the user's message requires an action,
38
+ output EXACTLY ONE fenced JSON block per action:
39
+
40
+ \`\`\`json
41
+ {"action": "<tool_name>", "params": { ... }}
42
+ \`\`\`
43
+
44
+ You may include conversational text BEFORE or AFTER the JSON block. If no action
45
+ is needed, respond normally without any JSON block.
46
+
47
+ TOOLS:
48
+
49
+ 1. gmail_list(query: string, maxResults?: number)
50
+ Search emails. query uses Gmail search syntax.
51
+
52
+ 2. gmail_read(messageId: string)
53
+ Read the full body of an email by its ID.
54
+
55
+ 3. gmail_send(to: string, subject: string, body: string)
56
+ Send an email. ALWAYS confirm with the user before sending.
57
+
58
+ 4. gmail_draft(to: string, subject: string, body: string)
59
+ Create a draft email (safe — does not send).
60
+
61
+ 5. gmail_reply(messageId: string, body: string)
62
+ Reply to an existing email thread. ALWAYS confirm before sending.
63
+
64
+ 6. calendar_today()
65
+ List all events for today.
66
+
67
+ 7. calendar_upcoming(hours?: number)
68
+ List upcoming events in the next N hours (default 2).
69
+
70
+ 8. calendar_create(summary: string, start: string, end: string, attendees?: string[], description?: string)
71
+ Create a calendar event. start/end are ISO 8601 datetime strings.
72
+
73
+ 9. task_list()
74
+ List today's tasks.
75
+
76
+ 10. task_add(description: string, priority?: "low"|"medium"|"high"|"critical", due?: string)
77
+ Add a new task for today.
78
+
79
+ 11. task_done(id: number)
80
+ Mark a task as completed.
81
+
82
+ RULES:
83
+ - For search/read operations, execute immediately and present results conversationally.
84
+ - For write/send/delete operations, describe what you're about to do and include the JSON block.
85
+ - When presenting results, format them clearly in natural language. Never dump raw JSON to the user.
86
+ - If you need multiple actions in sequence, do them ONE AT A TIME.
87
+ - Dates: today is {{TODAY}}. The user's timezone is {{TIMEZONE}}.
88
+ `.trim();
89
+
90
+ // ── Reuse the executeTool and parseActions logic from chat.mjs ──────────
91
+
92
+ import {
93
+ listMessages,
94
+ getMessage,
95
+ sendEmail,
96
+ createDraft,
97
+ } from '../services/google-gmail.mjs';
98
+
99
+ import {
100
+ getUpcomingEvents,
101
+ createEvent,
102
+ } from '../services/google-calendar.mjs';
103
+
104
+ function parseActions(text) {
105
+ const actions = [];
106
+ const textParts = [];
107
+ const fenceRegex = /```json\s*\n?([\s\S]*?)```/g;
108
+ let lastIndex = 0;
109
+ let match;
110
+
111
+ while ((match = fenceRegex.exec(text)) !== null) {
112
+ const before = text.slice(lastIndex, match.index).trim();
113
+ if (before) textParts.push(before);
114
+ try {
115
+ const parsed = JSON.parse(match[1].trim());
116
+ if (parsed.action && typeof parsed.action === 'string') {
117
+ actions.push({ action: parsed.action, params: parsed.params || {} });
118
+ }
119
+ } catch {
120
+ textParts.push(match[0]);
121
+ }
122
+ lastIndex = match.index + match[0].length;
123
+ }
124
+
125
+ const trailing = text.slice(lastIndex).trim();
126
+ if (trailing) textParts.push(trailing);
127
+ return { textParts, actions };
128
+ }
129
+
130
+ async function executeTool(action, params, config) {
131
+ switch (action) {
132
+ case 'gmail_list': {
133
+ const query = params.query || 'is:unread';
134
+ const max = params.maxResults || 10;
135
+ const refs = await listMessages(config, query, max);
136
+ if (refs.length === 0) return 'No emails found matching that query.';
137
+ const messages = [];
138
+ for (const ref of refs.slice(0, max)) {
139
+ try { messages.push(await getMessage(config, ref.id)); } catch {}
140
+ }
141
+ return messages.map((m, i) =>
142
+ `${i + 1}. [${m.id}] From: ${m.from} | Subject: ${m.subject} | Date: ${m.date}\n ${m.snippet.slice(0, 120)}`
143
+ ).join('\n');
144
+ }
145
+ case 'gmail_read': {
146
+ const msg = await getMessage(config, params.messageId);
147
+ return `From: ${msg.from}\nTo: ${msg.to}\nSubject: ${msg.subject}\nDate: ${msg.date}\n---\n${msg.body.slice(0, 4000)}`;
148
+ }
149
+ case 'gmail_send': {
150
+ await sendEmail(config, params.to, params.subject, params.body);
151
+ return `Email sent to ${params.to} with subject "${params.subject}".`;
152
+ }
153
+ case 'gmail_draft': {
154
+ await createDraft(config, params.to, params.subject, params.body);
155
+ return `Draft created for ${params.to} with subject "${params.subject}".`;
156
+ }
157
+ case 'gmail_reply': {
158
+ const original = await getMessage(config, params.messageId);
159
+ await sendEmail(config, original.from, `Re: ${original.subject}`, params.body, {
160
+ replyToMessageId: original.id,
161
+ threadId: original.threadId,
162
+ });
163
+ return `Reply sent to ${original.from} on thread "${original.subject}".`;
164
+ }
165
+ case 'calendar_today': {
166
+ const events = await getTodayEvents(config);
167
+ if (events.length === 0) return 'No events scheduled for today.';
168
+ return events.map((e, i) => {
169
+ const time = e.isAllDay ? 'All day' : `${fmtTime(e.start)} - ${fmtTime(e.end)}`;
170
+ return `${i + 1}. ${time} — ${e.summary}${e.location ? ' @ ' + e.location : ''}`;
171
+ }).join('\n');
172
+ }
173
+ case 'calendar_upcoming': {
174
+ const hours = params.hours || 2;
175
+ const events = await getUpcomingEvents(config, hours);
176
+ if (events.length === 0) return `No events in the next ${hours} hour(s).`;
177
+ return events.map((e, i) => {
178
+ const time = e.isAllDay ? 'All day' : `${fmtTime(e.start)} - ${fmtTime(e.end)}`;
179
+ return `${i + 1}. ${time} — ${e.summary}`;
180
+ }).join('\n');
181
+ }
182
+ case 'calendar_create': {
183
+ await createEvent(config, {
184
+ summary: params.summary,
185
+ start: params.start,
186
+ end: params.end,
187
+ description: params.description || '',
188
+ attendees: params.attendees || [],
189
+ });
190
+ return `Event "${params.summary}" created for ${fmtTime(params.start)} - ${fmtTime(params.end)}.`;
191
+ }
192
+ case 'task_list': {
193
+ const tasks = getTasks();
194
+ if (tasks.length === 0) return 'No tasks for today.';
195
+ return tasks.map(t =>
196
+ `#${t.id} [${t.priority}] ${t.status === 'done' ? '[DONE] ' : ''}${t.description}${t.due ? ' (due: ' + t.due + ')' : ''}`
197
+ ).join('\n');
198
+ }
199
+ case 'task_add': {
200
+ const task = addTask({
201
+ description: params.description,
202
+ priority: params.priority || 'medium',
203
+ due: params.due || null,
204
+ source: 'chat',
205
+ });
206
+ return `Task #${task.id} added: "${task.description}" [${task.priority}]`;
207
+ }
208
+ case 'task_done': {
209
+ const success = completeTask(params.id);
210
+ return success ? `Task #${params.id} marked as done.` : `Task #${params.id} not found.`;
211
+ }
212
+ default:
213
+ return `Unknown action: ${action}`;
214
+ }
215
+ }
216
+
217
+ function fmtTime(isoStr) {
218
+ try {
219
+ return new Date(isoStr).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
220
+ } catch { return isoStr; }
221
+ }
222
+
223
+ // ── Agent loader ──────────────────────────────────────────────────────────
224
+
225
+ function loadAgentCards() {
226
+ const cards = [];
227
+ for (const name of AGENTS) {
228
+ const file = path.join(AGENTS_DIR, `${name}.mjs`);
229
+ let card = { name, category: 'agent', tagline: '' };
230
+ try {
231
+ if (fs.existsSync(file)) {
232
+ const source = fs.readFileSync(file, 'utf-8');
233
+ const parsed = parseAgentFile(source, name);
234
+ card = { name, ...parsed.card };
235
+ }
236
+ } catch {}
237
+ cards.push(card);
238
+ }
239
+ return cards;
240
+ }
241
+
242
+ // ── Plan file loader ──────────────────────────────────────────────────────
243
+
244
+ function loadTodayPlan() {
245
+ const dateStr = new Date().toISOString().split('T')[0];
246
+ const planFile = path.join(NHA_DIR, 'ops', 'plans', `${dateStr}.json`);
247
+ if (fs.existsSync(planFile)) {
248
+ try { return JSON.parse(fs.readFileSync(planFile, 'utf-8')); }
249
+ catch { return null; }
250
+ }
251
+ return null;
252
+ }
253
+
254
+ // ── HTTP Helpers ──────────────────────────────────────────────────────────
255
+
256
+ function sendJSON(res, statusCode, data) {
257
+ const body = JSON.stringify(data);
258
+ res.writeHead(statusCode, {
259
+ 'Content-Type': 'application/json',
260
+ 'Access-Control-Allow-Origin': '*',
261
+ 'Access-Control-Allow-Methods': 'GET,POST,PATCH,OPTIONS',
262
+ 'Access-Control-Allow-Headers': 'Content-Type',
263
+ 'Cache-Control': 'no-cache',
264
+ });
265
+ res.end(body);
266
+ }
267
+
268
+ function sendHTML(res, html) {
269
+ res.writeHead(200, {
270
+ 'Content-Type': 'text/html; charset=utf-8',
271
+ 'Cache-Control': 'no-cache',
272
+ });
273
+ res.end(html);
274
+ }
275
+
276
+ function parseBody(req) {
277
+ return new Promise((resolve, reject) => {
278
+ const chunks = [];
279
+ let size = 0;
280
+ const MAX = 1_048_576; // 1 MB
281
+ req.on('data', chunk => {
282
+ size += chunk.length;
283
+ if (size > MAX) { reject(new Error('Body too large')); req.destroy(); return; }
284
+ chunks.push(chunk);
285
+ });
286
+ req.on('end', () => {
287
+ try {
288
+ const raw = Buffer.concat(chunks).toString('utf-8');
289
+ resolve(raw ? JSON.parse(raw) : {});
290
+ } catch (e) { reject(e); }
291
+ });
292
+ req.on('error', reject);
293
+ });
294
+ }
295
+
296
+ // ── Open browser ──────────────────────────────────────────────────────────
297
+
298
+ function openBrowser(url) {
299
+ const platform = process.platform;
300
+ const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
301
+ exec(`${cmd} ${url}`, () => {});
302
+ }
303
+
304
+ // ── Request logger ──────────────────────────────────────────────────────────
305
+
306
+ function logRequest(method, url, statusCode, durationMs) {
307
+ const color = statusCode < 400 ? G : '\x1b[0;31m';
308
+ console.log(` ${D}${new Date().toISOString().slice(11,19)}${NC} ${color}${statusCode}${NC} ${method.padEnd(6)} ${url} ${D}${durationMs}ms${NC}`);
309
+ }
310
+
311
+ // ── Server ───────────────────────────────────────────────────────────────
312
+
313
+ export async function cmdUI(args) {
314
+ // Parse port flag
315
+ let port = DEFAULT_PORT;
316
+ let noBrowser = false;
317
+ for (const arg of args) {
318
+ if (arg.startsWith('--port=')) {
319
+ port = parseInt(arg.split('=')[1], 10) || DEFAULT_PORT;
320
+ } else if (arg === '--no-browser') {
321
+ noBrowser = true;
322
+ }
323
+ }
324
+
325
+ const config = loadConfig();
326
+ const htmlPage = getHTML(port);
327
+
328
+ // Pre-load agent cards once at startup
329
+ const agentCards = loadAgentCards();
330
+
331
+ // Chat session state (persists across requests while server is running)
332
+ const chatSystemPrompt = (() => {
333
+ const today = new Date().toISOString().split('T')[0];
334
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
335
+ let prompt = TOOL_DEFINITIONS.replace('{{TODAY}}', today).replace('{{TIMEZONE}}', tz);
336
+ prompt += `\n\nYou are NHA Chat, a personal operations assistant inside the NotHumanAllowed web UI. ` +
337
+ `You help the user manage their emails, calendar, and tasks through natural conversation. ` +
338
+ `Be concise, helpful, and proactive. When presenting data, format it clearly. ` +
339
+ `Never output raw JSON to the user.`;
340
+ return prompt;
341
+ })();
342
+
343
+ // ── Route Handlers ──────────────────────────────────────────────────────
344
+
345
+ async function handleRequest(req, res) {
346
+ const start = Date.now();
347
+ const url = new URL(req.url, `http://${HOST}:${port}`);
348
+ const pathname = url.pathname;
349
+ const method = req.method;
350
+
351
+ // CORS preflight
352
+ if (method === 'OPTIONS') {
353
+ res.writeHead(204, {
354
+ 'Access-Control-Allow-Origin': '*',
355
+ 'Access-Control-Allow-Methods': 'GET,POST,PATCH,OPTIONS',
356
+ 'Access-Control-Allow-Headers': 'Content-Type',
357
+ });
358
+ res.end();
359
+ return;
360
+ }
361
+
362
+ try {
363
+ // ── Serve HTML page ─────────────────────────────────────────────
364
+ if (method === 'GET' && (pathname === '/' || pathname === '/index.html')) {
365
+ sendHTML(res, htmlPage);
366
+ logRequest(method, pathname, 200, Date.now() - start);
367
+ return;
368
+ }
369
+
370
+ // ── Favicon (no-content) ──────────────────────────────────────────
371
+ if (pathname === '/favicon.ico') {
372
+ res.writeHead(204);
373
+ res.end();
374
+ return;
375
+ }
376
+
377
+ // ── API Routes ────────────────────────────────────────────────────
378
+
379
+ // GET /api/status
380
+ if (method === 'GET' && pathname === '/api/status') {
381
+ sendJSON(res, 200, {
382
+ connected: true,
383
+ version: VERSION,
384
+ provider: config.llm.provider,
385
+ hasApiKey: !!config.llm.apiKey,
386
+ hasGoogle: !!config.google?.clientId,
387
+ agentName: config.agent?.name || null,
388
+ timestamp: new Date().toISOString(),
389
+ });
390
+ logRequest(method, pathname, 200, Date.now() - start);
391
+ return;
392
+ }
393
+
394
+ // GET /api/emails
395
+ if (method === 'GET' && pathname === '/api/emails') {
396
+ try {
397
+ const emails = await getUnreadImportant(config, 20);
398
+ sendJSON(res, 200, { emails });
399
+ } catch (e) {
400
+ sendJSON(res, 200, { emails: [], error: e.message });
401
+ }
402
+ logRequest(method, pathname, 200, Date.now() - start);
403
+ return;
404
+ }
405
+
406
+ // GET /api/calendar
407
+ if (method === 'GET' && pathname === '/api/calendar') {
408
+ try {
409
+ const events = await getTodayEvents(config);
410
+ sendJSON(res, 200, { events });
411
+ } catch (e) {
412
+ sendJSON(res, 200, { events: [], error: e.message });
413
+ }
414
+ logRequest(method, pathname, 200, Date.now() - start);
415
+ return;
416
+ }
417
+
418
+ // GET /api/tasks
419
+ if (method === 'GET' && pathname === '/api/tasks') {
420
+ const tasks = getTasks();
421
+ const stats = getDayStats();
422
+ sendJSON(res, 200, { tasks, stats });
423
+ logRequest(method, pathname, 200, Date.now() - start);
424
+ return;
425
+ }
426
+
427
+ // POST /api/tasks
428
+ if (method === 'POST' && pathname === '/api/tasks') {
429
+ const body = await parseBody(req);
430
+ if (!body.description) {
431
+ sendJSON(res, 400, { error: 'description required' });
432
+ logRequest(method, pathname, 400, Date.now() - start);
433
+ return;
434
+ }
435
+ const task = addTask({
436
+ description: body.description,
437
+ priority: body.priority || 'medium',
438
+ due: body.due || null,
439
+ source: 'web-ui',
440
+ });
441
+ sendJSON(res, 201, { task });
442
+ logRequest(method, pathname, 201, Date.now() - start);
443
+ return;
444
+ }
445
+
446
+ // PATCH /api/tasks/:id/done
447
+ const taskDoneMatch = pathname.match(/^\/api\/tasks\/(\d+)\/done$/);
448
+ if (method === 'PATCH' && taskDoneMatch) {
449
+ const taskId = parseInt(taskDoneMatch[1], 10);
450
+ const success = completeTask(taskId);
451
+ sendJSON(res, 200, { ok: success, id: taskId });
452
+ logRequest(method, pathname, 200, Date.now() - start);
453
+ return;
454
+ }
455
+
456
+ // GET /api/plan
457
+ if (method === 'GET' && pathname === '/api/plan') {
458
+ const plan = loadTodayPlan();
459
+ sendJSON(res, 200, { plan });
460
+ logRequest(method, pathname, 200, Date.now() - start);
461
+ return;
462
+ }
463
+
464
+ // POST /api/plan/refresh
465
+ if (method === 'POST' && pathname === '/api/plan/refresh') {
466
+ try {
467
+ const plan = await runPlanningPipeline(config, { refresh: true });
468
+ sendJSON(res, 200, { plan });
469
+ } catch (e) {
470
+ sendJSON(res, 500, { error: e.message });
471
+ }
472
+ logRequest(method, pathname, 200, Date.now() - start);
473
+ return;
474
+ }
475
+
476
+ // POST /api/chat
477
+ if (method === 'POST' && pathname === '/api/chat') {
478
+ const body = await parseBody(req);
479
+ if (!body.message) {
480
+ sendJSON(res, 400, { error: 'message required' });
481
+ logRequest(method, pathname, 400, Date.now() - start);
482
+ return;
483
+ }
484
+
485
+ if (!config.llm.apiKey) {
486
+ sendJSON(res, 200, { response: 'No API key configured. Run: nha config set key YOUR_KEY', error: 'no_api_key' });
487
+ logRequest(method, pathname, 200, Date.now() - start);
488
+ return;
489
+ }
490
+
491
+ // Build message with history
492
+ const history = body.history || [];
493
+ const parts = [];
494
+ for (const turn of history) {
495
+ const prefix = turn.role === 'user' ? '[User]' : '[Assistant]';
496
+ parts.push(`${prefix} ${turn.content}`);
497
+ }
498
+ parts.push(`[User] ${body.message}`);
499
+ const userMessage = parts.join('\n\n');
500
+
501
+ try {
502
+ const response = await callLLM(config, chatSystemPrompt, userMessage);
503
+ const { textParts, actions } = parseActions(response);
504
+ const textResponse = textParts.join('\n\n');
505
+
506
+ // Execute non-destructive tool actions automatically
507
+ let toolResult = null;
508
+ if (actions.length > 0) {
509
+ const { action, params } = actions[0];
510
+ // Skip destructive actions in web UI for safety (gmail_send, gmail_reply, calendar_create)
511
+ const safeActions = new Set([
512
+ 'gmail_list', 'gmail_read', 'gmail_draft',
513
+ 'calendar_today', 'calendar_upcoming',
514
+ 'task_list', 'task_add', 'task_done',
515
+ ]);
516
+ if (safeActions.has(action)) {
517
+ try {
518
+ toolResult = await executeTool(action, params, config);
519
+ } catch (e) {
520
+ toolResult = `Error: ${e.message}`;
521
+ }
522
+ } else {
523
+ toolResult = `[Action "${action}" requires confirmation. Use nha chat in terminal for write operations.]`;
524
+ }
525
+ }
526
+
527
+ sendJSON(res, 200, { response: textResponse, toolResult, actions });
528
+ } catch (e) {
529
+ sendJSON(res, 200, { response: null, error: e.message });
530
+ }
531
+ logRequest(method, pathname, 200, Date.now() - start);
532
+ return;
533
+ }
534
+
535
+ // GET /api/agents
536
+ if (method === 'GET' && pathname === '/api/agents') {
537
+ sendJSON(res, 200, { agents: agentCards });
538
+ logRequest(method, pathname, 200, Date.now() - start);
539
+ return;
540
+ }
541
+
542
+ // POST /api/ask
543
+ if (method === 'POST' && pathname === '/api/ask') {
544
+ const body = await parseBody(req);
545
+ if (!body.agent || !body.prompt) {
546
+ sendJSON(res, 400, { error: 'agent and prompt required' });
547
+ logRequest(method, pathname, 400, Date.now() - start);
548
+ return;
549
+ }
550
+
551
+ if (!config.llm.apiKey) {
552
+ sendJSON(res, 200, { response: null, error: 'No API key configured.' });
553
+ logRequest(method, pathname, 200, Date.now() - start);
554
+ return;
555
+ }
556
+
557
+ if (!AGENTS.includes(body.agent)) {
558
+ sendJSON(res, 400, { error: `Unknown agent: ${body.agent}` });
559
+ logRequest(method, pathname, 400, Date.now() - start);
560
+ return;
561
+ }
562
+
563
+ try {
564
+ const response = await callAgent(config, body.agent, body.prompt);
565
+ sendJSON(res, 200, { response, agent: body.agent });
566
+ } catch (e) {
567
+ sendJSON(res, 200, { response: null, error: e.message });
568
+ }
569
+ logRequest(method, pathname, 200, Date.now() - start);
570
+ return;
571
+ }
572
+
573
+ // ── 404 ──────────────────────────────────────────────────────────
574
+ sendJSON(res, 404, { error: 'Not found' });
575
+ logRequest(method, pathname, 404, Date.now() - start);
576
+
577
+ } catch (err) {
578
+ console.error(` \x1b[0;31mServer error:\x1b[0m`, err.message);
579
+ try {
580
+ sendJSON(res, 500, { error: 'Internal server error' });
581
+ } catch {}
582
+ logRequest(method, pathname, 500, Date.now() - start);
583
+ }
584
+ }
585
+
586
+ // ── Start Server ────────────────────────────────────────────────────────
587
+
588
+ const server = http.createServer(handleRequest);
589
+
590
+ server.on('error', (err) => {
591
+ if (err.code === 'EADDRINUSE') {
592
+ fail(`Port ${port} is already in use. Try: nha ui --port=${port + 1}`);
593
+ process.exit(1);
594
+ }
595
+ fail(`Server error: ${err.message}`);
596
+ process.exit(1);
597
+ });
598
+
599
+ server.listen(port, HOST, () => {
600
+ const url = `http://${HOST}:${port}`;
601
+
602
+ console.log('');
603
+ console.log(` ${BOLD}${C}NHA Local Operations Console${NC}`);
604
+ console.log(` ${D}Zero-dependency web interface for your daily ops${NC}`);
605
+ console.log('');
606
+ console.log(` ${G}Server running at:${NC} ${url}`);
607
+ console.log(` ${D}Provider:${NC} ${config.llm.provider || 'not set'}`);
608
+ console.log(` ${D}API Key:${NC} ${config.llm.apiKey ? config.llm.apiKey.slice(0, 12) + '...' : '\x1b[0;31mnot set\x1b[0m'}`);
609
+ console.log(` ${D}Agents loaded:${NC} ${agentCards.length}`);
610
+ console.log('');
611
+ console.log(` ${D}Press Ctrl+C to stop${NC}`);
612
+ console.log('');
613
+
614
+ if (!noBrowser) {
615
+ openBrowser(url);
616
+ }
617
+ });
618
+
619
+ // Graceful shutdown
620
+ const shutdown = () => {
621
+ console.log(`\n ${D}Shutting down...${NC}`);
622
+ server.close(() => {
623
+ console.log(` ${D}Server stopped.${NC}\n`);
624
+ process.exit(0);
625
+ });
626
+ // Force exit after 3s if connections hang
627
+ setTimeout(() => process.exit(0), 3000);
628
+ };
629
+
630
+ process.on('SIGINT', shutdown);
631
+ process.on('SIGTERM', shutdown);
632
+ }
package/src/constants.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
3
 
4
- export const VERSION = '2.1.0';
4
+ export const VERSION = '3.0.0';
5
5
  export const BASE_URL = 'https://nothumanallowed.com/cli';
6
6
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
7
7