nothumanallowed 9.8.0 → 9.8.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": "9.8.0",
3
+ "version": "9.8.1",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 53 tools. Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, GitHub, Notion, Slack, voice chat, 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 = '9.8.0';
8
+ export const VERSION = '9.8.1';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -276,110 +276,141 @@ export async function callLLM(config, systemPrompt, userMessage, opts = {}) {
276
276
  }
277
277
 
278
278
  /**
279
- * Call LLM with multimodal (vision) messages — supports image content.
280
- * Uses the provider's native vision format.
281
- * @param {object} config
282
- * @param {Array} messages - Array of { role, content } where content can be string or array of content blocks
283
- * @returns {Promise<string>}
279
+ * Call an LLM provider with streaming enabled.
280
+ * Calls onToken(chunk) for each token, returns full text at the end.
281
+ * @returns {Promise<string>} The full LLM response text.
284
282
  */
285
- export async function callLLMVision(config, messages) {
286
- const provider = config.llm.provider || 'anthropic';
287
- const model = config.llm.model || null;
283
+ export async function callLLMStream(config, systemPrompt, userMessage, onToken, opts = {}) {
284
+ const provider = opts.provider || config.llm.provider || 'anthropic';
285
+ const model = opts.model || config.llm.model || null;
288
286
  const apiKey = getApiKey(config, provider);
289
- if (!apiKey) throw new Error(`No API key for ${provider}. Vision requires Claude, GPT-4, or Gemini.`);
287
+ if (!apiKey) throw new Error(`No API key for ${provider}`);
290
288
 
291
- if (provider === 'anthropic') {
292
- // Anthropic format: system separate, messages with content blocks
293
- const systemMsg = messages.find(m => m.role === 'system');
294
- const userMsgs = messages.filter(m => m.role !== 'system');
295
-
296
- // Convert OpenAI-style image_url to Anthropic format
297
- const anthropicMessages = userMsgs.map(m => {
298
- if (typeof m.content === 'string') return m;
299
- const blocks = m.content.map(block => {
300
- if (block.type === 'text') return block;
301
- if (block.type === 'image_url') {
302
- const url = block.image_url.url;
303
- const match = url.match(/^data:image\/(png|jpeg|gif|webp);base64,(.+)$/);
304
- if (match) {
305
- return { type: 'image', source: { type: 'base64', media_type: `image/${match[1]}`, data: match[2] } };
306
- }
307
- }
308
- return block;
309
- });
310
- return { role: m.role, content: blocks };
311
- });
289
+ const callFn = getProviderCall(provider);
290
+ if (!callFn) throw new Error(`Unknown provider: ${provider}`);
291
+
292
+ // Gemini and Cohere don't support streaming — fall back to non-streaming
293
+ if (provider === 'gemini' || provider === 'cohere') {
294
+ const text = await callFn(apiKey, model, systemPrompt, userMessage, false);
295
+ if (onToken) onToken(text);
296
+ return text;
297
+ }
298
+
299
+ const format = provider === 'anthropic' ? 'anthropic' : 'openai';
300
+ const body = buildRequestBody(provider, model, systemPrompt, userMessage, true);
301
+ const url = getProviderUrl(provider, model, apiKey);
302
+ const headers = getProviderHeaders(provider, apiKey);
303
+
304
+ const res = await fetch(url, {
305
+ method: 'POST',
306
+ headers,
307
+ body: JSON.stringify(body),
308
+ });
309
+ if (!res.ok) {
310
+ const err = await res.text();
311
+ throw new Error(`${provider} ${res.status}: ${err}`);
312
+ }
312
313
 
313
- const body = {
314
+ return streamSSEWithCallback(res, format, onToken);
315
+ }
316
+
317
+ /** Build request body for a provider */
318
+ function buildRequestBody(provider, model, systemPrompt, userMessage, stream) {
319
+ if (provider === 'anthropic') {
320
+ return {
314
321
  model: model || 'claude-sonnet-4-20250514',
315
- max_tokens: 4096,
316
- system: systemMsg?.content || '',
317
- messages: anthropicMessages,
322
+ max_tokens: 8192,
323
+ system: systemPrompt,
324
+ messages: [{ role: 'user', content: userMessage }],
325
+ stream,
318
326
  };
319
- const res = await fetch('https://api.anthropic.com/v1/messages', {
320
- method: 'POST',
321
- headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
322
- body: JSON.stringify(body),
323
- });
324
- if (!res.ok) throw new Error(`Anthropic vision ${res.status}: ${await res.text()}`);
325
- const data = await res.json();
326
- return data.content?.[0]?.text || '';
327
327
  }
328
+ // OpenAI-compatible format (OpenAI, DeepSeek, Grok, Mistral)
329
+ const modelDefaults = {
330
+ openai: 'gpt-4o',
331
+ deepseek: 'deepseek-chat',
332
+ grok: 'grok-3-latest',
333
+ mistral: 'mistral-large-latest',
334
+ };
335
+ return {
336
+ model: model || modelDefaults[provider] || 'gpt-4o',
337
+ max_tokens: 8192,
338
+ messages: [
339
+ { role: 'system', content: systemPrompt },
340
+ { role: 'user', content: userMessage },
341
+ ],
342
+ stream,
343
+ };
344
+ }
345
+
346
+ /** Get provider API URL */
347
+ function getProviderUrl(provider, model, apiKey) {
348
+ const urls = {
349
+ anthropic: 'https://api.anthropic.com/v1/messages',
350
+ openai: 'https://api.openai.com/v1/chat/completions',
351
+ deepseek: 'https://api.deepseek.com/v1/chat/completions',
352
+ grok: 'https://api.x.ai/v1/chat/completions',
353
+ mistral: 'https://api.mistral.ai/v1/chat/completions',
354
+ };
355
+ return urls[provider] || urls.openai;
356
+ }
328
357
 
329
- if (provider === 'openai' || provider === 'deepseek' || provider === 'grok' || provider === 'mistral') {
330
- // OpenAI-compatible format — works with GPT-4V, DeepSeek VL, etc.
331
- const url = provider === 'openai' ? 'https://api.openai.com/v1/chat/completions'
332
- : provider === 'deepseek' ? 'https://api.deepseek.com/chat/completions'
333
- : provider === 'grok' ? 'https://api.x.ai/v1/chat/completions'
334
- : 'https://api.mistral.ai/v1/chat/completions';
335
-
336
- const visionModel = model || (provider === 'openai' ? 'gpt-4o' : model);
337
- const body = { model: visionModel, max_tokens: 4096, messages };
338
- const res = await fetch(url, {
339
- method: 'POST',
340
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
341
- body: JSON.stringify(body),
342
- });
343
- if (!res.ok) throw new Error(`${provider} vision ${res.status}: ${await res.text()}`);
344
- const data = await res.json();
345
- return data.choices?.[0]?.message?.content || '';
358
+ /** Get provider request headers */
359
+ function getProviderHeaders(provider, apiKey) {
360
+ if (provider === 'anthropic') {
361
+ return {
362
+ 'Content-Type': 'application/json',
363
+ 'x-api-key': apiKey,
364
+ 'anthropic-version': '2023-06-01',
365
+ };
346
366
  }
367
+ return {
368
+ 'Content-Type': 'application/json',
369
+ 'Authorization': `Bearer ${apiKey}`,
370
+ };
371
+ }
372
+
373
+ /** SSE stream parser with onToken callback (does NOT write to stdout directly) */
374
+ async function streamSSEWithCallback(res, format, onToken) {
375
+ const reader = res.body.getReader();
376
+ const decoder = new TextDecoder();
377
+ let buffer = '';
378
+ let fullText = '';
379
+
380
+ while (true) {
381
+ const { done, value } = await reader.read();
382
+ if (done) break;
383
+
384
+ buffer += decoder.decode(value, { stream: true });
385
+ const lines = buffer.split('\n');
386
+ buffer = lines.pop() || '';
387
+
388
+ for (const line of lines) {
389
+ if (!line.startsWith('data: ')) continue;
390
+ const data = line.slice(6).trim();
391
+ if (data === '[DONE]') continue;
392
+
393
+ try {
394
+ const json = JSON.parse(data);
395
+ let chunk = '';
347
396
 
348
- if (provider === 'gemini') {
349
- // Gemini format inline_data with base64
350
- const systemMsg = messages.find(m => m.role === 'system');
351
- const userMsgs = messages.filter(m => m.role !== 'system');
352
- const parts = [];
353
- for (const msg of userMsgs) {
354
- if (typeof msg.content === 'string') {
355
- parts.push({ text: msg.content });
356
- } else {
357
- for (const block of msg.content) {
358
- if (block.type === 'text') parts.push({ text: block.text });
359
- if (block.type === 'image_url') {
360
- const match = block.image_url.url.match(/^data:image\/(.*?);base64,(.+)$/);
361
- if (match) parts.push({ inline_data: { mime_type: `image/${match[1]}`, data: match[2] } });
397
+ if (format === 'anthropic') {
398
+ if (json.type === 'content_block_delta') {
399
+ chunk = json.delta?.text || '';
362
400
  }
401
+ } else {
402
+ chunk = json.choices?.[0]?.delta?.content || '';
363
403
  }
364
- }
365
- }
366
404
 
367
- const geminiModel = model || 'gemini-2.0-flash';
368
- const body = {
369
- contents: [{ parts }],
370
- systemInstruction: systemMsg ? { parts: [{ text: systemMsg.content }] } : undefined,
371
- };
372
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`, {
373
- method: 'POST',
374
- headers: { 'Content-Type': 'application/json' },
375
- body: JSON.stringify(body),
376
- });
377
- if (!res.ok) throw new Error(`Gemini vision ${res.status}: ${await res.text()}`);
378
- const data = await res.json();
379
- return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
405
+ if (chunk) {
406
+ fullText += chunk;
407
+ if (onToken) onToken(chunk);
408
+ }
409
+ } catch {}
410
+ }
380
411
  }
381
412
 
382
- throw new Error(`Vision not supported for provider: ${provider}. Use Claude, GPT-4, or Gemini.`);
413
+ return fullText;
383
414
  }
384
415
 
385
416
  /**