nothumanallowed 15.1.47 → 15.1.48

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.47",
3
+ "version": "15.1.48",
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.47';
8
+ export const VERSION = '15.1.48';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -16,7 +16,7 @@ import {
16
16
  } from '../../services/conversations.mjs';
17
17
  import { callLLMStream, callLLM, callLLMVision, parseAgentFile } from '../../services/llm.mjs';
18
18
  import { buildMemoryContext } from '../../services/memory.mjs';
19
- import { parseActions, executeTool, buildSystemPrompt } from '../../services/tool-executor.mjs';
19
+ import { parseActions, executeTool, buildSystemPrompt, stripOrphanFences } from '../../services/tool-executor.mjs';
20
20
 
21
21
  // Migrate on import (once)
22
22
  migrateOldHistory();
@@ -275,17 +275,59 @@ export function register(router) {
275
275
  a.params = { url: 'https://' + a.params.query.trim() };
276
276
  }
277
277
  }
278
- // Auto-detect email reading intent — force imap_list if LLM didn't emit the tool
278
+ // Auto-detect email reading intent — force the right IMAP tool if the
279
+ // LLM didn't emit one. Semantic keywords (offerta/RFQ/preventivo/...)
280
+ // trigger `imap_search` across all synced messages with each variant,
281
+ // because users almost never want "just the 5 latest" — they want
282
+ // "everything matching X". Generic "leggi le email" still falls back
283
+ // to imap_list. Multi-language: it/en/de/fr/es.
284
+ const lower = msg.toLowerCase();
279
285
  const wantsReadEmail = /\b(leggi|read|mostra|lista|ultime?|recenti?|email|mail|inbox|posta)\b.*\b(email|mail|messag|inbox|posta)\b|\b(email|mail)\b.*\b(leggi|read|mostra|lista|ultime?|recenti?)\b/i.test(msg);
280
- if (wantsReadEmail && !actions.some(a => a.action?.startsWith('imap_') || a.action === 'list_emails')) {
286
+ // Semantic intent quote / offer / proposal / order requests
287
+ const QUOTE_KEYWORDS = {
288
+ offerta: ['offerta', 'richiesta offerta', 'richiesta di offerta', 'preventivo', 'quotazione', 'rdo', 'rda'],
289
+ quote: ['quote', 'quotation', 'rfq', 'rfp', 'request for quote', 'request for proposal', 'price request', 'pricing'],
290
+ order: ['ordine', 'order', 'po ', 'purchase order', 'commessa', 'bestellung', 'commande'],
291
+ invoice: ['fattura', 'invoice', 'rechnung', 'facture'],
292
+ proposal: ['proposta', 'proposal', 'angebot', 'devis'],
293
+ };
294
+ let semanticBag = null;
295
+ if (/\b(offert|preventiv|quotaz|rdo\b|rda\b|quotation|rfq|rfp|request\s+for\s+(quote|proposal|pricing)|price\s+request|pricing|angebot|devis|proposta|proposal)/i.test(lower)) {
296
+ semanticBag = 'offerta';
297
+ } else if (/\b(ordin|order|purchase\s+order|po\s+\d|commessa|bestellung|commande)/i.test(lower)) {
298
+ semanticBag = 'order';
299
+ } else if (/\b(fattur|invoice|rechnung|facture)/i.test(lower)) {
300
+ semanticBag = 'invoice';
301
+ } else if (/\b(propost|proposal|angebot|devis)/i.test(lower)) {
302
+ semanticBag = 'proposal';
303
+ }
304
+
305
+ if ((wantsReadEmail || semanticBag) && !actions.some(a => a.action?.startsWith('imap_') || a.action === 'list_emails')) {
281
306
  try {
282
307
  const { listAccounts: _la } = await import('../../services/email-db.mjs');
283
308
  const imapAccs = _la();
284
309
  if (imapAccs.length > 0) {
285
310
  const firstAcc = imapAccs[0];
286
- const limitMatch = msg.match(/\b(\d+)\b/);
287
- const limit = limitMatch ? Math.min(parseInt(limitMatch[1]), 20) : 5;
288
- actions.push({ action: 'imap_list', params: { accountId: firstAcc.id, limit } });
311
+ if (semanticBag) {
312
+ // Push one imap_search per keyword variant in the bag. The
313
+ // synthesis step will dedupe overlapping hits. We default to a
314
+ // 60-day window (limit=80 per query) — much wider than the
315
+ // 5-email peek the old branch did.
316
+ const bag = QUOTE_KEYWORDS[semanticBag] || [];
317
+ const variants = [
318
+ ...(QUOTE_KEYWORDS.offerta || []).slice(0, 3),
319
+ ...(QUOTE_KEYWORDS.quote || []).slice(0, 3),
320
+ ...bag,
321
+ ];
322
+ const unique = [...new Set(variants)].slice(0, 6);
323
+ for (const q of unique) {
324
+ actions.push({ action: 'imap_search', params: { accountId: firstAcc.id, query: q, limit: 50 } });
325
+ }
326
+ } else {
327
+ const limitMatch = msg.match(/\b(\d+)\b/);
328
+ const limit = limitMatch ? Math.min(parseInt(limitMatch[1]), 50) : 20;
329
+ actions.push({ action: 'imap_list', params: { accountId: firstAcc.id, limit } });
330
+ }
289
331
  }
290
332
  } catch { /* fallback to LLM response */ }
291
333
  }
@@ -389,18 +431,23 @@ export function register(router) {
389
431
  });
