@owlmeans/bot 0.1.18-rc.2 → 0.1.18

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.
Files changed (63) hide show
  1. package/README.md +23 -21
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/bot/SKILL.md +68 -46
  4. package/build/consts.d.ts +6 -31
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +10 -36
  7. package/build/consts.js.map +1 -1
  8. package/build/consts.local.d.ts +3 -1
  9. package/build/consts.local.d.ts.map +1 -1
  10. package/build/consts.local.js +3 -1
  11. package/build/consts.local.js.map +1 -1
  12. package/build/helpers/index.d.ts +2 -2
  13. package/build/helpers/index.js +1 -1
  14. package/build/helpers/text/types.d.ts +35 -0
  15. package/build/helpers/text/types.d.ts.map +1 -0
  16. package/build/helpers/{rule → text}/types.js.map +1 -1
  17. package/build/helpers/text.d.ts +4 -0
  18. package/build/helpers/text.d.ts.map +1 -0
  19. package/build/helpers/text.js +77 -0
  20. package/build/helpers/text.js.map +1 -0
  21. package/build/protocols/types.d.ts +2 -17
  22. package/build/protocols/types.d.ts.map +1 -1
  23. package/build/protocols.d.ts +1 -1
  24. package/build/protocols.d.ts.map +1 -1
  25. package/build/protocols.js +2 -7
  26. package/build/protocols.js.map +1 -1
  27. package/build/schemas.d.ts +2 -4
  28. package/build/schemas.d.ts.map +1 -1
  29. package/build/schemas.js +3 -31
  30. package/build/schemas.js.map +1 -1
  31. package/build/types.d.ts +7 -41
  32. package/build/types.d.ts.map +1 -1
  33. package/build/utils/aliases/types.d.ts +0 -5
  34. package/build/utils/aliases/types.d.ts.map +1 -1
  35. package/build/utils/aliases.d.ts.map +1 -1
  36. package/build/utils/aliases.js +0 -5
  37. package/build/utils/aliases.js.map +1 -1
  38. package/package.json +7 -7
  39. package/src/consts.local.ts +4 -1
  40. package/src/consts.ts +6 -37
  41. package/src/helpers/index.ts +2 -2
  42. package/src/helpers/text/types.ts +35 -0
  43. package/src/helpers/text.ts +91 -0
  44. package/src/protocols/types.ts +2 -12
  45. package/src/protocols.ts +4 -21
  46. package/src/schemas.ts +5 -39
  47. package/src/types.ts +7 -45
  48. package/src/utils/aliases/types.ts +0 -1
  49. package/src/utils/aliases.ts +0 -5
  50. package/tests/protocols.spec.ts +3 -7
  51. package/tests/schemas.spec.ts +2 -30
  52. package/tests/text.spec.ts +101 -0
  53. package/tests/update.spec.ts +4 -10
  54. package/build/helpers/rule/types.d.ts +0 -26
  55. package/build/helpers/rule/types.d.ts.map +0 -1
  56. package/build/helpers/rule.d.ts +0 -4
  57. package/build/helpers/rule.d.ts.map +0 -1
  58. package/build/helpers/rule.js +0 -71
  59. package/build/helpers/rule.js.map +0 -1
  60. package/src/helpers/rule/types.ts +0 -26
  61. package/src/helpers/rule.ts +0 -89
  62. package/tests/rule.spec.ts +0 -129
  63. /package/build/helpers/{rule → text}/types.js +0 -0
