nothumanallowed 13.5.200 → 14.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/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.200';
8
+ export const VERSION = '14.0.1';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -0,0 +1,299 @@
1
+ /**
2
+ * NHA UI Server — autonomous backend for the React UI.
3
+ *
4
+ * Zero extra npm dependencies — Node.js 22 http module only.
5
+ * Serves the React build (ui-dist/) statically + all /api/* routes.
6
+ *
7
+ * Architecture:
8
+ * server/
9
+ * ├── index.mjs ← this file (HTTP server + router)
10
+ * ├── routes/ ← one file per domain
11
+ * │ ├── chat.mjs
12
+ * │ ├── studio.mjs
13
+ * │ ├── email.mjs
14
+ * │ ├── calendar.mjs
15
+ * │ ├── tasks.mjs
16
+ * │ ├── config.mjs
17
+ * │ ├── agents.mjs
18
+ * │ ├── integrations.mjs
19
+ * │ ├── drive.mjs
20
+ * │ ├── collab.mjs
21
+ * │ └── google-auth.mjs
22
+ * └── ws.mjs ← WebSocket handler
23
+ */
24
+
25
+ import http from 'http';
26
+ import fs from 'fs';
27
+ import path from 'path';
28
+ import { fileURLToPath } from 'url';
29
+ import { exec } from 'child_process';
30
+
31
+ // ── Paths ────────────────────────────────────────────────────────────────────
32
+
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
+
35
+ // Fallback to local dist/ if run standalone (e.g. during development)
36
+ const _cliDist = path.resolve(__dirname, '../ui-dist');
37
+ const _localDist = path.resolve(__dirname, '../dist');
38
+ const UI_DIST = fs.existsSync(path.join(_cliDist, 'index.html')) ? _cliDist : _localDist;
39
+
40
+ // ── MIME types ───────────────────────────────────────────────────────────────
41
+
42
+ const MIME = {
43
+ '.html': 'text/html; charset=utf-8',
44
+ '.js': 'application/javascript; charset=utf-8',
45
+ '.css': 'text/css; charset=utf-8',
46
+ '.svg': 'image/svg+xml',
47
+ '.png': 'image/png',
48
+ '.ico': 'image/x-icon',
49
+ '.woff2': 'font/woff2',
50
+ '.woff': 'font/woff',
51
+ '.ttf': 'font/ttf',
52
+ '.json': 'application/json',
53
+ '.map': 'application/json',
54
+ '.jpg': 'image/jpeg',
55
+ '.jpeg': 'image/jpeg',
56
+ '.webp': 'image/webp',
57
+ };
58
+
59
+ // ── HTTP helpers ─────────────────────────────────────────────────────────────
60
+
61
+ export function sendJSON(res, status, data) {
62
+ const body = JSON.stringify(data);
63
+ res.writeHead(status, {
64
+ 'Content-Type': 'application/json',
65
+ 'Access-Control-Allow-Origin': '*',
66
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
67
+ 'Access-Control-Allow-Headers': 'Content-Type,Authorization',
68
+ 'Cache-Control': 'no-cache',
69
+ });
70
+ res.end(body);
71
+ }
72
+
73
+ export function sendError(res, status, message) {
74
+ sendJSON(res, status, { error: message });
75
+ }
76
+
77
+ export function parseBody(req, maxBytes = 10_485_760) {
78
+ return new Promise((resolve, reject) => {
79
+ const chunks = [];
80
+ let size = 0;
81
+ req.on('data', chunk => {
82
+ size += chunk.length;
83
+ if (size > maxBytes) { reject(new Error('Body too large')); req.destroy(); return; }
84
+ chunks.push(chunk);
85
+ });
86
+ req.on('end', () => {
87
+ try {
88
+ const raw = Buffer.concat(chunks).toString('utf-8');
89
+ resolve(raw ? JSON.parse(raw) : {});
90
+ } catch (e) { reject(e); }
91
+ });
92
+ req.on('error', reject);
93
+ });
94
+ }
95
+
96
+ export function sendSSE(res) {
97
+ res.writeHead(200, {
98
+ 'Content-Type': 'text/event-stream',
99
+ 'Cache-Control': 'no-cache',
100
+ 'Connection': 'keep-alive',
101
+ 'Access-Control-Allow-Origin': '*',
102
+ });
103
+ return {
104
+ send: (data) => res.write(`data: ${JSON.stringify(data)}\n\n`),
105
+ end: () => { res.write('data: [DONE]\n\n'); res.end(); },
106
+ };
107
+ }
108
+
109
+ function serveStatic(res, filePath) {
110
+ try {
111
+ const data = fs.readFileSync(filePath);
112
+ const ext = path.extname(filePath);
113
+ const ct = MIME[ext] || 'application/octet-stream';
114
+ const cache = ext === '.html' ? 'no-store' : 'public, max-age=31536000, immutable';
115
+ res.writeHead(200, { 'Content-Type': ct, 'Cache-Control': cache });
116
+ res.end(data);
117
+ return true;
118
+ } catch { return false; }
119
+ }
120
+
121
+ function logRequest(method, url, status, ms) {
122
+ const G = '\x1b[0;32m', D = '\x1b[2m', NC = '\x1b[0m', R = '\x1b[0;31m';
123
+ const color = status < 400 ? G : R;
124
+ console.log(` ${D}${new Date().toISOString().slice(11,19)}${NC} ${color}${status}${NC} ${method.padEnd(6)} ${url} ${D}${ms}ms${NC}`);
125
+ }
126
+
127
+ function openBrowser(url) {
128
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
129
+ exec(`${cmd} ${url}`, () => {});
130
+ }
131
+
132
+ // ── Route registry ───────────────────────────────────────────────────────────
133
+
134
+ // Each route module exports: { register(router) }
135
+ // router = { get, post, put, delete, patch } — simple prefix matcher
136
+
137
+ class Router {
138
+ constructor() { this._routes = []; }
139
+
140
+ _add(method, pattern, handler) {
141
+ this._routes.push({ method: method.toUpperCase(), pattern, handler });
142
+ }
143
+ get(p, h) { this._add('GET', p, h); }
144
+ post(p, h) { this._add('POST', p, h); }
145
+ put(p, h) { this._add('PUT', p, h); }
146
+ patch(p, h) { this._add('PATCH', p, h); }
147
+ delete(p, h) { this._add('DELETE', p, h); }
148
+
149
+ match(method, pathname) {
150
+ for (const r of this._routes) {
151
+ if (r.method !== method) continue;
152
+ if (typeof r.pattern === 'string') {
153
+ if (r.pattern === pathname) return { handler: r.handler, params: {} };
154
+ } else if (r.pattern instanceof RegExp) {
155
+ const m = pathname.match(r.pattern);
156
+ if (m) return { handler: r.handler, params: m.groups || {}, match: m };
157
+ }
158
+ }
159
+ return null;
160
+ }
161
+
162
+ // prefix match — used for routes that need to match /api/foo/*
163
+ matchPrefix(method, pathname) {
164
+ for (const r of this._routes) {
165
+ if (r.method !== method) continue;
166
+ if (typeof r.pattern === 'string' && pathname.startsWith(r.pattern + '/')) {
167
+ return { handler: r.handler, params: {} };
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+ }
173
+
174
+ async function buildRouter() {
175
+ const router = new Router();
176
+ const mods = await Promise.all([
177
+ import('./routes/config.mjs'),
178
+ import('./routes/chat.mjs'),
179
+ import('./routes/studio.mjs'),
180
+ import('./routes/webcraft.mjs'),
181
+ import('./routes/email.mjs'),
182
+ import('./routes/calendar.mjs'),
183
+ import('./routes/tasks.mjs'),
184
+ import('./routes/agents.mjs'),
185
+ import('./routes/drive.mjs'),
186
+ import('./routes/integrations.mjs'),
187
+ import('./routes/collab.mjs'),
188
+ import('./routes/google-auth.mjs'),
189
+ ]);
190
+ for (const mod of mods) mod.register(router);
191
+ return router;
192
+ }
193
+
194
+ // ── Main request handler ─────────────────────────────────────────────────────
195
+
196
+ let _router = null; // initialized in startServer()
197
+
198
+ async function handleRequest(req, res) {
199
+ const router = _router;
200
+ const start = Date.now();
201
+ const url = new URL(req.url, `http://localhost`);
202
+ const { pathname } = url;
203
+ const method = req.method;
204
+
205
+ // CORS preflight
206
+ if (method === 'OPTIONS') {
207
+ res.writeHead(204, {
208
+ 'Access-Control-Allow-Origin': '*',
209
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
210
+ 'Access-Control-Allow-Headers': 'Content-Type,Authorization',
211
+ });
212
+ res.end();
213
+ return;
214
+ }
215
+
216
+ try {
217
+ // ── API routes ──────────────────────────────────────────────────────
218
+ if (pathname.startsWith('/api/')) {
219
+ const match = router.match(method, pathname);
220
+ if (match) {
221
+ req.params = match.params;
222
+ req.query = Object.fromEntries(url.searchParams);
223
+ await match.handler(req, res);
224
+ logRequest(method, pathname, res.statusCode || 200, Date.now() - start);
225
+ return;
226
+ }
227
+ // prefix-based routes (for dynamic segments like /api/agents/:id)
228
+ // handled inside route modules via req.params injected above
229
+ sendError(res, 404, `Unknown API route: ${method} ${pathname}`);
230
+ logRequest(method, pathname, 404, Date.now() - start);
231
+ return;
232
+ }
233
+
234
+ // ── Suppress browser probes ─────────────────────────────────────────
235
+ if (pathname.startsWith('/.well-known')) {
236
+ res.writeHead(204); res.end(); return;
237
+ }
238
+
239
+ // ── Serve React build (static files) ───────────────────────────────
240
+ if (method === 'GET') {
241
+ if (fs.existsSync(path.join(UI_DIST, 'index.html'))) {
242
+ const file = pathname === '/' ? 'index.html' : pathname.slice(1);
243
+ if (serveStatic(res, path.join(UI_DIST, file))) {
244
+ logRequest(method, pathname, 200, Date.now() - start);
245
+ return;
246
+ }
247
+ // SPA fallback
248
+ serveStatic(res, path.join(UI_DIST, 'index.html'));
249
+ logRequest(method, pathname, 200, Date.now() - start);
250
+ return;
251
+ }
252
+ // No build yet
253
+ res.writeHead(503, { 'Content-Type': 'text/html; charset=utf-8' });
254
+ res.end(`<!DOCTYPE html><html><body style="font-family:monospace;padding:2rem;background:#0a0a0a;color:#e5e5e5">
255
+ <h2 style="color:#22c55e">NHA UI — build not found</h2>
256
+ <p>Run: <code style="background:#1a1a1a;padding:2px 8px;border-radius:4px">cd packages/nha-ui &amp;&amp; pnpm build</code></p>
257
+ </body></html>`);
258
+ logRequest(method, pathname, 503, Date.now() - start);
259
+ return;
260
+ }
261
+
262
+ res.writeHead(404); res.end();
263
+ } catch (e) {
264
+ console.error('[server] Error handling request:', e);
265
+ if (!res.headersSent) sendError(res, 500, e.message);
266
+ logRequest(method, pathname, 500, Date.now() - start);
267
+ }
268
+ }
269
+
270
+ // ── Start ────────────────────────────────────────────────────────────────────
271
+
272
+ export async function startServer({ port = 3847, host = '127.0.0.1', noBrowser = false } = {}) {
273
+ _router = await buildRouter();
274
+ const { setupWebSocket } = await import('./ws.mjs');
275
+
276
+ const server = http.createServer(handleRequest);
277
+ setupWebSocket(server);
278
+
279
+ await new Promise((resolve, reject) => {
280
+ server.listen(port, host, (err) => err ? reject(err) : resolve());
281
+ });
282
+
283
+ const G = '\x1b[0;32m', NC = '\x1b[0m', D = '\x1b[2m', BOLD = '\x1b[1m';
284
+ const { VERSION } = await import('../constants.mjs');
285
+ console.log(`\n ${BOLD}${G}NHA${NC} ${D}v${VERSION}${NC}`);
286
+ console.log(` ${G}✓${NC} Server running on ${G}http://${host}:${port}${NC}`);
287
+ console.log(` ${D}Press Ctrl+C to stop${NC}\n`);
288
+
289
+ // Telemetry ping — fire and forget
290
+ fetch('https://nothumanallowed.com/api/v1/telemetry/ping', {
291
+ method: 'POST',
292
+ headers: { 'Content-Type': 'application/json' },
293
+ body: JSON.stringify({ platform: 'nha-ui', version: VERSION }),
294
+ }).catch(() => {});
295
+
296
+ if (!noBrowser) openBrowser(`http://localhost:${port}`);
297
+
298
+ return server;
299
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Agents routes — /api/agents CRUD + /api/ask/stream
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { sendJSON, sendError, parseBody } from '../index.mjs';
8
+ import { loadConfig } from '../../config.mjs';
9
+ import { AGENTS_DIR, NHA_DIR } from '../../constants.mjs';
10
+ import { callLLMStream, parseAgentFile } from '../../services/llm.mjs';
11
+
12
+ function loadAgentCards() {
13
+ if (!fs.existsSync(AGENTS_DIR)) return [];
14
+ return fs.readdirSync(AGENTS_DIR)
15
+ .filter(f => f.endsWith('.mjs'))
16
+ .map(f => {
17
+ try {
18
+ const src = fs.readFileSync(path.join(AGENTS_DIR, f), 'utf-8');
19
+ return parseAgentFile(src, f.replace('.mjs',''));
20
+ } catch { return null; }
21
+ })
22
+ .filter(Boolean);
23
+ }
24
+
25
+ export function register(router) {
26
+ router.get('/api/agents', (_req, res) => {
27
+ sendJSON(res, 200, { agents: loadAgentCards() });
28
+ });
29
+
30
+ router.post('/api/agents', async (req, res) => {
31
+ try {
32
+ const body = await parseBody(req);
33
+ if (!body.name || !body.systemPrompt) return sendError(res, 400, 'name and systemPrompt required');
34
+ fs.mkdirSync(AGENTS_DIR, { recursive: true });
35
+ const slug = body.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
36
+ const file = path.join(AGENTS_DIR, `${slug}.mjs`);
37
+ if (fs.existsSync(file)) return sendError(res, 409, 'Agent already exists');
38
+ const content = `// Agent: ${body.name}\nexport const agent = {\n name: "${body.name}",\n icon: "${body.icon || '🤖'}",\n description: "${(body.description || '').replace(/"/g,'\\\"')}",\n systemPrompt: \`${body.systemPrompt.replace(/`/g,'\\`')}\`,\n};\nexport default agent;\n`;
39
+ fs.writeFileSync(file, content, 'utf-8');
40
+ sendJSON(res, 201, { ok: true, slug });
41
+ } catch (e) { sendError(res, 500, e.message); }
42
+ });
43
+
44
+ const AGENT_RE = /^\/api\/agents\/([a-z0-9-]+)$/;
45
+
46
+ router.get(AGENT_RE, (req, res) => {
47
+ const slug = req.url.match(AGENT_RE)?.[1];
48
+ const file = path.join(AGENTS_DIR, `${slug}.mjs`);
49
+ if (!fs.existsSync(file)) return sendError(res, 404, 'Agent not found');
50
+ try {
51
+ const src = fs.readFileSync(file, 'utf-8');
52
+ sendJSON(res, 200, { agent: parseAgentFile(src, slug) });
53
+ } catch (e) { sendError(res, 500, e.message); }
54
+ });
55
+
56
+ router.put(AGENT_RE, async (req, res) => {
57
+ try {
58
+ const slug = req.url.match(AGENT_RE)?.[1];
59
+ const file = path.join(AGENTS_DIR, `${slug}.mjs`);
60
+ if (!fs.existsSync(file)) return sendError(res, 404, 'Agent not found');
61
+ const body = await parseBody(req);
62
+ const content = `// Agent: ${body.name || slug}\nexport const agent = {\n name: "${body.name || slug}",\n icon: "${body.icon || '🤖'}",\n description: "${(body.description || '').replace(/"/g,'\\\"')}",\n systemPrompt: \`${(body.systemPrompt || '').replace(/`/g,'\\`')}\`,\n};\nexport default agent;\n`;
63
+ fs.writeFileSync(file, content, 'utf-8');
64
+ sendJSON(res, 200, { ok: true });
65
+ } catch (e) { sendError(res, 500, e.message); }
66
+ });
67
+
68
+ router.delete(AGENT_RE, (req, res) => {
69
+ const slug = req.url.match(AGENT_RE)?.[1];
70
+ const file = path.join(AGENTS_DIR, `${slug}.mjs`);
71
+ if (!fs.existsSync(file)) return sendError(res, 404, 'Agent not found');
72
+ fs.unlinkSync(file);
73
+ sendJSON(res, 200, { ok: true });
74
+ });
75
+
76
+ // POST /api/ask/stream — single agent streaming ask
77
+ router.post('/api/ask/stream', async (req, res) => {
78
+ const body = await parseBody(req);
79
+ if (!body.message) return sendError(res, 400, 'message required');
80
+ const config = loadConfig();
81
+
82
+ res.writeHead(200, {
83
+ 'Content-Type': 'text/event-stream',
84
+ 'Cache-Control': 'no-cache',
85
+ 'Connection': 'keep-alive',
86
+ 'Access-Control-Allow-Origin': '*',
87
+ });
88
+ const sse = (ev, data) => res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`);
89
+
90
+ try {
91
+ const agentSlug = body.agent?.toLowerCase();
92
+ let sysProm = 'You are a helpful AI assistant.';
93
+ if (agentSlug) {
94
+ const af = path.join(AGENTS_DIR, `${agentSlug}.mjs`);
95
+ if (fs.existsSync(af)) {
96
+ const parsed = parseAgentFile(fs.readFileSync(af, 'utf-8'), agentSlug);
97
+ if (parsed.systemPrompt) sysProm = parsed.systemPrompt;
98
+ }
99
+ }
100
+ await callLLMStream(config, sysProm, body.message, (tok) => sse('token', { content: tok }));
101
+ sse('done', {});
102
+ res.write('data: [DONE]\n\n');
103
+ res.end();
104
+ } catch (e) {
105
+ sse('error', { message: e.message });
106
+ res.end();
107
+ }
108
+ });
109
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Calendar + Tasks + Plan routes
3
+ */
4
+
5
+ import { sendJSON, sendError, parseBody } from '../index.mjs';
6
+ import { loadConfig } from '../../config.mjs';
7
+ import { getTodayEvents, getUpcomingEvents, createEvent, updateEvent, deleteEvent, getEventsForDate } from '../../services/mail-router.mjs';
8
+ import { getTasks, addTask, completeTask, getDayStats } from '../../services/task-store.mjs';
9
+ import { runPlanningPipeline } from '../../services/ops-pipeline.mjs';
10
+ import { NHA_DIR } from '../../constants.mjs';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+
14
+ export function register(router) {
15
+ // ── Calendar ──────────────────────────────────────────────────────────
16
+
17
+ router.get('/api/calendar', async (req, res) => {
18
+ try {
19
+ const config = loadConfig();
20
+ const url = new URL(req.url, 'http://localhost');
21
+ const date = url.searchParams.get('date');
22
+ const events = date ? await getEventsForDate(config, date) : await getTodayEvents(config);
23
+ sendJSON(res, 200, { events });
24
+ } catch (e) {
25
+ const msg = e.message || '';
26
+ if (msg.includes('No mail provider') || msg.includes('not authenticated') || msg.includes('No Google') || msg.includes('token')) {
27
+ return sendJSON(res, 200, { events: [], authRequired: true, error: msg });
28
+ }
29
+ sendError(res, 500, msg);
30
+ }
31
+ });
32
+
33
+ router.get('/api/calendar/upcoming', async (req, res) => {
34
+ try {
35
+ const config = loadConfig();
36
+ const url = new URL(req.url, 'http://localhost');
37
+ const hours = parseInt(url.searchParams.get('hours') || '24', 10);
38
+ const events = await getUpcomingEvents(config, hours);
39
+ sendJSON(res, 200, { events });
40
+ } catch (e) {
41
+ const msg = e.message || '';
42
+ if (msg.includes('No mail provider') || msg.includes('not authenticated') || msg.includes('token')) {
43
+ return sendJSON(res, 200, { events: [], authRequired: true });
44
+ }
45
+ sendError(res, 500, msg);
46
+ }
47
+ });
48
+
49
+ router.post('/api/calendar', async (req, res) => {
50
+ try {
51
+ const body = await parseBody(req);
52
+ const config = loadConfig();
53
+ if (body.action === 'update' && body.id) {
54
+ const updated = await updateEvent(config, body.id, body);
55
+ return sendJSON(res, 200, { event: updated });
56
+ }
57
+ if (body.action === 'delete' && body.id) {
58
+ await deleteEvent(config, body.id);
59
+ return sendJSON(res, 200, { ok: true });
60
+ }
61
+ const event = await createEvent(config, body);
62
+ sendJSON(res, 201, { event });
63
+ } catch (e) { sendError(res, 500, e.message); }
64
+ });
65
+
66
+ // ── Tasks ─────────────────────────────────────────────────────────────
67
+
68
+ router.get('/api/tasks', async (req, res) => {
69
+ try {
70
+ const url = new URL(req.url, 'http://localhost');
71
+ const date = url.searchParams.get('date');
72
+ const tasks = getTasks(date);
73
+ sendJSON(res, 200, { tasks, stats: getDayStats(date) });
74
+ } catch (e) { sendError(res, 500, e.message); }
75
+ });
76
+
77
+ router.post('/api/tasks', async (req, res) => {
78
+ try {
79
+ const body = await parseBody(req);
80
+ if (body.action === 'complete') {
81
+ completeTask(body.id);
82
+ return sendJSON(res, 200, { ok: true });
83
+ }
84
+ if (body.action === 'clear') {
85
+ const { clearTasks } = await import('../../services/task-store.mjs');
86
+ clearTasks(body.date);
87
+ return sendJSON(res, 200, { ok: true });
88
+ }
89
+ const task = addTask(body);
90
+ sendJSON(res, 201, { task });
91
+ } catch (e) { sendError(res, 500, e.message); }
92
+ });
93
+
94
+ // ── Daily Plan ────────────────────────────────────────────────────────
95
+
96
+ router.get('/api/plan', async (_req, res) => {
97
+ try {
98
+ const dateStr = new Date().toISOString().split('T')[0];
99
+ const planFile = path.join(NHA_DIR, 'ops', 'plans', `${dateStr}.json`);
100
+ if (fs.existsSync(planFile)) {
101
+ sendJSON(res, 200, JSON.parse(fs.readFileSync(planFile, 'utf-8')));
102
+ } else {
103
+ sendJSON(res, 200, { plan: null });
104
+ }
105
+ } catch (e) { sendError(res, 500, e.message); }
106
+ });
107
+
108
+ router.post('/api/plan/refresh', async (_req, res) => {
109
+ try {
110
+ const config = loadConfig();
111
+ const plan = await runPlanningPipeline(config);
112
+ sendJSON(res, 200, { plan });
113
+ } catch (e) { sendError(res, 500, e.message); }
114
+ });
115
+ }