nothumanallowed 15.1.65 → 15.1.66

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": "15.1.65",
3
+ "version": "15.1.66",
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 = '15.1.65';
8
+ export const VERSION = '15.1.66';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -6,54 +6,82 @@ import crypto from 'crypto';
6
6
  import { fail } from './ui.mjs';
7
7
 
8
8
  /**
9
- * Download a file from URL to disk.
9
+ * Download a file from URL to disk, with automatic retry on transient errors.
10
+ *
11
+ * Retries on: timeouts, DNS failures, ECONNRESET, 5xx responses. Backs off
12
+ * exponentially (1s, 3s, 9s). Never retries on 4xx (semantic errors).
13
+ *
10
14
  * @param {string} url
11
15
  * @param {string} dest — absolute file path
12
16
  * @param {object} opts
13
- * @param {string} [opts.sha256] — expected hash (hex)
14
- * @param {number} [opts.timeout] — ms (default 30s)
17
+ * @param {string} [opts.sha256] — expected hash (hex)
18
+ * @param {number} [opts.timeout] — ms per attempt (default 30s)
19
+ * @param {number} [opts.retries] — total attempts (default 3)
15
20
  * @returns {Promise<boolean>}
16
21
  */
17
22
  export async function download(url, dest, opts = {}) {
18
23
  const timeout = opts.timeout ?? 30_000;
19
- try {
20
- const controller = new AbortController();
21
- const timer = setTimeout(() => controller.abort(), timeout);
24
+ const maxAttempts = Math.max(1, opts.retries ?? 3);
25
+ const { VERSION } = await import('./constants.mjs').catch(() => ({ VERSION: 'dev' }));
22
26
 
23
- const res = await fetch(url, {
24
- signal: controller.signal,
25
- headers: { 'User-Agent': 'nha-cli/1.0.0' },
26
- });
27
- clearTimeout(timer);
27
+ let lastErr = null;
28
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
29
+ try {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), timeout);
32
+ const res = await fetch(url, {
33
+ signal: controller.signal,
34
+ headers: { 'User-Agent': `nha-cli/${VERSION}` },
35
+ });
36
+ clearTimeout(timer);
28
37
 
29
- if (!res.ok) {
30
- fail(`HTTP ${res.status} downloading ${path.basename(dest)}`);
31
- return false;
32
- }
38
+ if (!res.ok) {
39
+ // 5xx retry; 4xx → permanent failure, no retry
40
+ if (res.status >= 500 && attempt < maxAttempts) {
41
+ await _delay(_backoff(attempt));
42
+ continue;
43
+ }
44
+ fail(`HTTP ${res.status} downloading ${path.basename(dest)}`);
45
+ return false;
46
+ }
33
47
 
34
- const buffer = Buffer.from(await res.arrayBuffer());
48
+ const buffer = Buffer.from(await res.arrayBuffer());
35
49
 
36
- if (opts.sha256) {
37
- const hash = crypto.createHash('sha256').update(buffer).digest('hex');
38
- if (hash !== opts.sha256) {
39
- fail(`Integrity check failed for ${path.basename(dest)}`);
40
- fail(` Expected: ${opts.sha256}`);
41
- fail(` Got: ${hash}`);
42
- return false;
50
+ if (opts.sha256) {
51
+ const hash = crypto.createHash('sha256').update(buffer).digest('hex');
52
+ if (hash !== opts.sha256) {
53
+ fail(`Integrity check failed for ${path.basename(dest)}`);
54
+ fail(` Expected: ${opts.sha256}`);
55
+ fail(` Got: ${hash}`);
56
+ return false;
57
+ }
43
58
  }
44
- }
45
59
 
46
- fs.mkdirSync(path.dirname(dest), { recursive: true });
47
- fs.writeFileSync(dest, buffer);
48
- return true;
49
- } catch (err) {
50
- if (err.name === 'AbortError') {
51
- fail(`Timeout downloading ${path.basename(dest)}`);
52
- } else {
53
- fail(`Failed to download ${path.basename(dest)}: ${err.message}`);
60
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
61
+ fs.writeFileSync(dest, buffer);
62
+ return true;
63
+ } catch (err) {
64
+ lastErr = err;
65
+ const transient = err.name === 'AbortError'
66
+ || /ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|ENETUNREACH|UND_ERR/i.test(err.message || '');
67
+ if (transient && attempt < maxAttempts) {
68
+ await _delay(_backoff(attempt));
69
+ continue;
70
+ }
71
+ break;
54
72
  }
55
- return false;
56
73
  }
74
+ if (lastErr) {
75
+ if (lastErr.name === 'AbortError') fail(`Timeout downloading ${path.basename(dest)} after ${maxAttempts} attempts`);
76
+ else fail(`Failed to download ${path.basename(dest)}: ${lastErr.message}`);
77
+ }
78
+ return false;
79
+ }
80
+
81
+ function _delay(ms) { return new Promise(r => setTimeout(r, ms)); }
82
+ function _backoff(attempt) {
83
+ // 1s, 3s, 9s — capped at 30s
84
+ return Math.min(30_000, 1000 * Math.pow(3, attempt - 1));
57
85
  }
58
86
 
