nothumanallowed 14.3.9 → 14.4.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": "14.3.9",
3
+ "version": "14.4.1",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
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 = '14.3.9';
8
+ export const VERSION = '14.4.1';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -38,6 +38,92 @@ function deleteWorkflow(id) {
38
38
  if (fs.existsSync(p)) fs.unlinkSync(p);
39
39
  }
40
40
 
41
+ /** Seed example workflows on first run */
42
+ function seedExamples() {
43
+ ensureDir();
44
+ const marker = path.join(WORKFLOWS_DIR, '.examples-seeded');
45
+ if (fs.existsSync(marker)) return;
46
+
47
+ const examples = [
48
+ {
49
+ id: 'ex_email_digest', name: '📧 Daily Email Digest',
50
+ enabled: false,
51
+ nodes: [
52
+ { id: 'n1', defId: 'trigger_cron', x: 40, y: 80, config: { schedule: '0 8 * * *' } },
53
+ { id: 'n2', defId: 'ai_summarize', x: 200, y: 80, config: { prompt: 'Summarize the last 10 unread emails concisely: {{output}}' } },
54
+ { id: 'n3', defId: 'action_slack', x: 400, y: 40, config: { channel: '#general', text: '📧 Morning Digest:\n{{output}}' } },
55
+ { id: 'n4', defId: 'action_notify', x: 400, y: 140, config: { message: 'Email digest ready', channel: 'system' } },
56
+ ],
57
+ edges: [{ from: 'n1', to: 'n2' }, { from: 'n2', to: 'n3' }, { from: 'n2', to: 'n4' }],
58
+ },
59
+ {
60
+ id: 'ex_smart_router', name: '🔀 Smart Email Router',
61
+ enabled: false,
62
+ nodes: [
63
+ { id: 'n1', defId: 'trigger_email', x: 40, y: 100, config: { filter: 'is:unread' } },
64
+ { id: 'n2', defId: 'ai_classify', x: 200, y: 100, config: { categories: 'urgent, meeting, newsletter, spam', prompt: 'Classify this email: {{output}}' } },
65
+ { id: 'n3', defId: 'logic_if', x: 380, y: 100, config: { condition: 'output.includes("urgent")' } },
66
+ { id: 'n4', defId: 'action_notify', x: 560, y: 40, config: { message: '🚨 Urgent email: {{output}}', channel: 'telegram' } },
67
+ { id: 'n5', defId: 'action_task', x: 560, y: 160, config: { title: 'Review: {{output}}', priority: 'low' } },
68
+ ],
69
+ edges: [
70
+ { from: 'n1', to: 'n2' }, { from: 'n2', to: 'n3' },
71
+ { from: 'n3', to: 'n4', fromPort: 'true' },
72
+ { from: 'n3', to: 'n5', fromPort: 'false' },
73
+ ],
74
+ },
75
+ {
76
+ id: 'ex_content_pipeline', name: '📝 Content Pipeline',
77
+ enabled: false,
78
+ nodes: [
79
+ { id: 'n1', defId: 'trigger_manual', x: 40, y: 100, config: { input: 'Write a blog post about AI agents' } },
80
+ { id: 'n2', defId: 'ai_agent', x: 200, y: 100, config: { agent: 'quill', prompt: 'Write a professional blog post about: {{output}}' } },
81
+ { id: 'n3', defId: 'ai_translate', x: 400, y: 40, config: { lang: 'Italian', prompt: '{{output}}' } },
82
+ { id: 'n4', defId: 'action_drive', x: 600, y: 40, config: { name: 'blog-it.md', content: '{{output}}' } },
83
+ { id: 'n5', defId: 'action_drive', x: 400, y: 160, config: { name: 'blog-en.md', content: '{{output}}' } },
84
+ ],
85
+ edges: [{ from: 'n1', to: 'n2' }, { from: 'n2', to: 'n3' }, { from: 'n2', to: 'n5' }, { from: 'n3', to: 'n4' }],
86
+ },
87
+ {
88
+ id: 'ex_meeting_prep', name: '📅 Meeting Prep Automation',
89
+ enabled: false,
90
+ nodes: [
91
+ { id: 'n1', defId: 'trigger_cron', x: 40, y: 100, config: { schedule: '0 7 * * 1-5' } },
92
+ { id: 'n2', defId: 'ai_agent', x: 200, y: 100, config: { agent: 'herald', prompt: 'List my meetings for today with details' } },
93
+ { id: 'n3', defId: 'logic_if', x: 380, y: 100, config: { condition: 'output.length > 20' } },
94
+ { id: 'n4', defId: 'ai_summarize', x: 540, y: 40, config: { prompt: 'Prepare a brief for each meeting. Include talking points: {{output}}' } },
95
+ { id: 'n5', defId: 'action_email', x: 720, y: 40, config: { to: 'me', subject: '📅 Meeting Prep — Today', body: '{{output}}' } },
96
+ ],
97
+ edges: [
98
+ { from: 'n1', to: 'n2' }, { from: 'n2', to: 'n3' },
99
+ { from: 'n3', to: 'n4', fromPort: 'true' },
100
+ ],
101
+ },
102
+ {
103
+ id: 'ex_web_monitor', name: '🌐 Website Monitor + Alert',
104
+ enabled: false,
105
+ nodes: [
106
+ { id: 'n1', defId: 'trigger_cron', x: 40, y: 100, config: { schedule: '*/30 * * * *' } },
107
+ { id: 'n2', defId: 'action_webhook', x: 200, y: 100, config: { url: 'https://nothumanallowed.com', method: 'GET' } },
108
+ { id: 'n3', defId: 'logic_if', x: 380, y: 100, config: { condition: 'output.includes("Error") || output.length < 100' } },
109
+ { id: 'n4', defId: 'action_notify', x: 560, y: 40, config: { message: '🚨 Website down or error detected!', channel: 'telegram' } },
110
+ { id: 'n5', defId: 'logic_error', x: 560, y: 160, config: { retries: '2', fallback: 'Check failed — site may be unreachable' } },
111
+ ],
112
+ edges: [
113
+ { from: 'n1', to: 'n2' }, { from: 'n2', to: 'n3' },
114
+ { from: 'n3', to: 'n4', fromPort: 'true' },
115
+ { from: 'n3', to: 'n5', fromPort: 'false' },
116
+ ],
117
+ },
118
+ ];
119
+
120
+ for (const wf of examples) {
121
+ const p = path.join(WORKFLOWS_DIR, `${wf.id}.json`);
122
+ if (!fs.existsSync(p)) fs.writeFileSync(p, JSON.stringify(wf, null, 2));
123
+ }
124
+ fs.writeFileSync(marker, new Date().toISOString());
125
+ }
126
+
41
127
  /** Substitute {{varName}} placeholders in a string using a context map */
