@saplingai/sapling-js 1.0.52 → 1.0.53

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/README.md CHANGED
@@ -102,6 +102,79 @@ client
102
102
  })
103
103
  ```
104
104
 
105
+ ### AI content detection
106
+
107
+ ```javascript
108
+ const { data } = await client.aidetect(documentText);
109
+ // { score: 0.05, ai_fraction: 0.51, text: '...', version: '...',
110
+ // sentence_scores: [{ sentence: '...', score: 0.97, start: 0, end: 112 }, ...],
111
+ // tokens: [...], token_probs: [...] }
112
+ ```
113
+
114
+ `score` (0–1) rates the whole text, so a document that is only partly AI-written can score low;
115
+ `ai_fraction` is the share of the text in sentences scoring ≥ 0.5 (null when nothing could be
116
+ scored), which flags those documents. `sentence_scores` use the same model and 0.5 threshold, and
117
+ `start`/`end` index `text` in Unicode code points (`end` exclusive) — slice with
118
+ `Array.from(text).slice(start, end).join('')` if the text may contain emoji. A response may also
119
+ carry an advisory `language` (`{ code, name, confidence }`) when the text is confidently
120
+ identified; the detector is trained on English. Options: `sent_scores` (default true),
121
+ `score_string` (HTML heatmap), `version` (pin a model). Pass an array of 1–10 texts for the batch
122
+ form, which answers `{ results: [...] }` in input order.
123
+
124
+ ### Rephrase, sentiment and readability statistics
125
+
126
+ ```javascript
127
+ const { data: rephrased } = await client.rephrase('hey, can u send it over', {
128
+ mapping: 'informal_to_formal', // default 'paraphrase'
129
+ num_results: 3, // 1–8, default 5
130
+ });
131
+ // { results: [{ hash: '...', original: '...', replacement: 'Could you please send it over?',
132
+ // rephrase_type: 'informal_to_formal', model_version: '...' }, ...] }
133
+
134
+ const { data: sentiment } = await client.sentiment('Love the new release. The install was painful.');
135
+ // { overall: [[0.71, 'POSITIVE'], [0.29, 'NEGATIVE']],
136
+ // results: [[[0.99, 'POSITIVE']], [[0.95, 'NEGATIVE'], ...]],
137
+ // sents: ['Love the new release.', 'The install was painful.'] }
138
+
139
+ const { data: stats } = await client.statistics(documentText, { lang: 'en' });
140
+ // { chars: 1234, words: 210, sentences: 12, reading_time_sec: 47.55, reading_time_min: 1,
141
+ // readability: 8.2, readability_scores: { flesch_reading_ease: 64.1, gunning_fog: 10.3, ... } }
142
+ ```
143
+
144
+ `rephrase()` mappings: `paraphrase`, `reword`, `expand`, `shorten`, `summarize`,
145
+ `passive_to_active`, `active_to_passive`, `informal_to_formal`, `formal_to_informal`,
146
+ `sentence_split`, `generate_title`, `generate_acronym`, `generate_meta_description`,
147
+ `switch_tone` (with `tone_mapping`) and `switch_tense` (with `tense_mapping`). Sentiment labels
148
+ are `POSITIVE`, `NEUTRAL` and `NEGATIVE`, as `[probability, label]` pairs, most likely first;
149
+ a text with no sentences (markup or punctuation only) gets a 204 with an empty body (`data === ''`),
150
+ and one the API cannot split into sentences gets a 200 with `[]`.
151
+ `statistics()` is unbilled; `lang` is one of `en`, `de`, `es`, `fr`, `it`, `nl`, `pl`, `ru`, `ar`
152
+ or `auto` and picks the readability formulas (`readability_scores` is empty for `auto`).
153
+
154
+ ### Similar terms, profanity and the custom dictionary
155
+
156
+ ```javascript
157
+ const { data: similar } = await client.similarTerms('pet');
158
+ // { neighbors: [[0.81, 'dog'], [0.78, 'cat'], [0.75, 'puppy']],
159
+ // synonyms: ['darling', 'favorite', 'favourite', ...] }
160
+
161
+ const { data: profanity } = await client.profanity('This is so damn stupid.');
162
+ // { toks: ['This', 'is', 'so', 'damn', 'stupid.'], labels: [0, 0, 0, 1, 1] }
163
+
164
+ // Words the grammar and spelling checks should accept (product names, jargon).
165
+ const { data: entry } = await client.addDictionaryEntry('Sapling', { case_sensitive: true });
166
+ // { id: '...', entry: 'Sapling', case_sensitive: true, created_at: '...', updated_at: '...' }
167
+ const { data: entries } = await client.dictionaryEntries(); // a bare array, sorted by entry
168
+ await client.deleteDictionaryEntry(entry.id); // 204, empty body
169
+ ```
170
+
171
+ `similarTerms()` returns up to five `[similarity, term]` neighbors (similarity 0–1, most similar
172
+ first) plus WordNet `synonyms` (multi-word ones use underscores); only the first 100 characters of
173
+ the query are used. `profanity()` splits the text on whitespace and labels each token 1 (profane)
174
+ or 0. Both are unbilled. `addDictionaryEntry()` answers 400 for a duplicate entry or one with more
175
+ than two digits; `deleteDictionaryEntry()` answers 400 (not 404) for an id that is not this
176
+ account's.
177
+
105
178
  ### Style-guide rulesets
106
179
 
107
180
  Check text against style rules inline, or save a named ruleset once (owned by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saplingai/sapling-js",
3
- "version": "1.0.52",
3
+ "version": "1.0.53",
4
4
  "description": "JavaScript library for using the Sapling.ai API.",
5
5
  "type": "module",
6
6
  "main": "./sapling/client.js",
@@ -38,7 +38,54 @@ export type EditOptions = {
38
38
  }
39
39
 
40
40
  export type AIDetectOptions = {
41
+ /** Include `sentence_scores` (server default true). */
41
42
  sent_scores?: boolean,
43
+ /** Include `score_string`, an HTML heatmap of the scored text. */
44
+ score_string?: boolean,
45
+ /** Pin a detector model version (e.g. `'20251027'`); omitted = current default. */
46
+ version?: string,
47
+ }
48
+
49
+ export type AIDetectSentenceScore = {
50
+ sentence: string;
51
+ /** Same model, 0–1 scale and 0.5 threshold as the overall `score`. */
52
+ score: number;
53
+ /** Offsets into the response `text` (`end` exclusive), counted in Unicode
54
+ * code points — slice with `Array.from(text)` if it may contain emoji. */
55
+ start: number;
56
+ end: number;
57
+ }
58
+
59
+ export type AIDetectLanguage = {
60
+ code: string;
61
+ /** English display name; null for codes without one. */
62
+ name: string | null;
63
+ confidence: number;
64
+ }
65
+
66
+ export type AIDetectResponse = {
67
+ /** 0 (likely human-written) to 1 (likely AI-generated). */
68
+ score: number;
69
+ /** Share of the text in sentences scoring >= 0.5 (flags partially
70
+ * AI-written documents); null when nothing could be scored. */
71
+ ai_fraction: number | null;
72
+ text: string;
73
+ tokens: string[];
74
+ token_probs: number[];
75
+ /** The pinned `version` when one was requested, otherwise an opaque
76
+ * identifier of the default model — log it, don't parse it. */
77
+ version: string;
78
+ /** Present unless `sent_scores: false`. */
79
+ sentence_scores?: AIDetectSentenceScore[];
80
+ /** Present when `score_string: true`. */
81
+ score_string?: string;
82
+ /** Advisory detected language; omitted unless detection is high-confidence.
83
+ * The detector is trained on English. */
84
+ language?: AIDetectLanguage;
85
+ }
86
+
87
+ export type AIDetectBatchResponse = {
88
+ results: AIDetectResponse[];
42
89
  }
43
90
 
44
91
  export type StyleguideRule = string | { name: string; description?: string };
@@ -274,7 +321,12 @@ export type QualityBatchResponse = {
274
321
  results: QualityResponse[];
275
322
  }
276
323
 
277
- export type PIIType = 'email' | 'phone' | 'ssn' | 'credit_card' | 'ip_address' | 'iban' | 'us_bank_routing';
324
+ export type PIIDeterministicType = 'email' | 'phone' | 'ssn' | 'credit_card' | 'ip_address' | 'iban' | 'us_bank_routing';
325
+ // Opt-in, LLM-backed named-entity PII types: detected only when listed in
326
+ // `types` explicitly (a request without them stays deterministic-only), and
327
+ // the text is then capped at 10,000 characters rather than 20,000.
328
+ export type PIINerType = 'person' | 'organization' | 'location';
329
+ export type PIIType = PIIDeterministicType | PIINerType;
278
330
 
279
331
  export type PIIOptions = {
280
332
  types?: PIIType[],
@@ -422,6 +474,105 @@ export type SummarizeBatchResponse = {
422
474
  results: SummarizeResponse[];
423
475
  }
424
476
 
477
+ export type RephraseMapping =
478
+ 'paraphrase' | 'passive_to_active' | 'active_to_passive' | 'informal_to_formal' |
479
+ 'formal_to_informal' | 'sentence_split' | 'reword' | 'generate_title' | 'generate_acronym' |
480
+ 'generate_meta_description' | 'summarize' | 'expand' | 'shorten' | 'switch_tone' | 'switch_tense';
481
+
482
+ export type RephraseToneMapping =
483
+ 'straightforward' | 'confident' | 'friendly' | 'empathetic' | 'professional' | 'casual' |
484
+ 'diplomatic' | 'persuasive';
485
+
486
+ export type RephraseTenseMapping =
487
+ 'simple past' | 'past perfect' | 'past continuous' | 'past perfect continuous' |
488
+ 'simple present' | 'present perfect' | 'present continuous' | 'present perfect continuous' |
489
+ 'simple future' | 'future perfect' | 'future continuous' | 'future perfect continuous';
490
+
491
+ export type RephraseOptions = {
492
+ /** Defaults to 'paraphrase'. */
493
+ mapping?: RephraseMapping,
494
+ /** Only used with mapping 'switch_tone'. Defaults to 'straightforward'. */
495
+ tone_mapping?: RephraseToneMapping,
496
+ /** Only used with mapping 'switch_tense'. Defaults to 'simple past'. */
497
+ tense_mapping?: RephraseTenseMapping,
498
+ /** Number of alternatives, 1-8 (default 5). Honored by paraphrase, informal_to_formal,
499
+ * formal_to_informal, generate_title and generate_acronym. */
500
+ num_results?: number,
501
+ session_id?: string,
502
+ }
503
+
504
+ export type RephraseResult = {
505
+ hash: string,
506
+ original: string,
507
+ replacement: string,
508
+ rephrase_type: RephraseMapping,
509
+ model_version: string,
510
+ }
511
+
512
+ export type RephraseResponse = {
513
+ results: RephraseResult[],
514
+ }
515
+
516
+ export type SentimentLabel = 'POSITIVE' | 'NEUTRAL' | 'NEGATIVE';
517
+
518
+ /** [probability, label] pairs, most likely first. */
519
+ export type SentimentScores = Array<[number, SentimentLabel]>;
520
+
521
+ export type SentimentResponse = {
522
+ overall: SentimentScores,
523
+ /** One entry per sentence, aligned with `sents`. */
524
+ results: SentimentScores[],
525
+ sents: string[],
526
+ }
527
+
528
+ export type StatisticsLang = 'en' | 'de' | 'es' | 'fr' | 'it' | 'nl' | 'pl' | 'ru' | 'ar' | 'auto';
529
+
530
+ export type StatisticsOptions = {
531
+ /** Defaults to 'en'. */
532
+ lang?: StatisticsLang,
533
+ }
534
+
535
+ export type StatisticsResponse = {
536
+ chars: number,
537
+ words: number,
538
+ sentences: number,
539
+ reading_time_sec: number,
540
+ reading_time_min: number,
541
+ /** Flesch-Kincaid grade level; -3.4 when the text cannot be scored. */
542
+ readability: number,
543
+ /** Formula name -> score. The formulas present depend on `lang`; empty for 'auto'.
544
+ * `text_standard` is a string (e.g. "8th and 9th grade"). */
545
+ readability_scores: Record<string, number | string>,
546
+ }
547
+
548
+ export type SimilarTermsResponse = {
549
+ /** Up to 5 [similarity, term] pairs (similarity 0-1), most similar first. */
550
+ neighbors: Array<[number, string]>,
551
+ /** WordNet lemma names; multi-word ones use underscores (e.g. "domestic_dog"). */
552
+ synonyms: string[],
553
+ }
554
+
555
+ export type ProfanityResponse = {
556
+ /** The text split on whitespace (punctuation stays attached). */
557
+ toks: string[],
558
+ /** Aligned with `toks`: 1 for profanity, 0 otherwise. */
559
+ labels: Array<0 | 1>,
560
+ }
561
+
562
+ export type DictionaryEntry = {
563
+ id: string,
564
+ entry: string,
565
+ case_sensitive: boolean,
566
+ /** RFC 1123 timestamp, e.g. "Wed, 07 Oct 2020 21:15:47 GMT". */
567
+ created_at: string,
568
+ updated_at: string,
569
+ }
570
+
571
+ export type AddDictionaryEntryOptions = {
572
+ /** Defaults to true. */
573
+ case_sensitive?: boolean,
574
+ }
575
+
425
576
  export type UserDetails = {
426
577
  userId?: string,
427
578
  anonUser?: boolean,
@@ -439,6 +590,18 @@ export declare class Client {
439
590
  complete(query: string, sessionId?: string): Promise<AxiosResponse>;
440
591
  acceptComplete(completeUUID: string, query: string, completion: string, sessionId?: string): Promise<AxiosResponse>;
441
592
  tone(text: string): Promise<AxiosResponse>;
593
+ rephrase(text: string, options?: RephraseOptions): Promise<AxiosResponse<RephraseResponse>>;
594
+ /** A text with no sentences (e.g. markup or punctuation only) gets a 204 with an empty
595
+ * body, so `data` is `''` then; a text the API cannot split into sentences gets a 200
596
+ * with `[]`. Check `Array.isArray(data) || !data` before reading fields. */
597
+ sentiment(text: string): Promise<AxiosResponse<SentimentResponse | '' | never[]>>;
598
+ statistics(text: string, options?: StatisticsOptions): Promise<AxiosResponse<StatisticsResponse>>;
599
+ similarTerms(query: string): Promise<AxiosResponse<SimilarTermsResponse>>;
600
+ profanity(text: string): Promise<AxiosResponse<ProfanityResponse>>;
601
+ dictionaryEntries(): Promise<AxiosResponse<DictionaryEntry[]>>;
602
+ addDictionaryEntry(entry: string, options?: AddDictionaryEntryOptions): Promise<AxiosResponse<DictionaryEntry>>;
603
+ /** Answers 204 with an empty body. */
604
+ deleteDictionaryEntry(entryId: string): Promise<AxiosResponse<''>>;
442
605
  extract(text: string, fields: ExtractField[], options?: ExtractOptions): Promise<AxiosResponse<ExtractResponse>>;
443
606
  extract(texts: string[], fields: ExtractField[], options?: ExtractOptions): Promise<AxiosResponse<ExtractBatchResponse>>;
444
607
  extract(text: string | string[], fields: ExtractField[], options?: ExtractOptions): Promise<AxiosResponse<ExtractResponse | ExtractBatchResponse>>;
@@ -449,7 +612,9 @@ export declare class Client {
449
612
  summarize(texts: string[], options?: SummarizeOptions): Promise<AxiosResponse<SummarizeBatchResponse>>;
450
613
  summarize(text: string | string[], options?: SummarizeOptions): Promise<AxiosResponse<SummarizeResponse | SummarizeBatchResponse>>;
451
614
 
452
- aidetect(text: string | string[], options?: AIDetectOptions): Promise<AxiosResponse>;
615
+ aidetect(text: string, options?: AIDetectOptions): Promise<AxiosResponse<AIDetectResponse>>;
616
+ aidetect(texts: string[], options?: AIDetectOptions): Promise<AxiosResponse<AIDetectBatchResponse>>;
617
+ aidetect(text: string | string[], options?: AIDetectOptions): Promise<AxiosResponse<AIDetectResponse | AIDetectBatchResponse>>;
453
618
  styleguide(text: string, rules: StyleguideRule[] | string): Promise<AxiosResponse<StyleguideResponse>>;
454
619
  styleguide(texts: string[], rules: StyleguideRule[] | string): Promise<AxiosResponse<StyleguideBatchResponse>>;
455
620
  styleguide(text: string | string[], rules: StyleguideRule[] | string): Promise<AxiosResponse<StyleguideResponse | StyleguideBatchResponse>>;
package/sapling/client.js CHANGED
@@ -8,6 +8,24 @@ const TRANSLATE_FORMALITIES = ['formal', 'informal'];
8
8
  const SUMMARIZE_LENGTHS = ['short', 'medium', 'long'];
9
9
  // Reading levels simplify() accepts; the API rejects anything else with a 400.
10
10
  const SIMPLIFY_READING_LEVELS = ['plain', 'elementary', 'middle_school', 'high_school'];
11
+ // rephrase() enums, mirroring the API's validators (utils/request_validator.py),
12
+ // which reject anything else with a 400.
13
+ const REPHRASE_MAPPINGS = [
14
+ 'paraphrase', 'passive_to_active', 'active_to_passive', 'informal_to_formal',
15
+ 'formal_to_informal', 'sentence_split', 'reword', 'generate_title', 'generate_acronym',
16
+ 'generate_meta_description', 'summarize', 'expand', 'shorten', 'switch_tone', 'switch_tense',
17
+ ];
18
+ const REPHRASE_TONE_MAPPINGS = [
19
+ 'straightforward', 'confident', 'friendly', 'empathetic', 'professional', 'casual',
20
+ 'diplomatic', 'persuasive',
21
+ ];
22
+ const REPHRASE_TENSE_MAPPINGS = [
23
+ 'simple past', 'past perfect', 'past continuous', 'past perfect continuous',
24
+ 'simple present', 'present perfect', 'present continuous', 'present perfect continuous',
25
+ 'simple future', 'future perfect', 'future continuous', 'future perfect continuous',
26
+ ];
27
+ // Languages statistics() scores readability for (plus 'auto').
28
+ const STATISTICS_LANGS = ['en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'ru', 'ar'];
11
29
 
12
30
  // Shared by styleguide() and saveStyleguideRuleset(). Shape/type checks only —
13
31
  // size limits (rule/description length) are server configuration and enforced
@@ -237,6 +255,286 @@ export class Client {
237
255
  return this.axiosPromise(url, 'post', data);
238
256
  }
239
257
 
258
+ /**
259
+ * Rephrase / paraphrase: generate alternative phrasings of a text.
260
+ *
261
+ * Returns `results`: one `{hash, original, replacement, rephrase_type,
262
+ * model_version}` entry per alternative. HTML is stripped server-side.
263
+ *
264
+ * @param {string} text - The text to rephrase (the API caps its length).
265
+ * @param {Object} [options]
266
+ * @param {string} [options.mapping] - Transformation to apply (see
267
+ * REPHRASE_MAPPINGS). Defaults to 'paraphrase'.
268
+ * @param {string} [options.tone_mapping] - Target tone; only used with
269
+ * mapping 'switch_tone'. Defaults to 'straightforward'.
270
+ * @param {string} [options.tense_mapping] - Target tense; only used with
271
+ * mapping 'switch_tense'. Defaults to 'simple past'.
272
+ * @param {number} [options.num_results] - Number of alternatives, 1–8
273
+ * (default 5). Honored by the paraphrase, informal_to_formal,
274
+ * formal_to_informal, generate_title and generate_acronym mappings; other
275
+ * mappings return their default number of results.
276
+ * @param {string} [options.session_id] - Optional session identifier used to
277
+ * group feedback events.
278
+ * @returns {Promise<AxiosResponse>}
279
+ */
280
+ rephrase(text, options = {}) {
281
+ const url = `${this.urlEndpoint}rephrase`;
282
+
283
+ // Shape/type/enum checks only; the length cap is server configuration and
284
+ // enforced there with a clear 400 (see seo()). The enums and the 1–8
285
+ // num_results bound mirror the API's validators, which reject anything else.
286
+ if (typeof text !== 'string' || text.trim().length === 0) {
287
+ throw new TypeError('rephrase: text must be a non-empty string.');
288
+ }
289
+
290
+ let data = {
291
+ text,
292
+ key: this.apiKey,
293
+ }
294
+
295
+ // `!== undefined` (not `!= null`) so an explicit `null` is rejected as a bad
296
+ // type rather than silently treated as "omitted", matching classify()/seo().
297
+ if (options?.mapping !== undefined) {
298
+ if (!REPHRASE_MAPPINGS.includes(options.mapping)) {
299
+ throw new TypeError(`rephrase: mapping must be one of ${REPHRASE_MAPPINGS.join(', ')}.`);
300
+ }
301
+ data.mapping = options.mapping;
302
+ }
303
+ if (options?.tone_mapping !== undefined) {
304
+ if (!REPHRASE_TONE_MAPPINGS.includes(options.tone_mapping)) {
305
+ throw new TypeError(
306
+ `rephrase: tone_mapping must be one of ${REPHRASE_TONE_MAPPINGS.join(', ')}.`);
307
+ }
308
+ data.tone_mapping = options.tone_mapping;
309
+ }
310
+ if (options?.tense_mapping !== undefined) {
311
+ if (!REPHRASE_TENSE_MAPPINGS.includes(options.tense_mapping)) {
312
+ throw new TypeError(
313
+ `rephrase: tense_mapping must be one of ${REPHRASE_TENSE_MAPPINGS.join(', ')}.`);
314
+ }
315
+ data.tense_mapping = options.tense_mapping;
316
+ }
317
+ if (options?.num_results !== undefined) {
318
+ if (!Number.isInteger(options.num_results)
319
+ || options.num_results < 1 || options.num_results > 8) {
320
+ throw new TypeError('rephrase: num_results must be an integer from 1 to 8.');
321
+ }
322
+ data.num_results = options.num_results;
323
+ }
324
+ if (options?.session_id !== undefined) {
325
+ if (typeof options.session_id !== 'string') {
326
+ throw new TypeError('rephrase: session_id must be a string.');
327
+ }
328
+ data.session_id = options.session_id;
329
+ }
330
+
331
+ return this.axiosPromise(url, 'post', data);
332
+ }
333
+
334
+ /**
335
+ * Sentiment analysis: positive / neutral / negative, overall and per sentence.
336
+ *
337
+ * Returns `sents` (the sentences found), `overall` (a list of
338
+ * `[probability, label]` pairs, most likely first) and `results` (one such
339
+ * list per sentence, aligned with `sents`). Labels are 'POSITIVE',
340
+ * 'NEUTRAL' or 'NEGATIVE'. A text with no sentences gets a 204 with no body
341
+ * (axios gives `data === ''`), and a text the API cannot split into
342
+ * sentences gets a 200 with `[]`.
343
+ *
344
+ * @param {string} text - The text to analyze, plain text or HTML (tags are
345
+ * stripped server-side; the API caps its length).
346
+ * @returns {Promise<AxiosResponse>}
347
+ */
348
+ sentiment(text) {
349
+ const url = `${this.urlEndpoint}sentiment`;
350
+
351
+ if (typeof text !== 'string' || text.trim().length === 0) {
352
+ throw new TypeError('sentiment: text must be a non-empty string.');
353
+ }
354
+
355
+ let data = {
356
+ text,
357
+ key: this.apiKey,
358
+ }
359
+ return this.axiosPromise(url, 'post', data);
360
+ }
361
+
362
+ /**
363
+ * Readability statistics: character / word / sentence counts, reading time
364
+ * and readability grades.
365
+ *
366
+ * Returns `chars`, `words`, `sentences`, `reading_time_sec`,
367
+ * `reading_time_min`, `readability` (a Flesch-Kincaid grade level, -3.4 when
368
+ * it cannot be scored) and `readability_scores` (a map of formula name to
369
+ * score; which formulas appear depends on `lang`, and it is empty for
370
+ * 'auto'). Unbilled.
371
+ *
372
+ * @param {string} text - The text to measure, plain text or HTML (tags are
373
+ * stripped server-side; the API caps it at 100,000 characters).
374
+ * @param {Object} [options]
375
+ * @param {string} [options.lang] - Language of the text: one of
376
+ * STATISTICS_LANGS ('en', 'de', 'es', 'fr', 'it', 'nl', 'pl', 'ru', 'ar')
377
+ * or 'auto'. Defaults to 'en'.
378
+ * @returns {Promise<AxiosResponse>}
379
+ */
380
+ statistics(text, options = {}) {
381
+ const url = `${this.urlEndpoint}statistics`;
382
+
383
+ // An empty text is a valid request (all-zero counts), so only the type is
384
+ // checked here.
385
+ if (typeof text !== 'string') {
386
+ throw new TypeError('statistics: text must be a string.');
387
+ }
388
+
389
+ let data = {
390
+ text,
391
+ key: this.apiKey,
392
+ }
393
+
394
+ if (options?.lang !== undefined) {
395
+ if (options.lang !== 'auto' && !STATISTICS_LANGS.includes(options.lang)) {
396
+ throw new TypeError(
397
+ `statistics: lang must be 'auto' or one of ${STATISTICS_LANGS.join(', ')}.`);
398
+ }
399
+ data.lang = options.lang;
400
+ }
401
+
402
+ return this.axiosPromise(url, 'post', data);
403
+ }
404
+
405
+ /**
406
+ * Similar terms: thesaurus synonyms plus semantically similar terms for a
407
+ * word or short phrase.
408
+ *
409
+ * Returns `neighbors` (up to 5 `[similarity, term]` pairs from embedding
410
+ * similarity, similarity 0–1, most similar first; only alphabetic terms)
411
+ * and `synonyms` (WordNet lemma names — multi-word ones use underscores,
412
+ * e.g. 'domestic_dog'). Unbilled.
413
+ *
414
+ * @param {string} query - The word or phrase to look up (the API uses only
415
+ * its first 100 characters).
416
+ * @returns {Promise<AxiosResponse>}
417
+ */
418
+ similarTerms(query) {
419
+ const url = `${this.urlEndpoint}thesaurus`;
420
+
421
+ // The API answers a blank query with a 400.
422
+ if (typeof query !== 'string' || query.trim().length === 0) {
423
+ throw new TypeError('similarTerms: query must be a non-empty string.');
424
+ }
425
+
426
+ let data = {
427
+ query,
428
+ key: this.apiKey,
429
+ }
430
+ return this.axiosPromise(url, 'post', data);
431
+ }
432
+
433
+ /**
434
+ * Profanity filter: label each whitespace-separated token of a text.
435
+ *
436
+ * Returns `toks` (the text split on whitespace; punctuation stays attached,
437
+ * e.g. 'stupid.') and `labels` (aligned with `toks`: 1 for profanity, 0
438
+ * otherwise). The text is not HTML-stripped. Unbilled.
439
+ *
440
+ * @param {string} text - The text to check.
441
+ * @returns {Promise<AxiosResponse>}
442
+ */
443
+ profanity(text) {
444
+ const url = `${this.urlEndpoint}profanity`;
445
+
446
+ if (typeof text !== 'string' || text.trim().length === 0) {
447
+ throw new TypeError('profanity: text must be a non-empty string.');
448
+ }
449
+
450
+ let data = {
451
+ text,
452
+ key: this.apiKey,
453
+ }
454
+ return this.axiosPromise(url, 'post', data);
455
+ }
456
+
457
+ /**
458
+ * List the custom dictionary of this API key's account: words the grammar
459
+ * and spelling checks accept as correct.
460
+ *
461
+ * Returns a bare array of `{id, entry, case_sensitive, created_at,
462
+ * updated_at}` entries, sorted by entry (timestamps are RFC 1123 strings).
463
+ *
464
+ * @returns {Promise<AxiosResponse>}
465
+ */
466
+ dictionaryEntries() {
467
+ const url = `${this.urlEndpoint}dictionary`;
468
+ // Not axiosPromise: this is a GET and the key rides the query string,
469
+ // like styleguideRulesets().
470
+ return axios({
471
+ url,
472
+ method: 'get',
473
+ params: { key: this.apiKey },
474
+ timeout: this.timeout,
475
+ });
476
+ }
477
+
478
+ /**
479
+ * Add a word to this API key's account dictionary so the grammar and
480
+ * spelling checks stop flagging it (product names, jargon...).
481
+ *
482
+ * Returns the created `{id, entry, case_sensitive, created_at, updated_at}`
483
+ * entry. The API answers 400 for a duplicate entry ("Entry already
484
+ * exists.") and for an entry with more than two digits.
485
+ *
486
+ * @param {string} entry - The word to add (surrounding whitespace is
487
+ * stripped server-side).
488
+ * @param {Object} [options]
489
+ * @param {boolean} [options.case_sensitive] - Whether matching is case
490
+ * sensitive. Defaults to true.
491
+ * @returns {Promise<AxiosResponse>}
492
+ */
493
+ addDictionaryEntry(entry, options = {}) {
494
+ const url = `${this.urlEndpoint}dictionary`;
495
+
496
+ if (typeof entry !== 'string' || entry.trim().length === 0) {
497
+ throw new TypeError('addDictionaryEntry: entry must be a non-empty string.');
498
+ }
499
+
500
+ let data = {
501
+ entry,
502
+ key: this.apiKey,
503
+ }
504
+
505
+ if (options?.case_sensitive !== undefined) {
506
+ if (typeof options.case_sensitive !== 'boolean') {
507
+ throw new TypeError('addDictionaryEntry: case_sensitive must be a boolean.');
508
+ }
509
+ data.case_sensitive = options.case_sensitive;
510
+ }
511
+
512
+ return this.axiosPromise(url, 'post', data);
513
+ }
514
+
515
+ /**
516
+ * Delete a custom dictionary entry by the `id` dictionaryEntries() or
517
+ * addDictionaryEntry() returned. Answers 204 with no body; an id that is
518
+ * malformed or not this account's gets a 400 (not a 404).
519
+ *
520
+ * @param {string} entryId - The entry's id (a UUID).
521
+ * @returns {Promise<AxiosResponse>}
522
+ */
523
+ deleteDictionaryEntry(entryId) {
524
+ if (typeof entryId !== 'string' || entryId.trim().length === 0) {
525
+ throw new TypeError('deleteDictionaryEntry: entryId must be a non-empty string.');
526
+ }
527
+ const url = `${this.urlEndpoint}dictionary/${encodeURIComponent(entryId)}`;
528
+ // The key rides the query string, like deleteStyleguideRuleset(): some
529
+ // HTTP stacks drop DELETE bodies.
530
+ return axios({
531
+ url,
532
+ method: 'delete',
533
+ params: { key: this.apiKey },
534
+ timeout: this.timeout,
535
+ });
536
+ }
537
+
240
538
  /**
241
539
  * Structured data extraction: pull caller-named fields out of unstructured text.
242
540
  *
@@ -866,6 +1164,18 @@ export class Client {
866
1164
  if (options.sent_scores != null) {
867
1165
  data.sent_scores = options.sent_scores;
868
1166
  }
1167
+ if (options.score_string !== undefined) {
1168
+ if (typeof options.score_string !== 'boolean') {
1169
+ throw new TypeError('aidetect: score_string must be a boolean.');
1170
+ }
1171
+ data.score_string = options.score_string;
1172
+ }
1173
+ if (options.version !== undefined) {
1174
+ if (typeof options.version !== 'string') {
1175
+ throw new TypeError('aidetect: version must be a string.');
1176
+ }
1177
+ data.version = options.version;
1178
+ }
869
1179
 
870
1180
  return this.axiosPromise(url, 'post', data);
871
1181
  }