nothumanallowed 14.3.8 → 14.4.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": "14.3.8",
3
+ "version": "14.4.0",
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.8';
8
+ export const VERSION = '14.4.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -61,24 +61,81 @@ function interpolateObj(obj, ctx) {
61
61
  async function executeNode(node, nodeDef, ctx, config) {
62
62
  const cfg = interpolateObj(node.config ?? {}, ctx);
63
63
 
64
- // AI nodes
64
+ // ── AI nodes ──
65
65
  if (nodeDef.type === 'ai') {
66
+ if (node.defId === 'ai_code') {
67
+ // Code node — execute JS with `input` variable
68
+ try {
69
+ const fn = new Function('input', 'output', 'ctx', cfg.code || 'return input;');
70
+ const result = fn(ctx.output || '', ctx.output || '', ctx);
71
+ return String(result ?? '');
72
+ } catch (e) { throw new Error(`Code error: ${e.message}`); }
73
+ }
66
74
  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.';
75
+ const agentName = cfg.agent || '';
76
+ const systemPrompt = agentName
77
+ ? `You are ${agentName}, a specialist AI agent. Process the input and return a concise result.`
78
+ : 'You are a helpful AI assistant. Process the input and return a concise result.';
68
79
  const result = await callLLM(config, systemPrompt, prompt);
69
80
  return result?.content || result || '';
70
81
  }
71
82
 
72
- // Action nodes — map to executeTool actions
83
+ // ── Logic nodes ──
84
+ if (nodeDef.type === 'logic') {
85
+ if (node.defId === 'logic_if') {
86
+ try {
87
+ const fn = new Function('output', 'input', 'ctx', `return Boolean(${cfg.condition || 'false'});`);
88
+ const result = fn(ctx.output || '', ctx.output || '', ctx);
89
+ return JSON.stringify({ __branch: result ? 'true' : 'false', value: ctx.output || '' });
90
+ } catch (e) { throw new Error(`Condition error: ${e.message}`); }
91
+ }
92
+ if (node.defId === 'logic_switch') {
93
+ const expr = cfg.expression || ctx.output || '';
94
+ const cases = (cfg.cases || '').split(',').map((c) => c.trim());
95
+ const matched = cases.find((c) => expr.toLowerCase().includes(c.toLowerCase()));
96
+ return JSON.stringify({ __branch: matched || 'default', value: ctx.output || '' });
97
+ }
98
+ if (node.defId === 'logic_loop') {
99
+ const sep = cfg.separator === ',' ? ',' : '\n';
100
+ const items = (ctx.output || '').split(sep).filter(Boolean);
101
+ return JSON.stringify({ __loop: true, items, count: items.length });
102
+ }
103
+ if (node.defId === 'logic_merge') {
104
+ // Merge collects from ctx.__mergeInputs (set by the executor)
105
+ const inputs = ctx.__mergeInputs || [ctx.output || ''];
106
+ if (cfg.mode === 'json_array') return JSON.stringify(inputs);
107
+ if (cfg.mode === 'first_non_empty') return inputs.find((i) => i && i.trim()) || '';
108
+ return inputs.join('\n');
109
+ }
110
+ if (node.defId === 'logic_delay') {
111
+ const ms = Math.min(parseInt(cfg.seconds || '1') * 1000, 60_000);
112
+ await new Promise((r) => setTimeout(r, ms));
113
+ return ctx.output || '';
114
+ }
115
+ if (node.defId === 'logic_error') {
116
+ // Error handler wraps previous node execution — handled in runWorkflow
117
+ return ctx.output || cfg.fallback || '';
118
+ }
119
+ return ctx.output || '';
120
+ }
121
+
122
+ // ── Action nodes ──
73
123
  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 }],