59
87
  /**
@@ -8,6 +8,7 @@ import { sendJSON, sendError, parseBody } from '../index.mjs';
8
8
  import { loadConfig } from '../../config.mjs';
9
9
  import { AGENTS_DIR, NHA_DIR } from '../../constants.mjs';
10
10
  import { callLLMStream, parseAgentFile } from '../../services/llm.mjs';
11
+ import { tryDirectActionAll } from '../../services/message-responder.mjs';
11
12
 
12
13
  function loadAgentCards() {
13
14
  if (!fs.existsSync(AGENTS_DIR)) return [];
@@ -88,6 +89,23 @@ export function register(router) {
88
89
  const sse = (ev, data) => res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`);
89
90
 
90
91
  try {
92
+ // ── Direct-action pre-step (deterministic tool exec, LLM only for NLU) ──
93
+ // Same dispatcher used by Telegram / Discord / Chat WebUI / AWF: if
94
+ // the user's message maps to a state-changing tool (CRUD on calendar,
95
+ // email, drive, slack, github, file, ...), execute it server-side and
96
+ // stream the result. LLM agent is bypassed entirely for actions.
97
+ const direct = await tryDirectActionAll(body.message, loadConfig(), {
98
+ auditKey: `agents:${body.agent || 'any'}`,
99
+ });
100
+ if (direct) {
101
+ sse('tool', { action: direct.action, status: 'done' });
102
+ sse('token', { content: direct.message });
103
+ sse('done', {});
104
+ res.write('data: [DONE]\n\n');
105
+ res.end();
106
+ return;
107
+ }
108
+
91
109
  const agentSlug = body.agent?.toLowerCase();
92
110
  const LANG_MAP = { it:'Italian', en:'English', es:'Spanish', fr:'French', de:'German', pt:'Portuguese', nl:'Dutch', pl:'Polish', ru:'Russian', zh:'Chinese', ja:'Japanese', ko:'Korean', ar:'Arabic', hi:'Hindi', tr:'Turkish', sv:'Swedish', da:'Danish', fi:'Finnish', cs:'Czech' };
93
111
  const lang = LANG_MAP[(config?.language || config?.lang || 'en').slice(0,2)] || 'English';
@@ -674,6 +674,19 @@ export function register(router) {
674
674
  const userLang = detectedFromMsg || settingLang;
675
675
  enrichedPrompt += `\n\nIMPORTANT: The user wrote their message in ${userLang}. Respond in ${userLang}.`;
676
676
 
677
+ // Direct-action pre-step — but ONLY when there's no attachment. With
678
+ // a PDF/image, the user wants the model to *read* the file; we don't
679
+ // want to short-circuit that into a tool call. With pure-text /api/chat
680
+ // (non-streaming), same dispatcher as the streaming variant.
681
+ if (!body.pdfBase64 && !body.imageBase64 && !body.fileContent) {
682
+ const direct = await tryDirectActionAll(body.message, config, {
683
+ auditKey: `chat-ns:${body.conversationId || 'anon'}`,
684
+ });
685
+ if (direct) {
686
+ return sendJSON(res, 200, { response: direct.message });
687
+ }
688
+ }
689
+
677
690
  let response;
678
691
 
679
692
  if (body.pdfBase64) {
package/src/updater.mjs CHANGED
@@ -170,9 +170,12 @@ export async function runUpdate() {
170
170
  }
171
171
 
172
172
  // ── Agents + Legion + PIF (downloaded from website, not npm) ───────────
173
+ // 45s timeout (was 15s) — VMs / slow connections can take that long for
174
+ // the first manifest fetch. The downloader retries internally for batch
175
+ // downloads, so this is just for the initial manifest.
173
176
  const res = await fetch(`${BASE_URL}/versions.json`, {
174
- signal: AbortSignal.timeout(15000),
175
- headers: { 'User-Agent': 'nha-cli/1.0.0' },
177
+ signal: AbortSignal.timeout(45000),
178
+ headers: { 'User-Agent': `nha-cli/${(await import('./constants.mjs')).VERSION}` },
176
179
  });
177
180
  if (!res.ok) {
178
181
  warn('Could not reach nothumanallowed.com for agent updates.');
@@ -195,7 +198,7 @@ export async function runUpdate() {
195
198
  // Update Legion
196
199
  if (legionCurrent !== legionLatest) {
197
200
  info(`Legion X: ${legionCurrent} → ${legionLatest}`);
198
- const success = await download(`${BASE_URL}/legion-x.mjs`, LEGION_FILE, { timeout: 60_000 });
201
+ const success = await download(`${BASE_URL}/legion-x.mjs`, LEGION_FILE, { timeout: 90_000, retries: 4 });
199
202
  if (success) { ok(`Legion X updated to v${legionLatest}`); updated = true; }
200
203
  } else {
201
204
  ok(`Legion X v${legionCurrent} (up to date)`);
@@ -204,7 +207,7 @@ export async function runUpdate() {
204
207
  // Update PIF
205
208
  if (pifCurrent !== pifLatest) {
206
209
  info(`PIF: ${pifCurrent} → ${pifLatest}`);
207
- const success = await download(`${BASE_URL}/pif.mjs`, PIF_FILE, { timeout: 60_000 });
210
+ const success = await download(`${BASE_URL}/pif.mjs`, PIF_FILE, { timeout: 90_000, retries: 4 });
208
211
  if (success) { ok(`PIF updated to v${pifLatest}`); updated = true; }
209
212
  } else {
210
213
  ok(`PIF v${pifCurrent} (up to date)`);