@@ -1,71 +0,0 @@
1
- import { BOT_KEYWORD_MAX, BOT_KEYWORDS_MAX, BotMatchMode } from '../consts.js';
2
- import { BOT_SPACE_PATTERN, BOT_TOKEN_PATTERN } from '../consts.local.js';
3
- export const createBotRuleHelper = () => {
4
- /**
5
- * NFKC, then a case fold: upper-casing first maps what lower-casing alone keeps apart (`ß` → `SS`
6
- * → `ss`, final `ς` → `Σ` → `σ`), and NFKC again recomposes what the case mapping decomposed.
7
- */
8
- const fold = (text) => text.normalize('NFKC').toUpperCase().toLowerCase().normalize('NFKC');
9
- const tokensOf = (text) => fold(text).match(BOT_TOKEN_PATTERN) ?? [];
10
- const spacedOf = (text) => fold(text).replace(BOT_SPACE_PATTERN, ' ').trim();
11
- const hasPhrase = (tokens, phrase) => {
12
- for (let at = 0; at + phrase.length <= tokens.length; at++) {
13
- if (phrase.every((token, offset) => tokens[at + offset] === token)) {
14
- return true;
15
- }
16
- }
17
- return false;
18
- };
19
- const match = (text, rules) => {
20
- if (typeof text !== 'string' || text.trim() === '') {
21
- return [];
22
- }
23
- // Folded once per message, never per rule.
24
- let tokens;
25
- let spaced;
26
- const matchesKeyword = (mode, keyword) => {
27
- if (mode === BotMatchMode.Contains) {
28
- const needle = spacedOf(keyword);
29
- spaced ??= spacedOf(text);
30
- return needle !== '' && spaced.includes(needle);
31
- }
32
- const phrase = tokensOf(keyword);
33
- tokens ??= tokensOf(text);
34
- return phrase.length > 0 && hasPhrase(tokens, phrase);
35
- };
36
- return rules
37
- .filter(rule => rule.enabled)
38
- .map((rule) => ({
39
- rule,
40
- keywords: [...new Set(rule.keywords.filter(keyword => typeof keyword === 'string' && matchesKeyword(rule.mode ?? BotMatchMode.Word, keyword)))],
41
- }))
42
- .filter(found => found.keywords.length > 0)
43
- // Array#sort is stable: rules of equal order keep the order they were given in.
44
- .sort((left, right) => (left.rule.order ?? 0) - (right.rule.order ?? 0));
45
- };
46
- const normalizeKeywords = (list) => {
47
- const seen = new Set();
48
- const result = [];
49
- for (const raw of list ?? []) {
50
- if (typeof raw !== 'string') {
51
- continue;
52
- }
53
- const keyword = Array.from(raw.replace(BOT_SPACE_PATTERN, ' ').trim())
54
- .slice(0, BOT_KEYWORD_MAX).join('').trim();
55
- const key = spacedOf(keyword);
56
- if (keyword === '' || seen.has(key)) {
57
- continue;
58
- }
59
- seen.add(key);
60
- result.push(keyword);
61
- if (result.length >= BOT_KEYWORDS_MAX) {
62
- break;
63
- }
64
- }
65
- return result;
66
- };
67
- const appliesTo = (rule, connection) => rule.connection == null || rule.connection === connection;
68
- return { match, normalizeKeywords, appliesTo };
69
- };
70
- export const botRuleHelper = createBotRuleHelper();
71
- //# sourceMappingURL=rule.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rule.js","sourceRoot":"","sources":["../../src/helpers/rule.ts"],"names":[],"mappings":"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"}
@@ -1,26 +0,0 @@
1
- import type { BotMatch, BotRule } from '../../types.js'
2
-
3
- /**
4
- * How a message is matched against keyword groups — the ONE implementation the server's dispatch,
5
- * the panels and the tests share, so a rule means the same thing everywhere.
6
- */
7
- export interface BotRuleHelper {
8
- /**
9
- * The enabled rules whose keywords the text matches, ordered by `order` (ties keep the given
10
- * order), each with the keywords (as stored) that matched.
11
- *
12
- * Both sides are folded the same way: NFKC, then case-folded (`ß` ≡ `ss`, `Ё` ≡ `ё`), accents
13
- * kept. `word` compares whole tokens — runs of letters, combining marks, digits and `_` — and a
14
- * multi-word keyword is a PHRASE that must appear as consecutive tokens; `contains` is a substring
15
- * of the folded text with whitespace runs collapsed. Disabled rules never match.
16
- */
17
- match: (text: string, rules: readonly BotRule[]) => BotMatch[]
18
- /**
19
- * A keyword list as it is stored: each trimmed with inner whitespace collapsed, cut to
20
- * `BOT_KEYWORD_MAX` code points, empties dropped, duplicates (by their folded form) dropped keeping
21
- * the first spelling, at most `BOT_KEYWORDS_MAX` kept.
22
- */
23
- normalizeKeywords: (list: readonly string[]) => string[]
24
- /** Whether a rule applies to a connection: its own, or a scope-wide one (`connection` null/absent). */
25
- appliesTo: (rule: Pick<BotRule, 'connection'>, connection: string) => boolean
26
- }
@@ -1,89 +0,0 @@
1
- import { BOT_KEYWORD_MAX, BOT_KEYWORDS_MAX, BotMatchMode } from '../consts.js'
2
- import { BOT_SPACE_PATTERN, BOT_TOKEN_PATTERN } from '../consts.local.js'
3
- import type { BotMatch, BotRule } from '../types.js'
4
- import type { BotRuleHelper } from './rule/types.js'
5
-
6
- export const createBotRuleHelper = (): BotRuleHelper => {
7
- /**
8
- * NFKC, then a case fold: upper-casing first maps what lower-casing alone keeps apart (`ß` → `SS`
9
- * → `ss`, final `ς` → `Σ` → `σ`), and NFKC again recomposes what the case mapping decomposed.
10
- */
11
- const fold = (text: string): string =>
12
- text.normalize('NFKC').toUpperCase().toLowerCase().normalize('NFKC')
13
-
14
- const tokensOf = (text: string): string[] => fold(text).match(BOT_TOKEN_PATTERN) ?? []
15
-
16
- const spacedOf = (text: string): string => fold(text).replace(BOT_SPACE_PATTERN, ' ').trim()
17
-
18
- const hasPhrase = (tokens: readonly string[], phrase: readonly string[]): boolean => {
19
- for (let at = 0; at + phrase.length <= tokens.length; at++) {
20
- if (phrase.every((token, offset) => tokens[at + offset] === token)) {
21
- return true
22
- }
23
- }
24
- return false
25
- }
26
-
27
- const match = (text: string, rules: readonly BotRule[]): BotMatch[] => {
28
- if (typeof text !== 'string' || text.trim() === '') {
29
- return []
30
- }
31
- // Folded once per message, never per rule.
32
- let tokens: string[] | undefined
33
- let spaced: string | undefined
34
-
35
- const matchesKeyword = (mode: BotMatchMode, keyword: string): boolean => {
36
- if (mode === BotMatchMode.Contains) {
37
- const needle = spacedOf(keyword)
38
- spaced ??= spacedOf(text)
39
- return needle !== '' && spaced.includes(needle)
40
- }
41
- const phrase = tokensOf(keyword)
42
- tokens ??= tokensOf(text)
43
- return phrase.length > 0 && hasPhrase(tokens, phrase)
44
- }
45
-
46
- return rules
47
- .filter(rule => rule.enabled)
48
- .map((rule): BotMatch => ({
49
- rule,
50
- keywords: [...new Set(rule.keywords.filter(
51
- keyword => typeof keyword === 'string' && matchesKeyword(rule.mode ?? BotMatchMode.Word, keyword)
52
- ))],
53
- }))
54
- .filter(found => found.keywords.length > 0)
55
- // Array#sort is stable: rules of equal order keep the order they were given in.
56
- .sort((left, right) => (left.rule.order ?? 0) - (right.rule.order ?? 0))
57
- }
58
-
59
- const normalizeKeywords = (list: readonly string[]): string[] => {
60
- const seen = new Set<string>()
61
- const result: string[] = []
62
-
63
- for (const raw of list ?? []) {
64
- if (typeof raw !== 'string') {
65
- continue
66
- }
67
- const keyword = Array.from(raw.replace(BOT_SPACE_PATTERN, ' ').trim())
68
- .slice(0, BOT_KEYWORD_MAX).join('').trim()
69
- const key = spacedOf(keyword)
70
- if (keyword === '' || seen.has(key)) {
71
- continue
72
- }
73
- seen.add(key)
74
- result.push(keyword)
75
- if (result.length >= BOT_KEYWORDS_MAX) {
76
- break
77
- }
78
- }
79
-
80
- return result
81
- }
82
-
83
- const appliesTo = (rule: Pick<BotRule, 'connection'>, connection: string): boolean =>
84
- rule.connection == null || rule.connection === connection
85
-
86
- return { match, normalizeKeywords, appliesTo }
87
- }
88
-
89
- export const botRuleHelper = createBotRuleHelper()
@@ -1,129 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { BOT_KEYWORD_MAX, BOT_KEYWORDS_MAX, BotMatchMode, BotRuleAction, botRuleHelper, type BotRule } from '../src/index.js'
3
-
4
- const at = '2026-10-26T10:00:00.000Z'
5
-
6
- const rule = (id: string, keywords: string[], extra: Partial<BotRule> = {}): BotRule => ({
7
- id,
8
- scope: 'org-1',
9
- connection: null,
10
- name: id,
11
- keywords,
12
- mode: BotMatchMode.Word,
13
- action: BotRuleAction.Delete,
14
- enabled: true,
15
- order: 0,
16
- createdAt: at,
17
- updatedAt: at,
18
- ...extra,
19
- })
20
-
21
- const ids = (text: string, rules: BotRule[]): string[] => botRuleHelper.match(text, rules).map(found => found.rule.id)
22
-
23
- describe('botRuleHelper.match — word mode', () => {
24
- test('matches whole words only, case-insensitively and across punctuation', () => {
25
- const spam = rule('spam', ['casino'])
26
-
27
- expect(ids('Visit the CASINO!', [spam])).toEqual(['spam'])
28
- expect(ids('casino, now', [spam])).toEqual(['spam'])
29
- expect(ids('casinos are fun', [spam])).toEqual([])
30
- expect(ids('megacasino', [spam])).toEqual([])
31
- })
32
-
33
- test('a multi-word keyword is a phrase of consecutive words', () => {
34
- const offer = rule('offer', ['free money'])
35
-
36
- expect(ids('Get FREE money now', [offer])).toEqual(['offer'])
37
- expect(ids('free—money', [offer])).toEqual(['offer'])
38
- expect(ids('money for free', [offer])).toEqual([])
39
- expect(ids('free real money', [offer])).toEqual([])
40
- })
41
-
42
- test('is Unicode-aware: Cyrillic, accents, compatibility forms and the German sharp s', () => {
43
- expect(ids('Купите СПАМ сейчас', [rule('ru', ['спам'])])).toEqual(['ru'])
44
- expect(ids('ёлка', [rule('yo', ['ЁЛКА'])])).toEqual(['yo'])
45
- expect(ids('Un café, por favor', [rule('es', ['CAFÉ'])])).toEqual(['es'])
46
- // Accents are kept: "cafe" is not "café".
47
- expect(ids('Un café', [rule('plain', ['cafe'])])).toEqual([])
48
- // NFKC: the full-width form and the ligature fold onto plain letters; NFD text onto NFC.
49
- expect(ids('CASINO night', [rule('wide', ['casino'])])).toEqual(['wide'])
50
- expect(ids('finance tips', [rule('lig', ['finance'])])).toEqual(['lig'])
51
- expect(ids('café au lait', [rule('nfd', ['café'])])).toEqual(['nfd'])
52
- expect(ids('STRASSE gesperrt', [rule('de', ['straße'])])).toEqual(['de'])
53
- })
54
-
55
- test('a keyword with no letters or digits never matches a word', () => {
56
- expect(ids('!!! now', [rule('bang', ['!!!'])])).toEqual([])
57
- })
58
- })
59
-
60
- describe('botRuleHelper.match — contains mode', () => {
61
- test('matches a folded substring, inside words too, with whitespace runs collapsed', () => {
62
- const inside = rule('inside', ['casino'], { mode: BotMatchMode.Contains })
63
- const punct = rule('punct', ['!!!'], { mode: BotMatchMode.Contains })
64
- const spaced = rule('spaced', ['free money'], { mode: BotMatchMode.Contains })
65
-
66
- expect(ids('MEGACASINO.com', [inside])).toEqual(['inside'])
67
- expect(ids('wow!!! ok', [punct])).toEqual(['punct'])
68
- expect(ids('get free\n money', [spaced])).toEqual(['spaced'])
69
- expect(ids('no match here', [inside, punct, spaced])).toEqual([])
70
- })
71
- })
72
-
73
- describe('botRuleHelper.match — groups', () => {
74
- test('skips disabled rules and answers [] for empty text', () => {
75
- const off = rule('off', ['spam'], { enabled: false })
76
-
77
- expect(ids('spam', [off])).toEqual([])
78
- expect(ids(' ', [rule('on', ['spam'])])).toEqual([])
79
- })
80
-
81
- test('orders matches by rule order, ties keeping the given order', () => {
82
- const rules = [
83
- rule('third', ['spam'], { order: 3 }),
84
- rule('first', ['spam'], { order: 1 }),
85
- rule('tie-a', ['spam'], { order: 2 }),
86
- rule('tie-b', ['spam'], { order: 2 }),
87
- ]
88
-
89
- expect(ids('spam', rules)).toEqual(['first', 'tie-a', 'tie-b', 'third'])
90
- })
91
-
92
- test('reports every group a message matches, each with its own matched keywords as stored', () => {
93
- const abuse = rule('abuse', ['Idiot', 'stupid', 'moron'], { order: 1, action: BotRuleAction.Warn })
94
- const hours = rule('hours', ['opening hours', 'open'], { order: 2, action: BotRuleAction.Reply })
95
- const quiet = rule('quiet', ['delivery'], { order: 0 })
96
- const found = botRuleHelper.match('You idiot, what are the OPENING hours? stupid!', [hours, quiet, abuse])
97
-
98
- expect(found.map(match => [match.rule.id, match.keywords])).toEqual([
99
- ['abuse', ['Idiot', 'stupid']],
100
- ['hours', ['opening hours']],
101
- ])
102
- })
103
- })
104
-
105
- describe('botRuleHelper.normalizeKeywords', () => {
106
- test('trims, collapses inner whitespace, drops empties and folded duplicates keeping the first spelling', () => {
107
- expect(botRuleHelper.normalizeKeywords([' Free money ', '', ' ', 'free MONEY', 'Спам', 'СПАМ', 'café']))
108
- .toEqual(['Free money', 'Спам', 'café'])
109
- })
110
-
111
- test('caps the keyword length (in code points) and the list size', () => {
112
- const long = '😀'.repeat(BOT_KEYWORD_MAX + 10)
113
- const [capped] = botRuleHelper.normalizeKeywords([long])
114
- const many = botRuleHelper.normalizeKeywords(Array.from({ length: BOT_KEYWORDS_MAX + 5 }, (_, n) => `kw${n}`))
115
-
116
- expect(Array.from(capped)).toHaveLength(BOT_KEYWORD_MAX)
117
- expect(many).toHaveLength(BOT_KEYWORDS_MAX)
118
- expect(many[0]).toBe('kw0')
119
- })
120
- })
121
-
122
- describe('botRuleHelper.appliesTo', () => {
123
- test('a scope-wide rule applies to every connection, an own one to its connection only', () => {
124
- expect(botRuleHelper.appliesTo({ connection: null }, 'c1')).toBe(true)
125
- expect(botRuleHelper.appliesTo({}, 'c1')).toBe(true)
126
- expect(botRuleHelper.appliesTo({ connection: 'c1' }, 'c1')).toBe(true)
127
- expect(botRuleHelper.appliesTo({ connection: 'c2' }, 'c1')).toBe(false)
128
- })
129
- })
File without changes