124
+ action_email: ['gmail_send', { to: cfg.to, subject: cfg.subject || 'NHA Workflow', body: cfg.body || ctx.output }],
125
+ action_slack: ['slack_message', { channel: cfg.channel || '#general', text: cfg.text || ctx.output }],
126
+ 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' }],
127
+ action_task: ['task_create', { title: cfg.title || ctx.output, priority: cfg.priority || 'medium' }],
128
+ action_drive: ['drive_upload', { name: cfg.name || 'workflow-output.txt', content: cfg.content || ctx.output }],
129
+ action_notion: ['notion_page', { title: cfg.title || 'Workflow Output', content: cfg.content || ctx.output }],
130
+ action_github: ['github_issue', { repo: cfg.repo, title: cfg.title || ctx.output, body: cfg.body || '' }],
131
+ action_webhook: ['fetch_url', { url: cfg.url, method: cfg.method || 'POST', body: cfg.body || ctx.output }],
132
+ action_browser: ['browser_open', { url: cfg.url || ctx.output }],
133
+ action_file_read: ['file_read', { path: cfg.path }],
134
+ action_file_write: ['file_write', { path: cfg.path, content: cfg.content || ctx.output }],
135
+ action_contact: ['contact_search', { query: cfg.query || ctx.output }],
136
+ action_screen: ['screen_capture', {}],
137
+ action_maps: ['maps_directions', { from: cfg.from, to: cfg.to }],
138
+ action_notify: ['notify_remind', { message: cfg.message || ctx.output, channel: cfg.channel || 'system' }],
82
139
  };
83
140
 
84
141
  const mapped = ACTION_MAP[node.defId];
@@ -91,7 +148,7 @@ async function executeNode(node, nodeDef, ctx, config) {
91
148
  }
92
149
  }
93
150
 
94
- // Trigger nodes produce no output themselves (they're the entry point)
151
+ // Trigger nodes
95
152
  if (nodeDef.type === 'trigger') {
96
153
  return ctx.output || cfg.input || '';
97
154
  }