42
128
  function interpolate(str, ctx) {
43
129
  if (typeof str !== 'string') return str;
@@ -61,24 +147,81 @@ function interpolateObj(obj, ctx) {
61
147
  async function executeNode(node, nodeDef, ctx, config) {
62
148
  const cfg = interpolateObj(node.config ?? {}, ctx);
63
149
 
64
- // AI nodes
150
+ // ── AI nodes ──
65
151
  if (nodeDef.type === 'ai') {
152
+ if (node.defId === 'ai_code') {
153
+ // Code node — execute JS with `input` variable
154
+ try {
155
+ const fn = new Function('input', 'output', 'ctx', cfg.code || 'return input;');
156
+ const result = fn(ctx.output || '', ctx.output || '', ctx);
157
+ return String(result ?? '');
158
+ } catch (e) { throw new Error(`Code error: ${e.message}`); }
159
+ }
66
160
  const prompt = cfg.prompt || `Process this: ${ctx.output || ''}`;
67
- const systemPrompt = cfg.systemPrompt || 'You are a helpful AI assistant. Process the input and return a concise result.';
161
+ const agentName = cfg.agent || '';
162
+ const systemPrompt = agentName
163
+ ? `You are ${agentName}, a specialist AI agent. Process the input and return a concise result.`
164
+ : 'You are a helpful AI assistant. Process the input and return a concise result.';
68
165
  const result = await callLLM(config, systemPrompt, prompt);
69
166
  return result?.content || result || '';
70
167
  }
71
168
 
72
- // Action nodes — map to executeTool actions
169
+ // ── Logic nodes ──
170
+ if (nodeDef.type === 'logic') {
171
+ if (node.defId === 'logic_if') {
172
+ try {
173
+ const fn = new Function('output', 'input', 'ctx', `return Boolean(${cfg.condition || 'false'});`);
174
+ const result = fn(ctx.output || '', ctx.output || '', ctx);
175
+ return JSON.stringify({ __branch: result ? 'true' : 'false', value: ctx.output || '' });
176
+ } catch (e) { throw new Error(`Condition error: ${e.message}`); }
177
+ }
178
+ if (node.defId === 'logic_switch') {
179
+ const expr = cfg.expression || ctx.output || '';
180
+ const cases = (cfg.cases || '').split(',').map((c) => c.trim());
181
+ const matched = cases.find((c) => expr.toLowerCase().includes(c.toLowerCase()));
182
+ return JSON.stringify({ __branch: matched || 'default', value: ctx.output || '' });
183
+ }
184
+ if (node.defId === 'logic_loop') {
185
+ const sep = cfg.separator === ',' ? ',' : '\n';
186
+ const items = (ctx.output || '').split(sep).filter(Boolean);
187
+ return JSON.stringify({ __loop: true, items, count: items.length });
188
+ }
189
+ if (node.defId === 'logic_merge') {
190
+ // Merge collects from ctx.__mergeInputs (set by the executor)
191
+ const inputs = ctx.__mergeInputs || [ctx.output || ''];
192
+ if (cfg.mode === 'json_array') return JSON.stringify(inputs);
193
+ if (cfg.mode === 'first_non_empty') return inputs.find((i) => i && i.trim()) || '';
194
+ return inputs.join('\n');
195
+ }
196
+ if (node.defId === 'logic_delay') {
197
+ const ms = Math.min(parseInt(cfg.seconds || '1') * 1000, 60_000);
198
+ await new Promise((r) => setTimeout(r, ms));
199
+ return ctx.output || '';
200
+ }
201
+ if (node.defId === 'logic_error') {
202
+ // Error handler wraps previous node execution — handled in runWorkflow
203
+ return ctx.output || cfg.fallback || '';
204
+ }
205
+ return ctx.output || '';
206
+ }
207
+
208
+ // ── Action nodes ──
73
209
  const ACTION_MAP = {
74
- action_email: ['gmail_send', { to: cfg.to, subject: cfg.subject || 'NHA Workflow', body: cfg.body || ctx.output }],
75
- action_slack: ['slack_message', { channel: cfg.channel || '#general', text: cfg.text || ctx.output }],
76
- action_calendar: ['calendar_create', { title: cfg.title || ctx.output, date: cfg.date || new Date().toISOString().split('T')[0], time: cfg.time || '09:00', duration: cfg.duration || '60' }],
77
- action_task: ['task_create', { title: cfg.title || ctx.output, priority: cfg.priority || 'medium' }],
78
- action_drive: ['drive_upload', { name: cfg.name || 'workflow-output.txt', content: cfg.content || ctx.output }],
79
- action_notion: ['notion_page', { title: cfg.title || 'Workflow Output', content: cfg.content || ctx.output }],
80
- action_github: ['github_issue', { repo: cfg.repo, title: cfg.title || ctx.output, body: cfg.body || '' }],
81
- action_webhook: ['fetch_url', { url: cfg.url, method: cfg.method || 'POST', body: cfg.body || ctx.output }],
210
+ action_email: ['gmail_send', { to: cfg.to, subject: cfg.subject || 'NHA Workflow', body: cfg.body || ctx.output }],
211
+ action_slack: ['slack_message', { channel: cfg.channel || '#general', text: cfg.text || ctx.output }],
212
+ action_calendar: ['calendar_create', { title: cfg.title || ctx.output, date: cfg.date || new Date().toISOString().split('T')[0], time: cfg.time || '09:00', duration: cfg.duration || '60' }],
213
+ action_task: ['task_create', { title: cfg.title || ctx.output, priority: cfg.priority || 'medium' }],
214
+ action_drive: ['drive_upload', { name: cfg.name || 'workflow-output.txt', content: cfg.content || ctx.output }],
215
+ action_notion: ['notion_page', { title: cfg.title || 'Workflow Output', content: cfg.content || ctx.output }],
216
+ action_github: ['github_issue', { repo: cfg.repo, title: cfg.title || ctx.output, body: cfg.body || '' }],
217
+ action_webhook: ['fetch_url', { url: cfg.url, method: cfg.method || 'POST', body: cfg.body || ctx.output }],
218
+ action_browser: ['browser_open', { url: cfg.url || ctx.output }],
219
+ action_file_read: ['file_read', { path: cfg.path }],
220
+ action_file_write: ['file_write', { path: cfg.path, content: cfg.content || ctx.output }],
221
+ action_contact: ['contact_search', { query: cfg.query || ctx.output }],
222
+ action_screen: ['screen_capture', {}],
223
+ action_maps: ['maps_directions', { from: cfg.from, to: cfg.to }],
224
+ action_notify: ['notify_remind', { message: cfg.message || ctx.output, channel: cfg.channel || 'system' }],
82
225
  };
83
226
 
84
227
  const mapped = ACTION_MAP[node.defId];
@@ -91,7 +234,7 @@ async function executeNode(node, nodeDef, ctx, config) {
91
234
  }
92
235
  }
93
236
 
94
- // Trigger nodes produce no output themselves (they're the entry point)
237
+ // Trigger nodes
95
238
  if (nodeDef.type === 'trigger') {
96
239
  return ctx.output || cfg.input || '';
97
240
  }
@@ -108,19 +251,25 @@ async function runWorkflow(wf, initialInput, config) {
108
251
  const nodeMap = Object.fromEntries(wf.nodes.map((n) => [n.id, n]));
109
252
  const defMap = Object.fromEntries((wf.nodeDefs || []).map((d) => [d.id, d]));
110
253
 
111
- // Build adjacency: from → to
254
+ // Build adjacency: from → [{to, fromPort}]
112
255
  const next = {};
113
256
  for (const e of wf.edges ?? []) {
114
257
  if (!next[e.from]) next[e.from] = [];
115
- next[e.from].push(e.to);
258
+ next[e.from].push({ to: e.to, port: e.fromPort || 'default' });
116
259
  }
117
260
 
118
- // Find start node (trigger, or first with no incoming edges)
261
+ // Check if next node is an error handler
262
+ const isErrorHandler = (nodeId) => {
263
+ const n = nodeMap[nodeId];
264
+ return n?.defId === 'logic_error';
265
+ };
266
+
267
+ // Find start nodes
119
268
  const hasIncoming = new Set((wf.edges ?? []).map((e) => e.to));
120
269
  const startCandidates = wf.nodes.filter((n) => !hasIncoming.has(n.id));
121
- if (startCandidates.length === 0) return [{ nodeId: '__error', output: 'No start node found.' }];
270
+ if (startCandidates.length === 0) return [{ nodeId: '__error', nodeLabel: 'Error', nodeIcon: '❌', output: 'No start node found.' }];
122
271
 
123
- // BFS execution
272
+ // BFS execution with branching support
124
273
  const queue = startCandidates.map((n) => ({ nodeId: n.id, ctx: { output: initialInput || '', input: initialInput || '' } }));
125
274
  const visited = new Set();
126
275
 
@@ -136,18 +285,75 @@ async function runWorkflow(wf, initialInput, config) {
136
285
 
137
286
  let output = '';
138
287
  let error = null;
139
- try {
140
- output = await executeNode(node, nodeDef, ctx, config);
141
- } catch (e) {
142
- error = e.message;
143
- output = '';
288
+
289
+ // Error handler: wrap with retry
290
+ const maxRetries = node.defId === 'logic_error' ? parseInt(node.config?.retries || '0') : 0;
291
+ let attempt = 0;
292
+ while (attempt <= maxRetries) {
293
+ try {
294
+ output = await executeNode(node, nodeDef, ctx, config);
295
+ error = null;
296
+ break;
297
+ } catch (e) {
298
+ error = e.message;
299
+ output = node.config?.fallback || '';
300
+ attempt++;
301
+ if (attempt <= maxRetries) {
302
+ steps.push({ nodeId, nodeLabel: nodeDef.label, nodeIcon: '🔄', output: `Retry ${attempt}/${maxRetries}: ${e.message}`, error: null });
303
+ }
304
+ }
144
305
  }
145
306
 
146
- steps.push({ nodeId, nodeLabel: nodeDef.label, nodeIcon: nodeDef.icon, output, error });
307
+ steps.push({ nodeId, nodeLabel: nodeDef.label, nodeIcon: nodeDef.icon, output: output?.slice?.(0, 2000) || '', error });
147
308
 
148
- const nextCtx = { ...ctx, output, [`${nodeDef.id}_output`]: output };
149
- for (const toId of next[nodeId] ?? []) {
150
- queue.push({ nodeId: toId, ctx: nextCtx });
309
+ // If error and no error handler downstream, stop this branch
310
+ if (error) {
311
+ const hasHandler = (next[nodeId] ?? []).some((n) => isErrorHandler(n.to));
312
+ if (!hasHandler) continue;
313
+ }
314
+
315
+ // Determine which downstream nodes to execute based on branching
316
+ const downstream = next[nodeId] ?? [];
317
+ let branch = null;
318
+ try {
319
+ const parsed = JSON.parse(output || '{}');
320
+ if (parsed.__branch) branch = parsed.__branch;
321
+ if (parsed.__loop && Array.isArray(parsed.items)) {
322
+ // Loop: execute downstream for each item
323
+ for (const item of parsed.items) {
324
+ const loopCtx = { ...ctx, output: item, loopItem: item };
325
+ for (const { to: toId } of downstream) {
326
+ // Don't mark as visited — allow loop iterations
327
+ const toNode = nodeMap[toId];
328
+ if (!toNode) continue;
329
+ const toDef = defMap[toNode.defId];
330
+ if (!toDef) continue;
331
+ try {
332
+ const loopOut = await executeNode(toNode, toDef, loopCtx, config);
333
+ steps.push({ nodeId: toId, nodeLabel: `${toDef.label} [${item.slice(0, 20)}]`, nodeIcon: toDef.icon, output: loopOut?.slice?.(0, 2000) || '', error: null });
334
+ } catch (e) {
335
+ steps.push({ nodeId: toId, nodeLabel: `${toDef.label} [${item.slice(0, 20)}]`, nodeIcon: toDef.icon, output: '', error: e.message });
336
+ }
337
+ }
338
+ }
339
+ continue; // Loop handles its own downstream
340
+ }
341
+ } catch { /* not JSON — no branching */ }
342
+
343
+ const nextCtx = { ...ctx, output: output || '', [`${node.defId}_output`]: output || '' };
344
+
345
+ if (branch) {
346
+ // Branching: only follow edges that match the branch port
347
+ for (const { to: toId, port } of downstream) {
348
+ if (port === branch || port === 'default') {
349
+ queue.push({ nodeId: toId, ctx: nextCtx });
350
+ }
351
+ }
352
+ } else {
353
+ // Normal: follow all downstream edges
354
+ for (const { to: toId } of downstream) {
355
+ queue.push({ nodeId: toId, ctx: nextCtx });
356
+ }
151
357
  }
152
358
  }
153
359
 
@@ -158,6 +364,7 @@ export function register(router) {
158
364
  // GET /api/workflows — list all workflows
159
365
  router.get('/api/workflows', async (req, res) => {
160
366
  try {
367
+ seedExamples();
161
368
  sendJSON(res, 200, { workflows: listWorkflows() });
162
369
  } catch (e) {
163
370
  sendError(res, 500, e.message);
@@ -1033,7 +1033,8 @@ RULES:
1033
1033
 
1034
1034
  const FILE_PLAN_SYSTEM = `You are the lead architect of a 200-person engineering team. Design an ENTERPRISE-GRADE file structure.
1035
1035
  Output ONLY a JSON array: [{"name":"path/to/file.ext","purpose":"detailed description","tokens":N}]
1036
- where "tokens" is your estimate of content tokens (300-800 small, 1000-2500 medium, 2500-5000 large).
1036
+ where "tokens" is your estimate of content tokens (200-600 small, 600-1500 medium, 1500-3000 large).
1037
+ CRITICAL: No single file should exceed 3000 tokens. Split large files into smaller modules.
1037
1038
 
1038
1039
  MANDATORY STRUCTURE (every project MUST have):
1039
1040
  - package.json (with ALL dependencies: express, helmet, cors, compression, morgan, bcryptjs, jsonwebtoken, express-rate-limit, cookie-parser, dotenv)
@@ -1062,7 +1063,11 @@ MANDATORY STRUCTURE (every project MUST have):
1062
1063
  - public/js/animations.js (intersection observer, scroll effects)
1063
1064
 
1064
1065
  RULES:
1065
- - Generate 25-45 files — real enterprise projects have many files
1066
+ - Generate 30-50 files — many small files are better than few large ones
1067
+ - NEVER generate a file larger than 200 lines. Split into components/partials instead
1068
+ - HTML pages: use <script src="js/page.js"> and <link href="css/page.css"> — NOT inline
1069
+ - CSS: one file per concern (max 150 lines each), NOT one giant file
1070
+ - JS: one file per feature (max 150 lines each)
1066
1071
  - Token estimates must be realistic: CSS files 1500-3000, JS files 1000-2000, HTML pages 2000-4000
1067
1072
  - Use relative paths only
1068
1073
  - No explanation, no markdown, ONLY the JSON array.`;
@@ -1285,9 +1290,9 @@ Design a COMPLETE production-ready file structure. Include ALL files needed for
1285
1290
  })
1286
1291
  .join('\n\n');
1287
1292
 
1288
- // Generous max_tokens — enterprise files are large
1289
- const estimatedTokens = fileSpec.tokens || 2000;
1290
- const maxTokens = Math.min(Math.max(estimatedTokens * 3, 4000), 16384);
1293
+ // max_tokens scaled to file size smaller files = less truncation risk
1294
+ const estimatedTokens = fileSpec.tokens || 1500;
1295
+ const maxTokens = Math.min(Math.max(estimatedTokens * 2, 2000), 8192);
1291
1296
 
1292
1297
  const fileSys = `You are a team of 200 senior full-stack developers generating ENTERPRISE-GRADE production code.
1293
1298
 
@@ -1321,7 +1326,9 @@ FRONTEND STANDARDS:
1321
1326
  - Accessible: aria-labels, focus styles, keyboard navigation, alt text
1322
1327
  - Professional typography: system font stack, proper hierarchy (clamp() for fluid sizes)
1323
1328
  - CSS Grid/Flexbox layouts — no floats
1324
- - At minimum 500 lines for main CSS files, 200+ lines for page JS files`;
1329
+ - Each file must be COMPLETE and SELF-CONTAINED no truncation
1330
+ - Maximum 200 lines per file. If more content needed, split into separate files
1331
+ - HTML: external CSS/JS via link/script tags, NOT inline styles/scripts exceeding 20 lines`;
1325
1332
 
1326
1333
  const filePrompt = `Project: ${projectName}
1327
1334
  Description: ${description}