nothumanallowed 15.1.65 → 15.1.67
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 +1 -1
- package/src/commands/ops.mjs +12 -2
- package/src/constants.mjs +1 -1
- package/src/downloader.mjs +61 -33
- package/src/server/routes/agents.mjs +18 -0
- package/src/server/routes/chat.mjs +13 -0
- package/src/services/message-responder.mjs +13 -6
- package/src/updater.mjs +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.67",
|
|
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/commands/ops.mjs
CHANGED
|
@@ -97,8 +97,18 @@ export async function cmdOps(args) {
|
|
|
97
97
|
const telegramConfigured = !!config.responder?.telegram?.token;
|
|
98
98
|
const discordConfigured = !!config.responder?.discord?.token;
|
|
99
99
|
console.log(`\n ${BOLD}Message Responder${NC}\n`);
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// Surface the exact reason the responder didn't activate (missing
|
|
101
|
+
// LLM key on a paid provider, missing token, etc.) instead of the
|
|
102
|
+
// generic "daemon restart needed" — which is misleading because the
|
|
103
|
+
// daemon IS running, the responder just refused to spin up.
|
|
104
|
+
const reason = responder.reason || '';
|
|
105
|
+
let inactiveHintTel = 'configured but inactive (try: nha ops stop && nha ops start)';
|
|
106
|
+
if (reason.startsWith('missing_key:')) {
|
|
107
|
+
const p = reason.slice('missing_key:'.length);
|
|
108
|
+
inactiveHintTel = `configured but LLM key missing for provider "${p}" — fix with: nha config set provider nha (free Liara) OR nha config set ${p}-key YOUR_KEY`;
|
|
109
|
+
}
|
|
110
|
+
console.log(` Telegram: ${responder.telegram ? G + 'active' + NC : telegramConfigured ? Y + inactiveHintTel + NC : D + 'not configured' + NC}`);
|
|
111
|
+
console.log(` Discord: ${responder.discord ? G + 'active' + NC : discordConfigured ? Y + inactiveHintTel + NC : D + 'not configured' + NC}`);
|
|
102
112
|
console.log(` Auto-route: ${config.responder?.autoRoute !== false ? G + 'keyword routing' + NC : D + 'CONDUCTOR only' + NC}`);
|
|
103
113
|
|
|
104
114
|
console.log('');
|
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.
|
|
8
|
+
export const VERSION = '15.1.67';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/downloader.mjs
CHANGED
|
@@ -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]
|
|
14
|
-
* @param {number} [opts.timeout]
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
48
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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) {
|
|
@@ -2749,12 +2749,19 @@ export function startResponder(config, log, wsBroadcast) {
|
|
|
2749
2749
|
const hasAnyToken = config.responder?.telegram?.token || config.responder?.discord?.token;
|
|
2750
2750
|
if (!hasAnyToken) {
|
|
2751
2751
|
log('[Responder] No tokens configured — skipping');
|
|
2752
|
-
return { telegram: false, discord: false };
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2752
|
+
return { telegram: false, discord: false, reason: 'no_token' };
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// Liara (provider 'nha') is the default free tier — it does NOT require an
|
|
2756
|
+
// API key. Only refuse to start if the user explicitly picked a paid
|
|
2757
|
+
// provider (anthropic, openai, gemini, ...) and forgot to set the key.
|
|
2758
|
+
// Previously we rejected ALL providers including Liara, leaving the user
|
|
2759
|
+
// stuck on "configured (daemon restart needed)" with no actionable hint.
|
|
2760
|
+
const provider = (config.llm?.provider || 'nha').toLowerCase();
|
|
2761
|
+
const PAID_PROVIDERS = new Set(['anthropic', 'openai', 'gemini', 'deepseek', 'grok', 'mistral', 'cohere']);
|
|
2762
|
+
if (PAID_PROVIDERS.has(provider) && !config.llm?.apiKey) {
|
|
2763
|
+
log(`[Responder] Provider "${provider}" requires an API key — cannot respond. Run: nha config set provider nha (to switch to free Liara) OR nha config set ${provider}-key YOUR_KEY`);
|
|
2764
|
+
return { telegram: false, discord: false, reason: `missing_key:${provider}` };
|
|
2758
2765
|
}
|
|
2759
2766
|
|
|
2760
2767
|
const result = { telegram: false, discord: false };
|
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(
|
|
175
|
-
headers: { 'User-Agent':
|
|
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:
|
|
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:
|
|
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)`);
|