390
432
  }
391
433
 
434
+ // Strip orphan tool-fence blocks that the LLM may have emitted as
435
+ // a "no-more-tools" marker (e.g. empty ```json ``` or '''json ''').
436
+ // They leaked into the chat panel as visible noise.
437
+ const cleanFullResponse = stripOrphanFences(fullResponse);
438
+
392
439
  // Persist to conversation
393
440
  if (body.conversationId) {
394
441
  try {
395
442
  const conv = loadConversation(body.conversationId);
396
443
  if (conv) {
397
- addMessages(conv, msg, fullResponse);
444
+ addMessages(conv, msg, cleanFullResponse);
398
445
  }
399
446
  } catch {}
400
447
  }
401
448
 
402
449
  if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; }
403
- sse('done', { content: fullResponse });
450
+ sse('done', { content: cleanFullResponse });
404
451
  res.write('data: [DONE]\n\n');
405
452
  res.end();
406
453
  } catch (e) {
@@ -820,9 +820,12 @@ export function parseActions(text) {
820
820
  const actions = [];
821
821
  const textParts = [];
822
822
 
823
- // Normalize: some LLMs output "json ... " (double-quote fences) instead of ```json ... ```
824
- // Replace "json\n{...}\n" patterns with proper ```json fences before parsing
823
+ // Normalize: some LLMs output "json ... ", 'json ... ' or '''json ... '''
824
+ // (Python-style triple-quote) instead of ```json ... ```. We rewrite all
825
+ // these to proper triple-backtick fences before the main regex runs.
825
826
  const normalized = text
827
+ .replace(/'''json\s*\n?([\s\S]*?)\n?\s*'''/g, (_, body) => '```json\n' + body.trim() + '\n```')
828
+ .replace(/"""json\s*\n?([\s\S]*?)\n?\s*"""/g, (_, body) => '```json\n' + body.trim() + '\n```')
826
829
  .replace(/"json\s*\n([\s\S]*?)\n\s*"/g, (_, body) => '```json\n' + body.trim() + '\n```')
827
830
  .replace(/'json\s*\n([\s\S]*?)\n\s*'/g, (_, body) => '```json\n' + body.trim() + '\n```');
828
831
 
@@ -914,6 +917,30 @@ export function parseActions(text) {
914
917
  return { textParts, actions };
915
918
  }
916
919
 
920
+ /**
921
+ * Strip orphan tool-fence blocks from a finished LLM response. These appear
922
+ * when the model emits an empty `'''json '''` (or backtick-fenced) block
923
+ * as a no-op marker — the parser doesn't pick them up as actions but they
924
+ * leak into the UI as visible noise. Run this on the final assistant text
925
+ * before showing it to the user.
926
+ */
927
+ export function stripOrphanFences(text) {
928
+ if (!text || typeof text !== 'string') return text;
929
+ return text
930
+ // Triple-backtick, triple-single, triple-double quote fences with `json`
931
+ // marker — empty or with a body. We strip the whole block.
932
+ .replace(/```json\s*\n?[\s\S]*?```/g, '')
933
+ .replace(/'''json\s*\n?[\s\S]*?'''/g, '')
934
+ .replace(/"""json\s*\n?[\s\S]*?"""/g, '')
935
+ // Bare action JSON that the parser already consumed but the synthesis
936
+ // step regurgitated (rare but happens with Liara). Only strip if the
937
+ // JSON shape matches "action":"...".
938
+ .replace(/\{\s*"action"\s*:\s*"[^"]+"\s*,?\s*("params"\s*:\s*\{[\s\S]*?\}\s*)?\}/g, '')
939
+ // Collapse blank-line clusters produced by the stripping.
940
+ .replace(/\n{3,}/g, '\n\n')
941
+ .trim();
942
+ }
943
+
917
944
  // ── Formatting Helpers ───────────────────────────────────────────────────────
918
945
 
919
946
  /**