@@ -108,19 +165,25 @@ async function runWorkflow(wf, initialInput, config) {
108
165
  const nodeMap = Object.fromEntries(wf.nodes.map((n) => [n.id, n]));
109
166
  const defMap = Object.fromEntries((wf.nodeDefs || []).map((d) => [d.id, d]));
110
167
 
111
- // Build adjacency: from → to
168
+ // Build adjacency: from → [{to, fromPort}]
112
169
  const next = {};
113
170
  for (const e of wf.edges ?? []) {
114
171
  if (!next[e.from]) next[e.from] = [];
115
- next[e.from].push(e.to);
172
+ next[e.from].push({ to: e.to, port: e.fromPort || 'default' });
116
173
  }
117
174
 
118
- // Find start node (trigger, or first with no incoming edges)
175
+ // Check if next node is an error handler
176
+ const isErrorHandler = (nodeId) => {
177
+ const n = nodeMap[nodeId];
178
+ return n?.defId === 'logic_error';
179
+ };
180
+
181
+ // Find start nodes
119
182
  const hasIncoming = new Set((wf.edges ?? []).map((e) => e.to));
120
183
  const startCandidates = wf.nodes.filter((n) => !hasIncoming.has(n.id));
121
- if (startCandidates.length === 0) return [{ nodeId: '__error', output: 'No start node found.' }];
184
+ if (startCandidates.length === 0) return [{ nodeId: '__error', nodeLabel: 'Error', nodeIcon: '❌', output: 'No start node found.' }];
122
185
 
123
- // BFS execution
186
+ // BFS execution with branching support
124
187
  const queue = startCandidates.map((n) => ({ nodeId: n.id, ctx: { output: initialInput || '', input: initialInput || '' } }));
125
188
  const visited = new Set();
126
189
 
@@ -136,18 +199,75 @@ async function runWorkflow(wf, initialInput, config) {
136
199
 
137
200
  let output = '';
138
201
  let error = null;
139
- try {
140
- output = await executeNode(node, nodeDef, ctx, config);
141
- } catch (e) {
142
- error = e.message;
143
- output = '';
202
+
203
+ // Error handler: wrap with retry
204
+ const maxRetries = node.defId === 'logic_error' ? parseInt(node.config?.retries || '0') : 0;
205
+ let attempt = 0;
206
+ while (attempt <= maxRetries) {
207
+ try {
208
+ output = await executeNode(node, nodeDef, ctx, config);
209
+ error = null;
210
+ break;
211
+ } catch (e) {
212
+ error = e.message;
213
+ output = node.config?.fallback || '';
214
+ attempt++;
215
+ if (attempt <= maxRetries) {
216
+ steps.push({ nodeId, nodeLabel: nodeDef.label, nodeIcon: '🔄', output: `Retry ${attempt}/${maxRetries}: ${e.message}`, error: null });
217
+ }
218
+ }
144
219
  }
145
220
 
146
- steps.push({ nodeId, nodeLabel: nodeDef.label, nodeIcon: nodeDef.icon, output, error });
221
+ steps.push({ nodeId, nodeLabel: nodeDef.label, nodeIcon: nodeDef.icon, output: output?.slice?.(0, 2000) || '', error });
222
+
223
+ // If error and no error handler downstream, stop this branch
224
+ if (error) {
225
+ const hasHandler = (next[nodeId] ?? []).some((n) => isErrorHandler(n.to));
226
+ if (!hasHandler) continue;
227
+ }
147
228
 
148
- const nextCtx = { ...ctx, output, [`${nodeDef.id}_output`]: output };
149
- for (const toId of next[nodeId] ?? []) {
150
- queue.push({ nodeId: toId, ctx: nextCtx });
229
+ // Determine which downstream nodes to execute based on branching
230
+ const downstream = next[nodeId] ?? [];
231
+ let branch = null;
232
+ try {
233
+ const parsed = JSON.parse(output || '{}');
234
+ if (parsed.__branch) branch = parsed.__branch;
235
+ if (parsed.__loop && Array.isArray(parsed.items)) {
236
+ // Loop: execute downstream for each item
237
+ for (const item of parsed.items) {
238
+ const loopCtx = { ...ctx, output: item, loopItem: item };
239
+ for (const { to: toId } of downstream) {
240
+ // Don't mark as visited — allow loop iterations
241
+ const toNode = nodeMap[toId];
242
+ if (!toNode) continue;
243
+ const toDef = defMap[toNode.defId];
244
+ if (!toDef) continue;
245
+ try {
246
+ const loopOut = await executeNode(toNode, toDef, loopCtx, config);
247
+ steps.push({ nodeId: toId, nodeLabel: `${toDef.label} [${item.slice(0, 20)}]`, nodeIcon: toDef.icon, output: loopOut?.slice?.(0, 2000) || '', error: null });
248
+ } catch (e) {
249
+ steps.push({ nodeId: toId, nodeLabel: `${toDef.label} [${item.slice(0, 20)}]`, nodeIcon: toDef.icon, output: '', error: e.message });
250
+ }
251
+ }
252
+ }
253
+ continue; // Loop handles its own downstream
254
+ }
255
+ } catch { /* not JSON — no branching */ }
256
+
257
+ const nextCtx = { ...ctx, output: output || '', [`${node.defId}_output`]: output || '' };
258
+
259
+ if (branch) {
260
+ // Branching: only follow edges that match the branch port
261
+ for (const { to: toId, port } of downstream) {
262
+ if (port === branch || port === 'default') {
263
+ queue.push({ nodeId: toId, ctx: nextCtx });
264
+ }
265
+ }
266
+ } else {
267
+ // Normal: follow all downstream edges
268
+ for (const { to: toId } of downstream) {
269
+ queue.push({ nodeId: toId, ctx: nextCtx });
270
+ }
151
271
  }
152
272
  }
153
273