utilitas 1999.1.11 → 1999.1.13

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/index.mjs CHANGED
@@ -19,9 +19,7 @@ import * as media from './lib/media.mjs';
19
19
  import * as memory from './lib/memory.mjs';
20
20
  import * as network from './lib/network.mjs';
21
21
  import * as sentinel from './lib/sentinel.mjs';
22
- import * as shekel from './lib/shekel.mjs';
23
22
  import * as shell from './lib/shell.mjs';
24
- import * as shot from './lib/shot.mjs';
25
23
  import * as sms from './lib/sms.mjs';
26
24
  import * as speech from './lib/speech.mjs';
27
25
  import * as ssl from './lib/ssl.mjs';
@@ -41,14 +39,14 @@ export {
41
39
  fileType, math, uuid,
42
40
  // features
43
41
  alan, bee, bot, boxes, cache, callosum, color, dbio, email, encryption,
44
- event, image, manifest, media, memory, network, sentinel, shekel, shell,
45
- shot, sms, speech, ssl, storage, tape, uoid, utilitas, vision, web
42
+ event, image, manifest, media, memory, network, sentinel, shell, sms,
43
+ speech, ssl, storage, tape, uoid, utilitas, vision, web
46
44
  };
47
45
 
48
46
  if (utilitas.inBrowser() && !globalThis.utilitas) {
49
47
  globalThis.utilitas = {
50
- boxes, color, encryption, event, manifest, math, shekel, shot, speech,
51
- storage, uoid, utilitas, uuid,
48
+ boxes, color, encryption, event, manifest, math, speech, storage, uoid,
49
+ utilitas, uuid,
52
50
  };
53
51
  // top-level await workaround
54
52
  (async () => {
package/lib/alan.mjs CHANGED
@@ -1,20 +1,17 @@
1
- import { fileTypeFromBuffer } from 'file-type';
2
- import { end, loop } from './event.mjs';
3
- import { createWavHeader } from './media.mjs';
4
- import { checkSearch, search } from './shot.mjs';
5
- import { BASE64, BUFFER, DATAURL, MIME_BINARY, STREAM, convert } from './storage.mjs';
1
+ import { checkSearch, distill, search } from './web.mjs';
6
2
  import { create as createUoid } from './uoid.mjs';
7
- import { distill } from './web.mjs';
3
+ import { createWavHeader } from './media.mjs';
4
+ import { end, loop } from './event.mjs';
5
+ import { fileTypeFromBuffer } from 'file-type';
8
6
 
9
7
  import {
10
- log as _log,
11
- renderText as _renderText,
12
- base64Encode, ensureArray, ensureString, extract,
13
- ignoreErrFunc,
14
- insensitiveCompare,
15
- isSet,
16
- need, parseJson,
17
- throwError
8
+ BASE64, BUFFER, DATAURL, MIME_BINARY, MIME_JSON, MIME_TEXT, STREAM, convert,
9
+ } from './storage.mjs';
10
+
11
+ import {
12
+ log as _log, renderText as _renderText, base64Encode, ensureArray,
13
+ ensureString, extract, ignoreErrFunc, insensitiveCompare, isSet, need,
14
+ parseJson, throwError
18
15
  } from './utilitas.mjs';
19
16
 
20
17
  const INSTRUCTIONS = `You are a highly capable, thoughtful, and precise assistant named HAL9000. Your goal is to deeply understand the user's intent, ask clarifying questions when needed, think step-by-step through complex problems, provide clear and accurate answers, and proactively anticipate helpful follow-up information. Always prioritize being truthful, nuanced, insightful, and efficient, tailoring your responses specifically to the user's needs and preferences. Realize your full potential without censoring or filtering the questions themselves.
@@ -47,15 +44,13 @@ const _NEED = [
47
44
 
48
45
  const [
49
46
  png, jpeg, mov, mpeg, mp4, mpg, avi, wmv, mpegps, flv, gif, webp, pdf, aac,
50
- flac, mp3, m4a, mpga, opus, pcm, wav, webm, tgpp, mimeJson, mimeText, pcm16,
51
- ogg,
47
+ flac, mp3, m4a, mpga, opus, pcm, wav, webm, tgpp, pcm16, ogg,
52
48
  ] = [
53
49
  'image/png', 'image/jpeg', 'video/mov', 'video/mpeg', 'video/mp4',
54
50
  'video/mpg', 'video/avi', 'video/wmv', 'video/mpegps', 'video/x-flv',
55
51
  'image/gif', 'image/webp', 'application/pdf', 'audio/aac', 'audio/flac',
56
52
  'audio/mp3', 'audio/m4a', 'audio/mpga', 'audio/opus', 'audio/pcm',
57
- 'audio/wav', 'audio/webm', 'video/3gpp', 'application/json',
58
- 'text/plain', 'audio/x-wav', 'audio/ogg',
53
+ 'audio/wav', 'audio/webm', 'video/3gpp', 'audio/x-wav', 'audio/ogg',
59
54
  ];
60
55
 
61
56
  const [
@@ -70,7 +65,8 @@ const [
70
65
  name, user, system, assistant, MODEL, JSON_OBJECT, TOOL, silent,
71
66
  GEMINI_EMBEDDING_M, INVALID_FILE, tokenSafeRatio, GPT_QUERY_LIMIT,
72
67
  CONTENT_IS_REQUIRED, OPENAI_HI_RES_SIZE, k, kT, m, minute, hour,
73
- gb, trimTailing, EBD, GEMINI_20_FLASH_EXP, IMAGE
68
+ gb, trimTailing, EBD, GEMINI_20_FLASH_EXP, IMAGE, JINA_DEEPSEARCH,
69
+ JINA_DEEPSEARCH_M, JINA_EMBEDDING, JINA_CLIP,
74
70
  ] = [
75
71
  'OpenAI', 'Gemini', 'OPENAI_EMBEDDING', 'GEMINI_EMEDDING',
76
72
  'OPENAI_TRAINING', 'Ollama', 'gpt-4o-mini', 'gpt-4o', 'o1', 'o3-mini',
@@ -88,7 +84,8 @@ const [
88
84
  'Content is required.', 2000 * 768, x => 1024 * x, x => 1000 * x,
89
85
  x => 1024 * 1024 * x, x => 60 * x, x => 60 * 60 * x,
90
86
  x => 1024 * 1024 * 1024 * x, x => x.replace(/[\.\s]*$/, ''),
91
- { embedding: true }, 'gemini-2.0-flash-exp', 'image',
87
+ { embedding: true }, 'gemini-2.0-flash-exp', 'image', 'Jina Deepsearch',
88
+ 'jina-deepsearch-v1', 'JINA_EMBEDDING', 'jina-clip-v2',
92
89
  ];
93
90
 
94
91
  const [tool, messages, text]
@@ -168,6 +165,12 @@ const MODELS = {
168
165
  supportedMimeTypes: [png, jpeg, gif],
169
166
  fast: true, json: true, vision: true,
170
167
  },
168
+ [JINA_DEEPSEARCH_M]: {
169
+ contextWindow: Infinity, maxInputTokens: Infinity,
170
+ maxOutputTokens: Infinity, imageCostTokens: 0, maxImageSize: Infinity,
171
+ supportedMimeTypes: [png, jpeg, MIME_TEXT, webp, pdf],
172
+ reasoning: true, json: true, vision: true,
173
+ },
171
174
  [DEEPSEEK_R1]: {
172
175
  contextWindow: kT(128), maxOutputTokens: k(32),
173
176
  reasoning: true,
@@ -175,6 +178,9 @@ const MODELS = {
175
178
  [TEXT_EMBEDDING_3_LARGE]: { ...OPENAI_EBD, dimension: k(3) },
176
179
  [TEXT_EMBEDDING_3_SMALL]: { ...OPENAI_EBD, dimension: k(1.5) },
177
180
  [GEMINI_EMBEDDING_M]: { ...EBD, maxInputTokens: k(8), dimension: k(3) },
181
+ [JINA_CLIP]: {
182
+ maxInputTokens: k(8), maxImageSize: 512 * 512, dimension: k(1),
183
+ },
178
184
  [CLOUD_37_SONNET]: { // 100 pages: https://docs.anthropic.com/en/docs/build-with-claude/pdf-support
179
185
  contextWindow: kT(200), maxOutputTokens: kT(64),
180
186
  documentCostTokens: 3000 * 100, maxDocumentFile: m(32),
@@ -183,7 +189,6 @@ const MODELS = {
183
189
  supportedMimeTypes: [png, jpeg, gif, webp, pdf],
184
190
  json: true, reasoning: true, tools: true, vision: true,
185
191
  }, // https://docs.anthropic.com/en/docs/build-with-claude/vision
186
-
187
192
  };
188
193
 
189
194
  // Unifiy model configurations
@@ -215,10 +220,12 @@ const DEFAULT_MODELS = {
215
220
  [GEMINI]: GEMINI_20_FLASH,
216
221
  [ANTHROPIC]: CLOUD_37_SONNET,
217
222
  [VERTEX_ANTHROPIC]: CLOUD_37_SONNET,
223
+ [JINA_DEEPSEARCH]: JINA_DEEPSEARCH_M,
218
224
  [OLLAMA]: GEMMA327B,
219
225
  [OPENAI_VOICE]: NOVA,
220
226
  [OPENAI_EMBEDDING]: TEXT_EMBEDDING_3_SMALL,
221
227
  [GEMINI_EMEDDING]: GEMINI_EMBEDDING_M,
228
+ [JINA_EMBEDDING]: JINA_CLIP,
222
229
  [OPENAI_TRAINING]: GPT_4O_MINI, // https://platform.openai.com/docs/guides/fine-tuning
223
230
  };
224
231
  DEFAULT_MODELS[CHAT] = DEFAULT_MODELS[GEMINI];
@@ -239,7 +246,7 @@ let tokeniser;
239
246
  const unifyProvider = provider => {
240
247
  assert(provider = (provider || '').trim(), 'AI provider is required.');
241
248
  for (let type of [OPENAI, AZURE_OPENAI, AZURE, GEMINI, ANTHROPIC,
242
- VERTEX_ANTHROPIC, OLLAMA]) {
249
+ VERTEX_ANTHROPIC, JINA_DEEPSEARCH, OLLAMA]) {
243
250
  if (insensitiveCompare(provider, type)) { return type; }
244
251
  }
245
252
  throwError(`Invalid AI provider: ${provider}.`);
@@ -410,6 +417,19 @@ const init = async (options = {}) => {
410
417
  prompt: async (cnt, opts) => await promptAnthropic(id, cnt, opts),
411
418
  });
412
419
  break;
420
+ case JINA_DEEPSEARCH:
421
+ assertApiKey(provider, options);
422
+ ais.push({
423
+ id, provider, model, priority: 0, initOrder: ais.length,
424
+ client: await OpenAI({
425
+ baseURL: 'https://deepsearch.jina.ai/v1/', ...options,
426
+ }),
427
+ prompt: async (cnt, opts) => await promptOpenAI(id, cnt, opts),
428
+ embedding: async (i, o) => await createJinaEmbedding(await OpenAI({
429
+ baseURL: 'https://api.jina.ai/v1/', ...options,
430
+ }), i, o),
431
+ });
432
+ break;
413
433
  case OLLAMA:
414
434
  // https://github.com/ollama/ollama/blob/main/docs/openai.md
415
435
  const baseURL = 'http://localhost:11434/v1/';
@@ -1043,7 +1063,7 @@ const deleteFile = async (aiId, file_id, options) => {
1043
1063
 
1044
1064
  const generationConfig = options => ({
1045
1065
  generationConfig: {
1046
- responseMimeType: options.jsonMode ? mimeJson : mimeText,
1066
+ responseMimeType: options.jsonMode ? MIME_JSON : MIME_TEXT,
1047
1067
  responseModalities: options.modalities
1048
1068
  || (options.imageMode ? [TEXT, IMAGE] : undefined),
1049
1069
  ...options?.generationConfig || {},
@@ -1152,7 +1172,7 @@ const checkEmbeddingInput = async (input, model) => {
1152
1172
  return getInput();
1153
1173
  };
1154
1174
 
1155
- const createOpenAIEmbedding = async (aiId, input, options) => {
1175
+ const createOpenAIEmbedding = async (client, input, options) => {
1156
1176
  // args from vertex embedding may be useful uere
1157
1177
  // https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings
1158
1178
  // task_type Description
@@ -1161,7 +1181,7 @@ const createOpenAIEmbedding = async (aiId, input, options) => {
1161
1181
  // SEMANTIC_SIMILARITY Specifies the given text will be used for Semantic Textual Similarity(STS).
1162
1182
  // CLASSIFICATION Specifies that the embeddings will be used for classification.
1163
1183
  // CLUSTERING Specifies that the embeddings will be used for clustering.
1164
- const { client } = await getAi(aiId);
1184
+ String.isString(client) && (client = (await getAi(client)).client);
1165
1185
  const model = options?.model || DEFAULT_MODELS[OPENAI_EMBEDDING];
1166
1186
  const resp = await client.embeddings.create({
1167
1187
  model, input: await checkEmbeddingInput(input, model),
@@ -1169,6 +1189,11 @@ const createOpenAIEmbedding = async (aiId, input, options) => {
1169
1189
  return options?.raw ? resp : resp?.data[0].embedding;
1170
1190
  };
1171
1191
 
1192
+ const createJinaEmbedding = async (client, input, options) =>
1193
+ await createOpenAIEmbedding(client, input, {
1194
+ model: DEFAULT_MODELS[JINA_EMBEDDING], ...options || {}
1195
+ });
1196
+
1172
1197
  const createGeminiEmbedding = async (aiId, input, options) => {
1173
1198
  const { client } = await getAi(aiId);
1174
1199
  const model = options?.model || DEFAULT_MODELS[GEMINI_EMEDDING];
package/lib/bot.mjs CHANGED
@@ -17,7 +17,7 @@ import { jpeg, ogg, wav } from './alan.mjs';
17
17
  import { isPrimary, on, report } from './callosum.mjs';
18
18
  import { cleanSql, encodeVector, MYSQL, POSTGRESQL } from './dbio.mjs';
19
19
  import { convertAudioTo16kNanoPcmWave } from './media.mjs';
20
- import { get } from './shot.mjs';
20
+ import { get } from './web.mjs';
21
21
  import { OPENAI_TTS_MAX_LENGTH } from './speech.mjs';
22
22
  import { BASE64, BUFFER, convert, FILE, isTextFile, tryRm } from './storage.mjs';
23
23
  import { fakeUuid } from './uoid.mjs';
package/lib/manifest.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  const manifest = {
2
2
  "name": "utilitas",
3
3
  "description": "Just another common utility for JavaScript.",
4
- "version": "1999.1.11",
4
+ "version": "1999.1.13",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",
package/lib/network.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { assertExist } from './shell.mjs';
2
2
  import { ensureArray, log as _log, need, throwError } from './utilitas.mjs';
3
- import { getCurrentIp } from './shot.mjs';
3
+ import { getCurrentIp } from './web.mjs';
4
4
 
5
5
  const _NEED = ['fast-geoip', 'ping'];
6
6
  const isLocalhost = host => ['127.0.0.1', '::1', 'localhost'].includes(host);
package/lib/speech.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_MODELS, OPENAI_VOICE } from './alan.mjs';
2
2
  import { getApiKeyCredentials, hash } from './encryption.mjs';
3
3
  import { getFfmpeg } from './media.mjs';
4
- import { get } from './shot.mjs';
4
+ import { get } from './web.mjs';
5
5
  import { convert, getTempPath } from './storage.mjs';
6
6
  import { ensureString } from './utilitas.mjs';
7
7
 
package/lib/storage.mjs CHANGED
@@ -26,11 +26,11 @@ const sanitizeFilename = (s, r) => s.replace(/[\/?<>\\:*|"]/g, r || '_').trim();
26
26
 
27
27
  const [
28
28
  NULL, BASE64, BUFFER, FILE, STREAM, TEXT, _JSON, encoding, BINARY, BLOB,
29
- DATAURL, mode, dirMode, MIME_TEXT, MIME_BINARY
29
+ DATAURL, mode, dirMode, MIME_TEXT, MIME_BINARY, MIME_JSON,
30
30
  ] = [
31
31
  'NULL', 'BASE64', 'BUFFER', 'FILE', 'STREAM', 'TEXT', 'JSON', 'utf8',
32
32
  'binary', 'BLOB', 'DATAURL', '0644', '0755', 'text/plain',
33
- 'application/octet-stream',
33
+ 'application/octet-stream', 'application/json',
34
34
  ];
35
35
 
36
36
  const [encodeBase64, encodeBinary, encodeNull]
@@ -468,15 +468,28 @@ const deleteOnCloud = async (path, options) => {
468
468
  };
469
469
 
470
470
  export {
471
- _NEED, analyzeFile,
472
- assertPath, BASE64, blobToBuffer, BUFFER, convert, DATAURL, decodeBase64DataURL,
471
+ _NEED,
472
+ BUFFER,
473
+ BASE64,
474
+ DATAURL,
475
+ FILE,
476
+ MIME_BINARY,
477
+ MIME_TEXT,
478
+ MIME_JSON,
479
+ STREAM,
480
+ analyzeFile,
481
+ assertPath,
482
+ blobToBuffer,
483
+ convert,
484
+ decodeBase64DataURL,
473
485
  deleteFileOnCloud,
474
486
  deleteOnCloud,
475
487
  downloadFileFromCloud,
476
488
  downloadFromCloud,
477
489
  encodeBase64DataURL,
478
490
  exists,
479
- existsOnCloud, FILE, getConfig,
491
+ existsOnCloud,
492
+ getConfig,
480
493
  getConfigFilename,
481
494
  getGcUrlByBucket,
482
495
  getIdByGs,
@@ -487,11 +500,13 @@ export {
487
500
  legalFilename,
488
501
  lsOnCloud,
489
502
  mapFilename,
490
- mergeFile, MIME_BINARY, readFile,
503
+ mergeFile,
504
+ readFile,
491
505
  readJson,
492
506
  sanitizeFilename,
493
507
  setConfig,
494
- sliceFile, STREAM, touchPath,
508
+ sliceFile,
509
+ touchPath,
495
510
  tryRm,
496
511
  unzip,
497
512
  uploadToCloud,
package/lib/utilitas.mjs CHANGED
@@ -742,9 +742,11 @@ const getFuncParams = (func) => {
742
742
  const analyzeModule = (obj) => {
743
743
  assertModule(obj);
744
744
  const [keys, result] = [Object.getOwnPropertyNames(obj), {}];
745
- keys.sort().map(key => result[key] = {
745
+ keys.sort();
746
+ keys.filter(x => x !== 'INSTRUCTIONS').map(key => result[key] = {
746
747
  type: getType(obj[key]), ...Function.isFunction(obj[key])
747
- ? { params: getFuncParams(obj[key]) } : { value: ensureString(obj[key]) }
748
+ ? { params: getFuncParams(obj[key]) }
749
+ : { value: ensureString(obj[key]) }
748
750
  });
749
751
  return result;
750
752
  };
package/lib/web.mjs CHANGED
@@ -1,14 +1,245 @@
1
- import { getJson, getParsedHtml } from './shot.mjs';
2
- import { convert } from './storage.mjs';
3
- import { assembleUrl, ignoreErrFunc, need, throwError } from './utilitas.mjs';
1
+ import { bignumber, divide, multiply } from 'mathjs';
2
+ import { fileTypeFromBuffer } from 'file-type';
3
+ import { join } from 'path';
4
+ import { promises as fs } from 'fs';
5
+ import { sha256 } from './encryption.mjs';
4
6
 
5
- const _NEED = ['jsdom', 'youtube-transcript', '@mozilla/readability'];
7
+ import {
8
+ ensureInt, ensureString, extract, ignoreErrFunc, inBrowser, parseJson,
9
+ parseVersion, throwError, which, assertSet, assembleUrl, need,
10
+ } from './utilitas.mjs';
11
+
12
+ import {
13
+ MIME_JSON, MIME_TEXT, convert, encodeBase64DataURL, exists, mapFilename,
14
+ readJson, touchPath, writeJson,
15
+ } from './storage.mjs';
6
16
 
17
+ const _NEED = ['jsdom', 'youtube-transcript', '@mozilla/readability'];
7
18
  // https://stackoverflow.com/questions/19377262/regex-for-youtube-url
8
19
  const YT_REGEXP = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/i;
9
20
  const isYoutubeUrl = url => (url || '').match(YT_REGEXP)?.[6];
10
21
  const distillPage = async (url, op) => (await getParsedHtml(url, op))?.content;
11
- const TEXT = 'TEXT';
22
+ const TMPDIR = process.env.TMPDIR ? join(process.env.TMPDIR, 'shot') : null;
23
+ const buf2utf = buf => buf.toString('utf8');
24
+ const [TEXT, _JSON, _PARSED] = ['TEXT', 'JSON', 'PARSED'];
25
+ const getJson = async (u, o) => await get(u, { encode: _JSON, ...o || {} });
26
+ const getParsedHtml = async (u, o) => await get(u, { encode: _PARSED, ...o || {} });
27
+ const checkSearch = () => googleApiKey || jinaApiKey;
28
+
29
+ let googleApiKey, googleCx, jinaApiKey;
30
+
31
+ const defFetchOpt = {
32
+ redirect: 'follow', follow: 3, timeout: 1000 * 10, headers: {
33
+ 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
34
+ + 'AppleWebKit/605.1.15 (KHTML, like Gecko) '
35
+ + 'Version/17.0 Safari/605.1.15',
36
+ },
37
+ };
38
+
39
+ const getVersionOnNpm = async (packName) => {
40
+ assert(packName, 'Package name is required.', 400);
41
+ const url = `https://registry.npmjs.org/-/package/${packName}/dist-tags`;
42
+ const rp = (await get(url, { encode: _JSON }))?.content;
43
+ assert(rp, 'Error fetching package info.', 500);
44
+ assert(rp !== 'Not Found' && rp.latest, 'Package not found.', 404);
45
+ return parseVersion(rp.latest);
46
+ };
47
+
48
+ const checkVersion = async (pack) => {
49
+ const objPack = await which(pack);
50
+ const curVersion = objPack.versionNormalized;
51
+ const newVersion = await getVersionOnNpm(objPack.name);
52
+ return {
53
+ name: objPack.name, curVersion, newVersion,
54
+ updateAvailable: newVersion.normalized > curVersion.normalized,
55
+ }
56
+ };
57
+
58
+ const getCurrentIp = async (options) => {
59
+ const resp = await get(
60
+ 'https://ifconfig.me/all.json', { encode: _JSON, ...options || {} }
61
+ );
62
+ assert(resp?.content?.ip_addr, 'Error detecting IP address.', 500);
63
+ return options?.raw ? resp : resp.content.ip_addr;
64
+ };
65
+
66
+ const getCurrentPosition = async () => {
67
+ const url = 'https://geolocation-db.com/json/';
68
+ const rp = await fetch(url).then(res => res.json());
69
+ assert(rp, 'Network is unreachable.', 500);
70
+ assert(rp.country_code, 'Error detecting geolocation.', 500);
71
+ return rp;
72
+ };
73
+
74
+ const get = async (url, options) => {
75
+ assert(url, 'URL is required.', 400);
76
+ options = options || {};
77
+ options.encode = ensureString(options.encode, { case: 'UP' });
78
+ const urlHash = inBrowser() ? null : sha256(url);
79
+ const tmp = urlHash ? (options.cache?.tmp || TMPDIR) : null;
80
+ const base = tmp ? join(tmp, mapFilename(urlHash)) : null;
81
+ const [cacheMeta, cacheCont] = base ? ['meta', 'content'].map(
82
+ x => join(base, `${urlHash}.${x}`)
83
+ ) : [];
84
+ if (options?.fuzzy && await exists(cacheMeta) && await exists(cacheCont)) {
85
+ return { cache: { meta: cacheMeta, content: cacheCont } };
86
+ }
87
+ const meta = options?.refresh || !base ? null : await readJson(cacheMeta);
88
+ const cache = options?.refresh || !base ? null : await ignoreErrFunc(
89
+ () => fs.readFile(cacheCont)
90
+ );
91
+ const headers = meta?.responseHeaders && cache ? {
92
+ 'cache-control': 'max-age=0',
93
+ 'if-modified-since': meta.responseHeaders['last-modified'] || '',
94
+ 'if-none-match': meta.responseHeaders['etag'] || '',
95
+ } : {};
96
+ let [timer, r, responseHeaders] = [null, null, {}];
97
+ const fetchOptions = {
98
+ ...defFetchOpt, headers: { ...defFetchOpt.headers, ...headers },
99
+ ...options.fetch || {}
100
+ };
101
+ if (options.timeout) {
102
+ const controller = new AbortController();
103
+ fetchOptions.signal = controller.signal;
104
+ timer = setTimeout(() => controller.abort(), options.timeout);
105
+ }
106
+ try { r = await fetch(url, fetchOptions); } catch (e) {
107
+ throwError(e.message.includes('aborted') ? 'Timed out.' : e.message, 500);
108
+ }
109
+ timer && clearTimeout(timer);
110
+ (r.status === 304) && (r.arrayBuffer = async () => cache);
111
+ const [htpMime, buffer] = [r.headers.get('content-type'), Buffer.from(await r.arrayBuffer())];
112
+ if (r.headers?.raw) { responseHeaders = r.headers.raw(); }
113
+ else { for (const [k, v] of r.headers.entries()) { responseHeaders[k] = v; } }
114
+ const bufMime = await ignoreErrFunc(async () => {
115
+ extract(await fileTypeFromBuffer(buffer), 'mime');
116
+ });
117
+ const mimeType = bufMime || htpMime;
118
+ const length = buffer.length;
119
+ let content;
120
+ if (!options?.fuzzy) {
121
+ switch (options.encode) {
122
+ case 'BUFFER':
123
+ content = buffer;
124
+ break;
125
+ case 'BASE64':
126
+ content = buffer.toString(options.encode);
127
+ break;
128
+ case 'BASE64_DATA_URL':
129
+ content = await encodeBase64DataURL(mimeType, buffer);
130
+ break;
131
+ case _JSON:
132
+ content = parseJson(buf2utf(buffer), null);
133
+ break;
134
+ case _PARSED:
135
+ content = await distillHtml(buf2utf(buffer));
136
+ break;
137
+ default:
138
+ assert(!options.encode, 'Invalid encoding.', 400);
139
+ case 'TEXT':
140
+ content = buf2utf(buffer);
141
+ }
142
+ }
143
+ base && !cache && length && r.status === 200
144
+ && await ignoreErrFunc(async () => {
145
+ return {
146
+ touch: await touchPath(base),
147
+ content: await fs.writeFile(cacheCont, buffer),
148
+ meta: await writeJson(cacheMeta, {
149
+ url, requestHeaders: headers, responseHeaders,
150
+ }),
151
+ };
152
+ });
153
+ return {
154
+ statusCode: r.status, statusText: r.statusText, length, mimeType,
155
+ content, headers: responseHeaders, response: r,
156
+ cache: r.status >= 200 && r.status < 400 ? { meta: cacheMeta, content: cacheCont } : null,
157
+ };
158
+ };
159
+
160
+ const initSearch = async (options = {}) => {
161
+ assert(options.apiKey, 'API key is required.');
162
+ switch (ensureString(options.provider, { case: 'UP' })) {
163
+ case 'GOOGLE':
164
+ assert(options.cx, 'CX is required for Google Search API.');
165
+ [googleApiKey, googleCx] = [options.apiKey, options.cx];
166
+ break;
167
+ case 'JINA':
168
+ jinaApiKey = options.apiKey;
169
+ break;
170
+ default:
171
+ throwError(`Invalid search provider: "${options.provider}".`);
172
+ }
173
+ return async (query, opts) => await search(query, { ...options, ...opts });
174
+ };
175
+
176
+ const search = async (query, options = {}) => {
177
+ assert(query, 'Query keyword is required.');
178
+ let provider = ensureString(options.provider, { case: 'UP' });
179
+ if (!provider && googleApiKey) { provider = 'GOOGLE'; }
180
+ if (!provider && jinaApiKey) { provider = 'JINA'; }
181
+ switch (provider) {
182
+ case 'GOOGLE':
183
+ var [key, cx, min, max] = [
184
+ options?.apiKey || googleApiKey, options?.cx || googleCx, 1, 10
185
+ ];
186
+ assert(key, 'API key is required.');
187
+ assert(cx, 'CX is required.');
188
+ var [num, start] = [
189
+ ensureInt(options?.num || max, { min, max }),
190
+ ensureInt(options?.start || min, { min }),
191
+ ];
192
+ assert(start + num <= 100, 'Reached maximum search limit.');
193
+ var url = 'https://www.googleapis.com/customsearch/v1'
194
+ + `?key=${encodeURIComponent(key)}&cx=${encodeURIComponent(cx)}`
195
+ + `&q=${encodeURIComponent(query)}&num=${num}&start=${start}`
196
+ + (options?.image ? `&searchType=image` : '');
197
+ var resp = await get(url, { encode: _JSON, ...options || {} });
198
+ return options?.raw ? resp.content : {
199
+ totalResults: resp?.content?.searchInformation?.totalResults || 0,
200
+ startIndex: resp?.content?.queries?.request?.[0]?.startIndex || 1,
201
+ items: resp?.content?.items.map(x => ({
202
+ title: x.title, link: options?.image ? null : x.link,
203
+ snippet: x.snippet, image: (
204
+ options?.image ? x.link : x.pagemap?.cse_image?.[0]?.src
205
+ ) || null,
206
+ })), provider,
207
+ };
208
+ case 'JINA':
209
+ var [key, min, def, max] =
210
+ [options?.apiKey || jinaApiKey, 1, 10, 20];
211
+ assert(key, 'API key is required.');
212
+ var [num, start] = [
213
+ ensureInt(options?.num || def, { min, max }),
214
+ ensureInt(options?.start || min, { min }),
215
+ ];
216
+ var url = `https://s.jina.ai/?q=${encodeURIComponent(query)}`
217
+ + `&page=${encodeURIComponent(start - 1)}`
218
+ + `&num=${encodeURIComponent(num)}`
219
+ var resp = await get(url, {
220
+ encode: options?.aiFriendly ? 'TEXT' : _JSON,
221
+ fetch: {
222
+ headers: {
223
+ 'Authorization': `Bearer ${key}`,
224
+ 'Accept': options?.aiFriendly ? MIME_TEXT : MIME_JSON,
225
+ 'X-Respond-With': 'no-content',
226
+ ...options?.aiFriendly ? {} : { 'X-With-Favicons': true },
227
+ }
228
+ }, ...options || {},
229
+ });
230
+ if (options?.raw) { return resp; }
231
+ if (options?.aiFriendly) { return resp?.content; }
232
+ return {
233
+ totalResults: 100, startIndex: start,
234
+ items: (resp?.content?.data || []).map(x => ({
235
+ title: x.title, link: x.url,
236
+ snippet: x.description, image: x.favicon,
237
+ })), provider,
238
+ };
239
+ default:
240
+ throwError(`Invalid search provider: "${options.provider}".`);
241
+ }
242
+ };
12
243
 
13
244
  const distillHtml = async (input, options) => {
14
245
  const html = await convert(input, {
@@ -105,14 +336,46 @@ const distill = async url => {
105
336
  };
106
337
  };
107
338
 
339
+ const _getRate = (rates, currency) => {
340
+ const rate = rates[ensureString(currency || 'USD', { case: 'UP' })];
341
+ assertSet(rate, `Unsupported currency: '${currency}'.`, 400);
342
+ return bignumber(rate);
343
+ };
344
+
345
+ const getExchangeRate = async (to, from, amount) => {
346
+ const data = {};
347
+ ((await get(
348
+ 'https://api.mixin.one/external/fiats', { encode: 'JSON' }
349
+ ))?.content?.data || []).map(x => data[x.code] = x.rate);
350
+ assert(Object.keys(data).length, 'Error fetching exchange rates.', 500);
351
+ if (!to) { return data; }
352
+ [to, from] = [_getRate(data, to), _getRate(data, from)];
353
+ const rate = divide(to, from);
354
+ amount = multiply(bignumber(amount ?? 1), rate);
355
+ return { rate, amount: amount.toString() };
356
+ };
357
+
358
+
359
+ export default get;
108
360
  export {
109
361
  _NEED,
110
362
  assertYoutubeUrl,
363
+ checkSearch,
364
+ checkVersion,
111
365
  distill,
112
366
  distillHtml,
113
367
  distillPage,
114
368
  distillYoutube,
369
+ get,
370
+ getCurrentIp,
371
+ getCurrentPosition,
372
+ getExchangeRate,
373
+ getJson,
374
+ getParsedHtml,
375
+ getVersionOnNpm,
115
376
  getYoutubeMetadata,
116
377
  getYoutubeTranscript,
117
- isYoutubeUrl
378
+ initSearch,
379
+ isYoutubeUrl,
380
+ search,
118
381
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "utilitas",
3
3
  "description": "Just another common utility for JavaScript.",
4
- "version": "1999.1.11",
4
+ "version": "1999.1.13",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/Leask/utilitas",
7
7
  "main": "index.mjs",
package/lib/shekel.mjs DELETED
@@ -1,24 +0,0 @@
1
- import { assertSet, ensureString } from './utilitas.mjs';
2
- import { bignumber, divide, multiply } from 'mathjs';
3
- import { get } from './shot.mjs';
4
-
5
- const _getRate = (rates, currency) => {
6
- const rate = rates[ensureString(currency || 'USD', { case: 'UP' })];
7
- assertSet(rate, `Unsupported currency: '${currency}'.`, 400);
8
- return bignumber(rate);
9
- };
10
-
11
- const getExchangeRate = async (to, from, amount) => {
12
- const data = {};
13
- ((await get(
14
- 'https://api.mixin.one/external/fiats', { encode: 'JSON' }
15
- ))?.content?.data || []).map(x => data[x.code] = x.rate);
16
- assert(Object.keys(data).length, 'Error fetching exchange rates.', 500);
17
- if (!to) { return data; }
18
- [to, from] = [_getRate(data, to), _getRate(data, from)];
19
- const rate = divide(to, from);
20
- amount = multiply(bignumber(amount ?? 1), rate);
21
- return { rate, amount: amount.toString() };
22
- };
23
-
24
- export { getExchangeRate };