alexa-ai 2.1.1 → 2.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,80 @@ All notable changes to `alexa-ai` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [2.2.1] — 2026-09-07
8
+
9
+ ### Added
10
+ - Device identity: the client keeps a stable random `deepai_device_id`
11
+ (32 bytes, base64url) and sends it as a cookie; anonymous `/api/*`
12
+ generation is rate-limited per device. Override with `deviceId` /
13
+ `DEEPAI_DEVICE_ID`.
14
+ - `examples/text2img-standalone.js` — zero-dependency CLI for anonymous
15
+ image generation using the full browser request dialect.
16
+
17
+ ## [2.2.0] — 2026-09-07
18
+
19
+ ### Fixed
20
+ - Image generation with anonymous `tryit-…` keys. The key's hex part is a
21
+ deterministic hash over the User-Agent
22
+ (`H(UA + H(UA + H(UA + digits + SALT)))`, see `DeepAIClient._islandHash`);
23
+ `generateTryItKey(userAgent)` now derives it correctly, and `headers()`
24
+ mints a fresh key per request because anonymous keys are single-use.
25
+ - `generateImage()` now sends the browser dialect for anonymous requests
26
+ (`generation_source`, `width`/`height` mapped from `aspectRatio`,
27
+ `image_generator_version`, `quality`).
28
+
29
+ ### Added
30
+ - Anonymous browser-shaped retry when a registered non-Pro key is refused
31
+ (result `via: 'anonymous'`, `runApiWithTryItKey(name, fields)`); disable
32
+ with `{ noAnonymousFallback: true }`.
33
+ - `DEEPAI_KEY` env alias alongside `DEEPAI_API_KEY`.
34
+ - `DeepAIClient.isTryItKey(key)`.
35
+
36
+ ### Changed
37
+ - Extended refusal classification ("Pro members", "model only available…",
38
+ "insufficient_credits", …) so quota errors rotate keys and report
39
+ `DEEPAI_QUOTA_EXCEEDED` correctly.
40
+ - `share_url` preferred over `output_url` in generated-image results.
41
+ - Default `userAgent` updated (the tryit key hash is computed over it; keep
42
+ `userAgent` stable if you persist keys).
43
+
44
+ ## [2.1.2] — 2026-09-06
45
+
46
+ ### Changed
47
+ - **`searchWeb()` now searches the web itself** instead of trusting DeepAI's
48
+ server-side web access. Observed live with `gpt-4o-mini`: the same query
49
+ produced one report without any sources and one with four invented links,
50
+ because the model skipped the search and wrote from memory. The engine now
51
+ queries Bing, Bing News, Wikipedia, Google News and DuckDuckGo in parallel
52
+ (no API key needed; the four feeds were verified live on 2026-09-06 —
53
+ DuckDuckGo's html endpoint serves a bot challenge from data-centre IPs, so
54
+ its lite endpoint is tried first and a challenge page yields no results), hands the results to the model as numbered material, and asks
55
+ it to cite result numbers and never write URLs. The `*Sources:*` block is
56
+ built from the search results only — cited first — so a link the model made
57
+ up can never reach the chat. Citation markers are removed from the prose;
58
+ URLs the model still types are turned into citations when they match a
59
+ result and dropped otherwise.
60
+ - Result gained `grounded` (did the engine's search answer?), `providers`,
61
+ `via` (`'model'` | `'digest'`), and each source carries `date`, `provider`
62
+ and `cited`.
63
+ - New options: `results` (bring your own search API's results), `search:false`,
64
+ `providers`, `maxResults`; constructor options `webSearch`,
65
+ `webSearchProviders`, `webSearchTimeout`, `webSearchResults`,
66
+ `webSearchProvider(query, { maxResults, signal })`.
67
+ - When every provider fails the request falls back to DeepAI's web access as
68
+ before (`grounded:false`); when the search worked but the model call failed,
69
+ a plain digest of the results is returned (`via:'digest'`, still `ok:true`).
70
+
71
+ ### Added
72
+ - `WebSearch` service (exported) with parsers for the DuckDuckGo html/lite
73
+ pages, RSS 2.0 news feeds and the MediaWiki search API, redirect unwrapping
74
+ (`duckduckgo.com/l/?uddg=`, `bing.com/news/apiclick.aspx?url=`), ad and
75
+ duplicate filtering, and provider round-robin.
76
+ - `WebAnswer.formatResults()`, `extractCitations()`, `stripUrls()`, `digest()`.
77
+ - The npm package now ships the test suite (`test/`), so the published
78
+ tarball is a complete snapshot of the repository at the tagged version:
79
+ `npm explore alexa-ai -- npm test`.
80
+
7
81
  ## [2.1.1] — 2026-09-06
8
82
 
9
83
  ### Fixed
package/README.md CHANGED
@@ -427,23 +427,39 @@ await ai.deepai.runApi('waifu2x', { image: buffer }); // any /api/<name> e
427
427
 
428
428
  All media arguments accept the same shapes as `chat({ image })`.
429
429
 
430
- **`generateImage()` on a free key.** `POST /api/text2img` is a paid endpoint:
431
- anonymous keys receive `{"status": "Out of API credits"}`. The engine tries it
432
- first because it is fast and returns a plain URL; when it is refused, it
433
- drives the same in-chat `generate_image` tool the deepai.org web client uses,
434
- which works on free chat keys. The result reports the route that answered
435
- (`via: 'api' | 'chat'`). Options: `{ apiOnly }`, `{ chatToolOnly }`,
436
- `{ aspectRatio: '16:9' }` for the chat tool, and `width` / `height` /
437
- `image_generator_version` for the API.
430
+ **`generateImage()` on a free key.** Three routes are tried, in order:
431
+
432
+ 1. `POST /api/text2img` with your key. Pro keys succeed here immediately.
433
+ With an anonymous `tryit-…` key the engine automatically sends the exact
434
+ browser dialect (`generation_source=chat`, `width`/`height` mapped from
435
+ `aspectRatio`, `image_generator_version=hd`, `quality=true`) and mints a
436
+ **fresh single-use key per request** — tryit keys are hash-validated
437
+ against the User-Agent and burned after one request.
438
+ 2. If a registered (non-Pro) key is refused with `402 "Pro members in good
439
+ standing"`, one anonymous browser-shaped retry. (`noAnonymousFallback:
440
+ true` disables it.)
441
+ 3. The legacy in-chat `generate_image` function call (kept for model
442
+ versions that still honour it).
443
+
444
+ The result reports the route that answered (`via: 'api' | 'anonymous' |
445
+ 'chat'`). Options: `{ apiOnly }`, `{ chatToolOnly }`, `{ noAnonymousFallback }`,
446
+ `{ aspectRatio: '16:9' }`, and `width` / `height` / `image_generator_version`
447
+ for the API. The returned URL prefers `share_url` (public, stable) over
448
+ `output_url`, matching the browser client.
438
449
 
439
450
  **`summarizeText()`** follows the same pattern — `/api/summarization` first, a
440
451
  stateless chat request as the fallback.
441
452
 
442
- **`searchWeb()`** is a one-off research request with DeepAI's web access
443
- enabled. It applies the persona and formatting rules but touches no user's
444
- memory or history, so it can be called with no jid at all. The default answer
445
- is long-form and ready to send to WhatsApp — an intro, three to five
446
- `*Heading:*` sections with numbered `*Title*: detail` points, and one
453
+ **`searchWeb()`** is a one-off research request that touches no user's
454
+ memory or history, so it can be called with no jid at all. The engine does the
455
+ searching itself — Bing, Bing News, Wikipedia, Google News and DuckDuckGo, in
456
+ parallel, no API key needed — and hands the results to the model as numbered
457
+ material. The model writes the report from them and cites result numbers; it
458
+ is told never to write a URL. The `*Sources:*` block is then built from the
459
+ search results only (cited ones first), so every link the bot shows is a page
460
+ that actually came back from a search, never one the model made up. The
461
+ default answer is long-form and ready to send to WhatsApp — an intro, three to
462
+ five `*Heading:*` sections with numbered `*Title*: detail` points, and one
447
463
  `*Sources:*` block at the end:
448
464
 
449
465
  ```
@@ -471,15 +487,25 @@ answer still comes back under `minWords` (default 150), one follow-up turn asks
471
487
  the model to rewrite it in full, and the longer reply wins — `attempts` and
472
488
  `words` in the result show what happened.
473
489
 
474
- DeepAI reports the pages it used in two different ways depending on the model
475
- that answered: some send a structured web-results packet, others (observed
476
- with `gpt-4o-mini`) just write a `Sources:` list in prose. Both end up in the
477
- `sources` array as `{ title, url, description }`; the list is rendered exactly
478
- once in `text`, and `answer` holds the prose without it. Sentences in which
479
- the model talks about itself — *"I'm a language model"*, *"I can't browse the
480
- web"*, *"based on my training data"* — are removed, and leftover template
481
- placeholders are dropped. Third-party names in the research itself (news
482
- about OpenAI or Google) are kept verbatim.
490
+ The result tells you which path answered:
491
+
492
+ ```js
493
+ const r = await ai.searchWeb('coffee');
494
+ // r.grounded true → the engine searched; sources are real search results
495
+ // false → no search results (host offline / providers blocked);
496
+ // DeepAI's own web access answered, and its sources are
497
+ // whatever the model reported — treat them with care
498
+ // r.providers ['bing', 'bing-news', 'wikipedia', 'google-news', 'duckduckgo'] — who answered
499
+ // r.sources [{ title, url, description, date, provider, cited }]
500
+ // r.via 'model' | 'digest' (the model failed but the search worked:
501
+ // a plain list of the results is returned)
502
+ // r.words, r.attempts, r.model
503
+ ```
504
+
505
+ Sentences in which the model talks about itself — *"I'm a language model"*,
506
+ *"I can't browse the web"*, *"based on my training data"* — are removed, and
507
+ leftover template placeholders are dropped. Third-party names in the research
508
+ itself (news about OpenAI or Google) are kept verbatim.
483
509
 
484
510
  ```js
485
511
  await ai.searchWeb('coffee', {
@@ -489,9 +515,37 @@ await ai.searchWeb('coffee', {
489
515
  maxSources: 3, // how many to list in `text` (the array is not capped)
490
516
  language: 'Sinhala', // answer language (default: the language of the query)
491
517
  instructions: 'focus on Sri Lanka',
518
+ providers: ['bing-news', 'google-news'], // only news for this call
519
+ maxResults: 5, // results handed to the model (default 8)
520
+ search: false, // skip the built-in search, use DeepAI's web access
521
+ });
522
+
523
+ // Bring your own search API (Brave, Serper, Tavily, …): pass its results and
524
+ // the built-in search is skipped — the sources block is built from them.
525
+ await ai.searchWeb('coffee', {
526
+ results: [{ title, url, description, date }],
492
527
  });
493
528
  ```
494
529
 
530
+ Engine-wide settings in the constructor:
531
+
532
+ ```js
533
+ new AlexaAI({
534
+ webSearch: true, // false = never search; always use DeepAI's
535
+ webSearchProviders: ['bing', 'bing-news', 'wikipedia', 'google-news', 'duckduckgo'],
536
+ webSearchTimeout: 8000, // per provider, ms
537
+ webSearchResults: 8, // results handed to the model
538
+ webSearchProvider: async (query, { maxResults, signal }) => [...], // replace the built-ins
539
+ });
540
+ ```
541
+
542
+ The built-in providers are public pages and feeds, so they can change or be
543
+ rate-limited; each one is best-effort and a failure just means fewer results.
544
+ When none return anything the request falls back to DeepAI's server-side web
545
+ access — which on the free models is exactly the behaviour that motivated this
546
+ design: it often skips the search and writes a plausible report with invented
547
+ links. Check `grounded` if that matters to you.
548
+
495
549
  ### Moderation and administration
496
550
 
497
551
  ```js
@@ -601,7 +655,8 @@ AlexaAI orchestrator; the only class the bot touches
601
655
  │ no "Alexa Mini", no self-denial)
602
656
  ├── AmnesiaGuard never lets her deny a memory she actually has
603
657
  ├── ImageDescriber vision chain: documents -> DeepAI -> OCR -> fallback
604
- ├── WebAnswer searchWeb prompt; lifts "Sources:" lists out of prose
658
+ ├── WebSearch the engine's own web search (Bing, Bing/Google News, Wikipedia, DuckDuckGo)
659
+ ├── WebAnswer searchWeb prompt, citation handling, sources block
605
660
  ├── Media normalises every media input shape
606
661
  └── JidParser normalises @lid / @s.whatsapp.net / @g.us
607
662
  ```
@@ -770,7 +825,7 @@ refresh it automatically. Reinstall and verify:
770
825
  ```bash
771
826
  npm uninstall alexa-ai
772
827
  npm install github:AlexaInc/deepai
773
- node -e "console.log(require('alexa-ai').version)" # must print 2.1.1 or newer
828
+ node -e "console.log(require('alexa-ai').version)" # must print 2.1.2 or newer
774
829
  ```
775
830
 
776
831
  `AlexaAI.version` and `AlexaAI.methods()` let the bot assert this at startup;
@@ -780,15 +835,53 @@ node -e "console.log(require('alexa-ai').version)" # must print 2.1.1 or newer
780
835
  `text`**
781
836
  Fixed in 2.1.1. Earlier builds asked the model for *"a short, direct answer"*
782
837
  and only read sources from DeepAI's structured packet, which `gpt-4o-mini`
783
- does not send. Reinstall as above and confirm the version is 2.1.1 or newer.
784
- If a particular model still answers briefly, check `attempts` / `words` in the
785
- result: the engine retries once below `minWords`, and a persistently short
786
- model is best swapped with the `model` option.
838
+ does not send. If a particular model still answers briefly, check `attempts` /
839
+ `words` in the result: the engine retries once below `minWords`, and a
840
+ persistently short model is best swapped with the `model` option.
841
+
842
+ **`searchWeb()` sometimes has no sources, or the sources are links that do
843
+ not exist**
844
+ Fixed in 2.1.2. Before, the model was both researcher and writer: DeepAI's
845
+ server-side search ran unreliably on the free models, so the model often
846
+ wrote from memory — one run had no sources, the next had four invented ones.
847
+ The engine now searches the web itself and the sources block is built only
848
+ from those results. If `sources` is still empty, look at `grounded` in the
849
+ result: `false` means none of the search providers answered from your host
850
+ (firewall, DNS, rate limit — enable `debug:true` to see each provider's
851
+ error), and the reply came from DeepAI's own web access. Point
852
+ `webSearchProvider` at a search API you control if the public endpoints are
853
+ blocked where the bot runs.
854
+
855
+ **Works in the browser playground but fails in Postman / Node.js** — three
856
+ separate requirements:
857
+
858
+ 1. **Anonymous keys are single-use and User-Agent-bound.** Each key is
859
+ `tryit-<digits>-<hash>` with
860
+ `hash = H(UA + H(UA + H(UA + digits + "hackers_become_a_little_stinkier_every_time_they_hack")))`,
861
+ validated against the request's `User-Agent` header. A reused key or
862
+ random hex → `401 "Please pass a valid Api-Key"`. A fresh, correctly
863
+ hashed key must be minted per request — `headers()` does this
864
+ automatically for `tryit-…` keys.
865
+ 2. **The body must be `multipart/form-data`** (never JSON), with
866
+ `generation_source=img` (model page) or `generation_source=chat` +
867
+ `width`/`height`/`image_generator_version=hd`/`quality=true` (chat page).
868
+ Never set `Content-Type` manually — the multipart boundary is generated.
869
+ 3. **`Origin: https://deepai.org` is required**, and anonymous generation is
870
+ refused from datacenter/VPN IPs (`401 "Please try this model on deepai.org"`) —
871
+ run from a residential IP. Passing `deviceId` (the `deepai_device_id`
872
+ cookie value) also helps the per-device quota.
873
+
874
+ `examples/text2img-standalone.js` is a zero-dependency CLI implementing the
875
+ full recipe.
787
876
 
788
877
  **`generateImage()` returns `{ ok: false, error: 'DEEPAI_QUOTA_EXCEEDED' }`**
789
- Both routes were refused: `/api/text2img` needs credits and the in-chat image
790
- tool hit the key's chat quota. Add more keys (`keys: [...]`) or wait for the
791
- quota to reset. `message` carries DeepAI's exact wording.
878
+ `/api/text2img` is Pro-only for registered keys ("APIs are only available for
879
+ Pro members in good standing"), the anonymous browser-shaped retry was
880
+ refused too (DeepAI soft-blocks free image generation from datacenter IPs
881
+ with "Please try this model on deepai.org" — run the bot from a residential
882
+ IP), and the in-chat tool produced no image. Add a Pro key, more keys
883
+ (`keys: [...]`), or run from a non-datacenter IP. `message` carries DeepAI's
884
+ exact wording for each attempted route.
792
885
 
793
886
  **Photos are answered with "I can't view images right now"**
794
887
  Native vision needs a paid DeepAI key; on a free key only text inside the
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * text2img-standalone.js — zero-dependency DeepAI text-to-image CLI.
5
+ *
6
+ * Speaks the anonymous browser dialect: a fresh single-use `tryit-…` key
7
+ * (hashed over the User-Agent) per run, browser-identical headers and a
8
+ * multipart/form-data body, plus a stable `deepai_device_id` cookie.
9
+ *
10
+ * Usage:
11
+ * node examples/text2img-standalone.js "a cute orange cat"
12
+ * node examples/text2img-standalone.js "a cat" --out cat.jpg
13
+ * node examples/text2img-standalone.js "a cat" --aspect 16:9
14
+ * node examples/text2img-standalone.js "a cat" --device-id <cookieValue>
15
+ * node examples/text2img-standalone.js "a cat" --key <proKey>
16
+ *
17
+ * Requires Node.js 18+ (global fetch). Anonymous generation is refused
18
+ * from datacenter/VPN IPs; run from a residential network.
19
+ */
20
+
21
+ const API_URL = 'https://api.deepai.org/api/text2img';
22
+ const SALT = 'hackers_become_a_little_stinkier_every_time_they_hack';
23
+ // Keep this EXACT string in sync with the User-Agent header below — the key
24
+ // hash is computed over it and the server recomputes it from the request.
25
+ const USER_AGENT =
26
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Deterministic key hash (see DeepAIClient._islandHash in the engine).
30
+ // The integer/bit-level behaviour is intentional — do not simplify it.
31
+ // ---------------------------------------------------------------------------
32
+ function islandHash(input) {
33
+ const a = [];
34
+ for (let b = 0; 64 > b; ) a[b] = 0 | (4294967296 * Math.sin(++b % Math.PI));
35
+ let d, e, f, g = [(d = 1732584193), (e = 4023233417), ~d, ~e], h = [];
36
+ const l = unescape(encodeURI(input)) + '€';
37
+ let k = l.length;
38
+ let c = (--k / 4 + 2) | 15;
39
+ for (h[--c] = 8 * k; ~k; ) h[k >> 2] |= l.charCodeAt(k) << (8 * k--);
40
+ for (let b = 0, m = 0; b < c; b += 16) {
41
+ for (k = g; 64 > m; k = [ (f = k[3]), d + (((f = k[0] + [d & e | ~d & f, f & d | ~f & e, d ^ e ^ f, e ^ (d | ~f)][(k = m >> 4)] + a[m] + ~~h[b | [m, 5 * m + 1, 3 * m + 5, 7 * m][k] & 15]) << (k = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21][4 * k + (m++ % 4)])) | (f >>> -k)), d, e ]) {
42
+ d = k[1] | 0;
43
+ e = k[2];
44
+ }
45
+ for (m = 4; m; ) g[--m] += k[m];
46
+ }
47
+ let result = '';
48
+ for (let i = 0; 32 > i; ) result += ((g[i >> 3] >> 4 * (1 ^ i++)) & 15).toString(16);
49
+ return result.split('').reverse().join('');
50
+ }
51
+
52
+ /** Mint a fresh anonymous key, valid for exactly ONE request with USER_AGENT. */
53
+ function freshTryItKey(userAgent = USER_AGENT) {
54
+ const digits = String(Math.round(Math.random() * 100000000000));
55
+ const H = islandHash;
56
+ return `tryit-${digits}-${H(userAgent + H(userAgent + H(userAgent + digits + SALT)))}`;
57
+ }
58
+
59
+ /** Random device id, same shape the site sets as the deepai_device_id cookie. */
60
+ function randomDeviceId() {
61
+ return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // CLI
66
+ // ---------------------------------------------------------------------------
67
+ function parseArgs(argv) {
68
+ const opts = { prompt: '', out: null, key: null, deviceId: randomDeviceId(), aspect: null, genSource: 'img' };
69
+ for (let i = 0; i < argv.length; i++) {
70
+ const a = argv[i];
71
+ if (a === '--out') opts.out = argv[++i];
72
+ else if (a === '--key') opts.key = argv[++i];
73
+ else if (a === '--device-id') opts.deviceId = argv[++i];
74
+ else if (a === '--aspect') opts.aspect = argv[++i];
75
+ else if (a === '--chat-source') opts.genSource = 'chat';
76
+ else if (a === '--help' || a === '-h') opts.help = true;
77
+ else if (!opts.prompt) opts.prompt = a;
78
+ }
79
+ return opts;
80
+ }
81
+
82
+ const ASPECTS = { '16:9': [832, 448], '4:3': [768, 576], '1:1': [640, 640], '3:4': [576, 768], '9:16': [448, 832] };
83
+
84
+ async function main() {
85
+ const opts = parseArgs(process.argv.slice(2));
86
+ if (opts.help || !opts.prompt) {
87
+ console.log('Usage: node examples/text2img-standalone.js "your prompt" [--out file.jpg] [--key PRO_KEY] [--device-id VALUE] [--aspect 16:9|1:1|9:16|4:3|3:4] [--chat-source]');
88
+ process.exit(opts.help ? 0 : 1);
89
+ }
90
+
91
+ // ---- body: multipart/form-data, NEVER JSON -----------------------------
92
+ const form = new FormData();
93
+ form.append('text', opts.prompt);
94
+ form.append('generation_source', opts.genSource); // 'img' = model page, 'chat' = chat page
95
+ if (opts.aspect && ASPECTS[opts.aspect]) {
96
+ // the chat-page dialect maps aspect ratios to pixel sizes
97
+ const [w, h] = ASPECTS[opts.aspect];
98
+ form.append('width', String(w));
99
+ form.append('height', String(h));
100
+ form.append('image_generator_version', 'hd');
101
+ form.append('quality', 'true');
102
+ }
103
+
104
+ // ---- headers: fresh single-use anonymous key (or your Pro key) ---------
105
+ const apiKey = opts.key || freshTryItKey();
106
+ const headers = {
107
+ 'api-key': apiKey,
108
+ 'User-Agent': USER_AGENT, // MUST match the UA the key was hashed with
109
+ Origin: 'https://deepai.org',
110
+ Referer: 'https://deepai.org/machine-learning-model/text2img',
111
+ Accept: '*/*',
112
+ 'Accept-Language': 'en-US,en;q=0.9',
113
+ Cookie: `deepai_device_id=${opts.deviceId}`,
114
+ // NOTE: do NOT set Content-Type yourself — undici adds the multipart
115
+ // boundary. A manual Content-Type without the boundary is rejected.
116
+ };
117
+
118
+ console.log(`Prompt : ${opts.prompt}`);
119
+ console.log(`Key : ${opts.key ? '(registered key — needs Pro)' : apiKey + ' (fresh, single-use)'}`);
120
+ console.log('POST : ' + API_URL);
121
+
122
+ const res = await fetch(API_URL, { method: 'POST', headers, body: form });
123
+ const raw = await res.text();
124
+ let data = null;
125
+ try { data = JSON.parse(raw); } catch { /* non-JSON */ }
126
+
127
+ if (!res.ok || data?.err || (data?.status && !data.share_url && !data.output_url)) {
128
+ console.error(`\nFAILED HTTP ${res.status}`);
129
+ console.error(raw.slice(0, 500));
130
+ const s = String(data?.status || data?.err || '');
131
+ if (/valid Api-Key/i.test(s)) console.error('\n→ The key is invalid or already used. Keys are single-use and must be hashed for the exact User-Agent sent; this script mints a fresh one each run.');
132
+ else if (/try this model on deepai\.org/i.test(s)) console.error('\n→ DeepAI is refusing anonymous generation from your IP (datacenter/VPN) or the Origin header is missing. Run from a residential IP and keep the Origin/Referer headers.');
133
+ else if (/Pro members/i.test(s)) console.error('\n→ Your registered key is on the free plan; /api/* needs Pro. Use the anonymous mode (omit --key) or upgrade.');
134
+ else if (/try it exceeded/i.test(s)) console.error('\n→ Free quota for this device/IP is exhausted. Try a different --device-id or wait for the reset.');
135
+ process.exit(1);
136
+ }
137
+
138
+ const url = data.share_url || data.output_url;
139
+ console.log(`\nOK HTTP ${res.status}`);
140
+ console.log(`Image : ${url}`);
141
+ if (data.id) console.log(`ID : ${data.id}`);
142
+
143
+ // ---- optional: download the image --------------------------------------
144
+ const out = opts.out || `deepai-${Date.now()}.jpg`;
145
+ try {
146
+ const img = await fetch(url);
147
+ if (img.ok) {
148
+ const buf = Buffer.from(await img.arrayBuffer());
149
+ require('fs').writeFileSync(out, buf);
150
+ console.log(`Saved : ${out} (${(buf.length / 1024).toFixed(1)} KB)`);
151
+ } else {
152
+ console.log(`(download skipped: HTTP ${img.status} — open the URL above in a browser)`);
153
+ }
154
+ } catch (e) {
155
+ console.log(`(download failed: ${e.message} — open the URL above in a browser)`);
156
+ }
157
+ }
158
+
159
+ main().catch((e) => { console.error('ERROR:', e.message); process.exit(1); });
package/index.js CHANGED
@@ -33,6 +33,7 @@ const ResponseFormatter = require('./src/services/ResponseFormatter');
33
33
  const TriggerDetector = require('./src/services/TriggerDetector');
34
34
  const ImageDescriber = require('./src/services/ImageDescriber');
35
35
  const WebAnswer = require('./src/services/WebAnswer');
36
+ const WebSearch = require('./src/services/WebSearch');
36
37
  const JidParser = require('./src/utils/JidParser');
37
38
  const Media = require('./src/utils/Media');
38
39
  const StreamParser = require('./src/core/StreamParser');
@@ -63,6 +64,7 @@ module.exports.ResponseFormatter = ResponseFormatter;
63
64
  module.exports.TriggerDetector = TriggerDetector;
64
65
  module.exports.ImageDescriber = ImageDescriber;
65
66
  module.exports.WebAnswer = WebAnswer;
67
+ module.exports.WebSearch = WebSearch;
66
68
  module.exports.JidParser = JidParser;
67
69
  module.exports.Media = Media;
68
70
  module.exports.StreamParser = StreamParser;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alexa-ai",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "description": "AI engine for the Alexa WhatsApp bot: DeepAI-powered chat with PostgreSQL-backed long-term memory, cross-chat identity (@lid <-> phone), vision/OCR, image generation and web search.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -40,6 +40,7 @@
40
40
  "index.js",
41
41
  "src/",
42
42
  "examples/",
43
+ "test/",
43
44
  "README.md",
44
45
  "CHANGELOG.md",
45
46
  "LICENSE"