@leemour/cli-messaging 0.145.0 → 0.147.0

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 (103) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES +34 -0
  3. package/dist/background/units.d.ts +1 -0
  4. package/dist/background/units.d.ts.map +1 -1
  5. package/dist/background/units.js +9 -0
  6. package/dist/background/units.js.map +1 -1
  7. package/dist/cli/bot/people.js +1 -1
  8. package/dist/cli/bot/people.js.map +1 -1
  9. package/dist/cli/index.d.ts +4 -1
  10. package/dist/cli/index.d.ts.map +1 -1
  11. package/dist/cli/index.js +4 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/messenger/backfill-command.d.ts.map +1 -1
  14. package/dist/cli/messenger/backfill-command.js +4 -0
  15. package/dist/cli/messenger/backfill-command.js.map +1 -1
  16. package/dist/cli/messenger/chats-members-command.d.ts.map +1 -1
  17. package/dist/cli/messenger/chats-members-command.js +35 -1
  18. package/dist/cli/messenger/chats-members-command.js.map +1 -1
  19. package/dist/cli/messenger/chats-stats-command.js +1 -1
  20. package/dist/cli/messenger/chats-stats-command.js.map +1 -1
  21. package/dist/cli/messenger/context.d.ts +2 -0
  22. package/dist/cli/messenger/context.d.ts.map +1 -1
  23. package/dist/cli/messenger/context.js.map +1 -1
  24. package/dist/cli/messenger/messages-search-command.d.ts.map +1 -1
  25. package/dist/cli/messenger/messages-search-command.js +3 -1
  26. package/dist/cli/messenger/messages-search-command.js.map +1 -1
  27. package/dist/cli/messenger/points.d.ts +2 -1
  28. package/dist/cli/messenger/points.d.ts.map +1 -1
  29. package/dist/cli/messenger/points.js +1 -0
  30. package/dist/cli/messenger/points.js.map +1 -1
  31. package/dist/cli/messenger/server-command.d.ts.map +1 -1
  32. package/dist/cli/messenger/server-command.js +18 -2
  33. package/dist/cli/messenger/server-command.js.map +1 -1
  34. package/dist/domain/models.d.ts +10 -0
  35. package/dist/domain/models.d.ts.map +1 -1
  36. package/dist/mcp/prompts.d.ts.map +1 -1
  37. package/dist/mcp/prompts.js +27 -8
  38. package/dist/mcp/prompts.js.map +1 -1
  39. package/dist/mcp/tools/chats.d.ts.map +1 -1
  40. package/dist/mcp/tools/chats.js +19 -1
  41. package/dist/mcp/tools/chats.js.map +1 -1
  42. package/dist/mcp/tools/inbox.d.ts +2 -0
  43. package/dist/mcp/tools/inbox.d.ts.map +1 -1
  44. package/dist/mcp/tools/inbox.js +27 -4
  45. package/dist/mcp/tools/inbox.js.map +1 -1
  46. package/dist/mcp/tools/review.d.ts.map +1 -1
  47. package/dist/mcp/tools/review.js +23 -2
  48. package/dist/mcp/tools/review.js.map +1 -1
  49. package/dist/replies/decide.d.ts +34 -0
  50. package/dist/replies/decide.d.ts.map +1 -0
  51. package/dist/replies/decide.js +109 -0
  52. package/dist/replies/decide.js.map +1 -0
  53. package/dist/replies/rules.d.ts +127 -0
  54. package/dist/replies/rules.d.ts.map +1 -0
  55. package/dist/replies/rules.js +119 -0
  56. package/dist/replies/rules.js.map +1 -0
  57. package/dist/replies/state.d.ts +36 -0
  58. package/dist/replies/state.d.ts.map +1 -0
  59. package/dist/replies/state.js +67 -0
  60. package/dist/replies/state.js.map +1 -0
  61. package/dist/search/search.d.ts.map +1 -1
  62. package/dist/search/search.js +4 -3
  63. package/dist/search/search.js.map +1 -1
  64. package/dist/search/snowball/LICENSE +29 -0
  65. package/dist/search/snowball/base-stemmer.js +445 -0
  66. package/dist/search/snowball/english-stemmer.js +775 -0
  67. package/dist/search/snowball/russian-stemmer.js +468 -0
  68. package/dist/search/snowball/spanish-stemmer.js +722 -0
  69. package/dist/search/stem.d.ts +35 -0
  70. package/dist/search/stem.d.ts.map +1 -0
  71. package/dist/search/stem.js +76 -0
  72. package/dist/search/stem.js.map +1 -0
  73. package/dist/services/archive.d.ts.map +1 -1
  74. package/dist/services/archive.js +4 -2
  75. package/dist/services/archive.js.map +1 -1
  76. package/dist/services/chat-stats.d.ts +3 -0
  77. package/dist/services/chat-stats.d.ts.map +1 -1
  78. package/dist/services/chat-stats.js +3 -0
  79. package/dist/services/chat-stats.js.map +1 -1
  80. package/dist/services/chats.d.ts +10 -0
  81. package/dist/services/chats.d.ts.map +1 -1
  82. package/dist/services/chats.js +61 -0
  83. package/dist/services/chats.js.map +1 -1
  84. package/dist/services/members-audit.d.ts +44 -0
  85. package/dist/services/members-audit.d.ts.map +1 -0
  86. package/dist/services/members-audit.js +111 -0
  87. package/dist/services/members-audit.js.map +1 -0
  88. package/dist/services/messages-search.d.ts +4 -2
  89. package/dist/services/messages-search.d.ts.map +1 -1
  90. package/dist/services/messages-search.js +13 -5
  91. package/dist/services/messages-search.js.map +1 -1
  92. package/dist/services/messages.js +1 -1
  93. package/dist/services/messages.js.map +1 -1
  94. package/dist/store/sqlite/completeness.d.ts +10 -1
  95. package/dist/store/sqlite/completeness.d.ts.map +1 -1
  96. package/dist/store/sqlite/completeness.js +15 -3
  97. package/dist/store/sqlite/completeness.js.map +1 -1
  98. package/dist/store/store.d.ts +2 -1
  99. package/dist/store/store.d.ts.map +1 -1
  100. package/dist/store/store.js +9 -2
  101. package/dist/store/store.js.map +1 -1
  102. package/package.json +2 -2
  103. package/parity.json +58 -0
@@ -1,18 +1,24 @@
1
+ import { CliError } from "@leemour/cli-core";
1
2
  import * as v from "valibot";
3
+ import { checkPoints } from "../../cli/messenger/points.js";
2
4
  import { REVIEW_DAYS, reviewStart, UNANSWERED_HOURS } from "../../services/inbox.js";
3
5
  import { momentOf } from "../../services/moment.js";
4
6
  import { heard, hearForTool, modelWith } from "../../speech/hearing.js";
5
7
  import { chatOf, READ, tool } from "../tool.js";
8
+ import { kindsInput } from "./inbox.js";
6
9
  export const reviewTools = (messenger) => ({
7
10
  review: tool({
8
11
  title: "Review who owes what",
9
12
  description: "Every message, the owner's too (outgoing: true), in each chat that changed since a point — for sorting out " +
10
- `what the owner owes and what others owe. Without \`since_time\`, the last ${REVIEW_DAYS} days. Muted and archived ` +
13
+ `what the owner owes and what others owe. Without \`since_time\`, the last ${REVIEW_DAYS} days; with \`new\`, ` +
14
+ "what changed since the last call with new, from points this tool keeps apart from the owner's own. Muted and archived " +
11
15
  "chats are left out unless they mention the owner, or `all` is set. Returns { since, until, complete, chats: " +
12
16
  "[{ id, title, messages, more }], skipped, partial, quiet }; when complete, the next review starts at until.",
13
17
  input: v.object({
14
18
  since_time: v.optional(v.pipe(v.string(), v.description("where the last review ended: ISO 8601, or 2h / 1d ago"))),
19
+ new: v.optional(v.pipe(v.boolean(), v.description("what changed since the last call with new"))),
15
20
  chat: v.optional(chatOf(messenger)),
21
+ kinds: kindsInput,
16
22
  unanswered: v.optional(v.pipe(v.number(), v.minValue(0), v.description(`only questions nobody answered, asked at least this many hours ago (${UNANSWERED_HOURS} is usual)`))),
17
23
  all: v.optional(v.pipe(v.boolean(), v.description("muted and archived chats too"))),
18
24
  transcribe: v.optional(v.pipe(v.boolean(), v.description("turn voice messages not heard yet into text; can take minutes"))),
@@ -20,12 +26,26 @@ export const reviewTools = (messenger) => ({
20
26
  }),
21
27
  annotations: READ,
22
28
  served: async (services, args, defaults, connect) => {
29
+ if (args.new && (args.since_time !== undefined || args.unanswered !== undefined)) {
30
+ // An open question stays open: moving past it would drop it from the next review.
31
+ throw new CliError("validation_error", "new keeps its own point — not with since_time or unanswered");
32
+ }
33
+ const saved = args.new
34
+ ? checkPoints(messenger.app, {
35
+ command: "mcp-review",
36
+ profile: defaults.settings.profile,
37
+ env: defaults.env,
38
+ firstLookMs: REVIEW_DAYS * 86_400_000,
39
+ })
40
+ : undefined;
23
41
  const model = modelWith(args.transcribe, args.model);
24
42
  let hearing;
25
43
  const transcribe = args.transcribe === true;
26
44
  const found = await services.inbox.review({
27
- since: args.since_time === undefined ? reviewStart() : momentOf(args.since_time, "since_time"),
45
+ since: args.since_time === undefined ? (saved?.first ?? reviewStart()) : momentOf(args.since_time, "since_time"),
46
+ ...(saved === undefined ? {} : { points: saved.chats }),
28
47
  ...(args.chat === undefined ? {} : { chat: args.chat }),
48
+ ...(args.kinds === undefined ? {} : { kinds: args.kinds }),
29
49
  ...(args.all ? { all: true } : {}),
30
50
  ...(args.unanswered === undefined ? {} : { unansweredAfterHours: args.unanswered }),
31
51
  enrich: async (raw) => {
@@ -37,6 +57,7 @@ export const reviewTools = (messenger) => ({
37
57
  };
38
58
  },
39
59
  });
60
+ saved?.save(found.checked ?? {});
40
61
  return {
41
62
  ...found,
42
63
  complete: found.complete && (hearing?.unheard.length ?? 0) === 0,
@@ -1 +1 @@
1
- {"version":3,"file":"review.js","sourceRoot":"","sources":["../../../src/mcp/tools/review.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../../../src/mcp/tools/review.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { Chat, Id, Message } from "../domain/models.js";
2
+ import { type ReplyRule } from "./rules.js";
3
+ import { type RepliesState } from "./state.js";
4
+ /** What the matcher knows about one arriving message beyond the message itself. */
5
+ export interface Incoming {
6
+ message: Message;
7
+ chat: Pick<Chat, "id" | "kind">;
8
+ /** The owner, to tell a mention or a reply to them; `null` when the account is not known. */
9
+ owner: {
10
+ id: Id | null;
11
+ username?: string;
12
+ };
13
+ sender: {
14
+ isBot: boolean;
15
+ isContact: boolean;
16
+ };
17
+ /** ms: when `serve` began catching up. Older messages are never answered — a week away must not get a week of replies. */
18
+ since: number;
19
+ }
20
+ export type Decision = {
21
+ reply: {
22
+ text: string;
23
+ asReply: boolean;
24
+ model: ReplyRule["reply"]["model"];
25
+ };
26
+ } | {
27
+ skip: string;
28
+ };
29
+ /**
30
+ * Whether `rule` answers this message at `now`, and with what — or why not, in words `replies test`
31
+ * can print. Pure: it reads `state` and changes nothing; the caller records a reply it sent.
32
+ */
33
+ export declare const decide: (rule: ReplyRule, incoming: Incoming, state: RepliesState, now: number) => Decision;
34
+ //# sourceMappingURL=decide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/replies/decide.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC5D,OAAO,EAA8B,KAAK,SAAS,EAAe,MAAM,YAAY,CAAA;AACpF,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAE3D,mFAAmF;AACnF,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC,CAAA;IAC/B,6FAA6F;IAC7F,KAAK,EAAE;QAAE,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,0HAA0H;IAC1H,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,QAAQ,GAChB;IAAE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAA;KAAE,CAAA;CAAE,GACjF;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAIpB;;;GAGG;AACH,eAAO,MAAM,MAAM,SAAU,SAAS,YAAY,QAAQ,SAAS,YAAY,OAAO,MAAM,KAAG,QAwC9F,CAAA"}
@@ -0,0 +1,109 @@
1
+ import { DAYS } from "./rules.js";
2
+ import { answeredKey } from "./state.js";
3
+ const skip = (why) => ({ skip: why });
4
+ /**
5
+ * Whether `rule` answers this message at `now`, and with what — or why not, in words `replies test`
6
+ * can print. Pure: it reads `state` and changes nothing; the caller records a reply it sent.
7
+ */
8
+ export const decide = (rule, incoming, state, now) => {
9
+ const { message, chat, owner, sender } = incoming;
10
+ if (state.paused)
11
+ return skip("replies are paused");
12
+ if (!rule.on)
13
+ return skip("the rule is off");
14
+ if (message.outgoing !== false)
15
+ return skip(message.outgoing ? "your own message" : "the account is not known");
16
+ if (message.senderIsChat || message.senderId === null)
17
+ return skip("sent as a chat, not by a person");
18
+ if (sender.isBot)
19
+ return skip("sent by a bot");
20
+ if (chat.kind !== "dialog" && chat.kind !== "group")
21
+ return skip(`a ${chat.kind} is never answered`);
22
+ if (message.editedAt !== null)
23
+ return skip("an edited message");
24
+ if (message.scheduledFor !== undefined)
25
+ return skip("a scheduled message");
26
+ if (Date.parse(message.timestamp) < incoming.since)
27
+ return skip("older than the catch-up start");
28
+ if (state.answered.includes(answeredKey(message)))
29
+ return skip("already answered");
30
+ const { where, when } = rule;
31
+ if (where.kinds.length > 0 && !where.kinds.includes(chat.kind))
32
+ return skip(`the rule does not answer a ${chat.kind}`);
33
+ if (where.chats.length > 0 && !where.chats.includes(chat.id))
34
+ return skip("not one of the rule's chats");
35
+ if (where.notChats.includes(chat.id))
36
+ return skip("one of the chats the rule leaves out");
37
+ const toMe = mentionsOwner(message, owner);
38
+ if (chat.kind === "group" && !where.chats.includes(chat.id) && !toMe) {
39
+ return skip("a group message that neither mentions you nor replies to you");
40
+ }
41
+ if (when.hours && !outside(when.hours, now))
42
+ return skip("inside the hours the rule leaves alone");
43
+ if (when.words.length > 0 && !when.words.some((word) => hasWord(message.text, word))) {
44
+ return skip("none of the rule's words");
45
+ }
46
+ if (when.question && !isQuestion(message.text))
47
+ return skip("not a question");
48
+ if (when.mentionsMe && !toMe)
49
+ return skip("does not mention you or reply to you");
50
+ const { from } = when;
51
+ if (from.people.length > 0 && !from.people.includes(message.senderId))
52
+ return skip("not from the rule's people");
53
+ if (from.notPeople.includes(message.senderId))
54
+ return skip("from a person the rule leaves out");
55
+ if (from.contactsOnly && !sender.isContact)
56
+ return skip("not from a contact");
57
+ const sent = state.sent[rule.id];
58
+ if (over(sent?.chats[chat.id], rule.limits.perChat, now))
59
+ return skip("the chat's limit is reached");
60
+ if (over(sent?.people[message.senderId], rule.limits.perPerson, now))
61
+ return skip("the person's limit is reached");
62
+ const text = filled(rule.reply.template, message.senderName);
63
+ if (text === undefined)
64
+ return skip("the template needs a name the sender has not given");
65
+ return { reply: { text, asReply: rule.reply.asReply, model: rule.reply.model } };
66
+ };
67
+ const mentionsOwner = (message, owner) => message.replyTo?.outgoing === true ||
68
+ (owner.id !== null && (message.mentions ?? []).includes(owner.id)) ||
69
+ (owner.username !== undefined && hasWord(message.text, `@${owner.username}`));
70
+ const local = (zone, now) => {
71
+ const parts = new Intl.DateTimeFormat("en-US", {
72
+ timeZone: zone,
73
+ weekday: "short",
74
+ hour: "2-digit",
75
+ minute: "2-digit",
76
+ hourCycle: "h23",
77
+ }).formatToParts(now);
78
+ const part = (type) => parts.find((one) => one.type === type)?.value ?? "";
79
+ return {
80
+ day: part("weekday").toLowerCase(),
81
+ minute: Number(part("hour")) * 60 + Number(part("minute")),
82
+ };
83
+ };
84
+ /**
85
+ * Outside the working time: not on one of `days`, or on one but not inside the window. A window that
86
+ * crosses midnight belongs to the day it starts on, so Friday 22:00–02:00 covers Saturday 01:00.
87
+ */
88
+ const outside = ({ outside: window, days, timezone }, now) => {
89
+ const { day, minute } = local(timezone, now);
90
+ const crosses = window.to < window.from;
91
+ const working = crosses
92
+ ? (days.includes(day) && minute >= window.from) || (days.includes(dayBefore(day)) && minute < window.to)
93
+ : days.includes(day) && minute >= window.from && minute < window.to;
94
+ return !working;
95
+ };
96
+ const dayBefore = (day) => DAYS[(DAYS.indexOf(day) + 6) % 7];
97
+ const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
98
+ /** Whole words, any case, any script: `price` is not found in `prices`, `цена` is found in `Цена?`. */
99
+ const hasWord = (text, word) => new RegExp(`(?<![\\p{L}\\p{N}_])${escaped(word.trim())}(?![\\p{L}\\p{N}_])`, "iu").test(text);
100
+ /** A shared link's query string is not a question. */
101
+ const isQuestion = (text) => text.replace(/https?:\/\/\S+/g, "").includes("?");
102
+ const over = (sent, { count, ms }, now) => (sent ?? []).filter((at) => now - Date.parse(at) < ms).length >= count;
103
+ const filled = (template, senderName) => {
104
+ const name = senderName?.trim() ?? "";
105
+ if (/\{(firstName|name)\}/.test(template) && name === "")
106
+ return undefined;
107
+ return template.replaceAll("{firstName}", name.split(/\s+/)[0] ?? name).replaceAll("{name}", name);
108
+ };
109
+ //# sourceMappingURL=decide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide.js","sourceRoot":"","sources":["../../src/replies/decide.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ import * as v from "valibot";
2
+ import type { AppIdentity } from "../cli/app.js";
3
+ export declare const REPLY_ACTIONS: readonly ["reply", "task"];
4
+ export declare const REPLY_MODELS: readonly ["fill-only", "may-reword"];
5
+ /** Channels, saved messages and chats of unknown kind are never answered, so a rule cannot name them. */
6
+ export declare const REPLY_KINDS: readonly ["dialog", "group"];
7
+ export declare const PLACEHOLDERS: readonly ["firstName", "name"];
8
+ export declare const DAYS: readonly ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
9
+ export type Day = (typeof DAYS)[number];
10
+ /** Minutes after midnight, `[from, to)`; `to` below `from` crosses midnight. */
11
+ export interface Window {
12
+ from: number;
13
+ to: number;
14
+ }
15
+ /** At most `count` sends inside any `ms`. */
16
+ export interface Limit {
17
+ count: number;
18
+ ms: number;
19
+ }
20
+ declare const rule: v.SchemaWithPipe<readonly [v.StrictObjectSchema<{
21
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a rule id is lower-case letters, digits and -">]>;
22
+ readonly on: v.BooleanSchema<undefined>;
23
+ readonly do: v.SchemaWithPipe<readonly [v.ArraySchema<v.PicklistSchema<readonly ["reply", "task"], undefined>, undefined>, v.MinLengthAction<("reply" | "task")[], 1, "a rule has to do something: reply">]>;
24
+ readonly where: v.StrictObjectSchema<{
25
+ readonly kinds: v.ArraySchema<v.PicklistSchema<readonly ["dialog", "group"], undefined>, undefined>;
26
+ readonly chats: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
27
+ readonly notChats: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
28
+ }, undefined>;
29
+ readonly when: v.StrictObjectSchema<{
30
+ readonly hours: v.NullableSchema<v.StrictObjectSchema<{
31
+ readonly outside: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a window is written \"09:00-19:00\", 24-hour">, v.TransformAction<string, Window>, v.CheckAction<Window, "a window has to start and end at different times">]>;
32
+ readonly days: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TransformAction<string, ("fri" | "mon" | "sat" | "sun" | "thu" | "tue" | "wed")[] | undefined>, v.CheckAction<("fri" | "mon" | "sat" | "sun" | "thu" | "tue" | "wed")[] | undefined, `days are written "mon-fri" or "sat,sun", from ${string}`>, v.TransformAction<("fri" | "mon" | "sat" | "sun" | "thu" | "tue" | "wed")[] | undefined, ("fri" | "mon" | "sat" | "sun" | "thu" | "tue" | "wed")[]>]>;
33
+ readonly timezone: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "a time zone is written as \"Europe/Madrid\"">]>;
34
+ }, undefined>, undefined>;
35
+ readonly words: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MinLengthAction<string, 1, undefined>]>, undefined>;
36
+ readonly question: v.BooleanSchema<undefined>;
37
+ readonly mentionsMe: v.BooleanSchema<undefined>;
38
+ readonly from: v.StrictObjectSchema<{
39
+ readonly people: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
40
+ readonly notPeople: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
41
+ readonly contactsOnly: v.BooleanSchema<undefined>;
42
+ }, undefined>;
43
+ }, undefined>;
44
+ readonly reply: v.StrictObjectSchema<{
45
+ readonly template: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "a reply template cannot be empty">, v.CheckAction<string, `a template fills only ${string}`>]>;
46
+ readonly model: v.PicklistSchema<readonly ["fill-only", "may-reword"], undefined>;
47
+ readonly asReply: v.BooleanSchema<undefined>;
48
+ }, undefined>;
49
+ readonly limits: v.StrictObjectSchema<{
50
+ readonly perChat: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a limit is written \"1/12h\": this many replies in this long — m, h or d">, v.TransformAction<string, Limit>]>;
51
+ readonly perPerson: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a limit is written \"1/12h\": this many replies in this long — m, h or d">, v.TransformAction<string, Limit>]>;
52
+ }, undefined>;
53
+ }, undefined>, v.CheckAction<{
54
+ id: string;
55
+ on: boolean;
56
+ do: ("reply" | "task")[];
57
+ where: {
58
+ kinds: ("dialog" | "group")[];
59
+ chats: string[];
60
+ notChats: string[];
61
+ };
62
+ when: {
63
+ hours: {
64
+ outside: Window;
65
+ days: ("fri" | "mon" | "sat" | "sun" | "thu" | "tue" | "wed")[];
66
+ timezone: string;
67
+ } | null;
68
+ words: string[];
69
+ question: boolean;
70
+ mentionsMe: boolean;
71
+ from: {
72
+ people: string[];
73
+ notPeople: string[];
74
+ contactsOnly: boolean;
75
+ };
76
+ };
77
+ reply: {
78
+ template: string;
79
+ model: "fill-only" | "may-reword";
80
+ asReply: boolean;
81
+ };
82
+ limits: {
83
+ perChat: Limit;
84
+ perPerson: Limit;
85
+ };
86
+ }, "\"task\" waits for the tasks package; only \"reply\" works now">]>;
87
+ export type ReplyRule = v.InferOutput<typeof rule>;
88
+ /**
89
+ * Every key written out, so the file shows all there is to set, as moderation's does (max-cli
90
+ * `NEED-314`). Off until the owner turns it on, and the limits are the tightest worth having.
91
+ */
92
+ export declare const defaultRule: (id: string) => {
93
+ id: string;
94
+ on: boolean;
95
+ do: string[];
96
+ where: {
97
+ kinds: string[];
98
+ chats: never[];
99
+ notChats: never[];
100
+ };
101
+ when: {
102
+ hours: null;
103
+ words: never[];
104
+ question: boolean;
105
+ mentionsMe: boolean;
106
+ from: {
107
+ people: never[];
108
+ notPeople: never[];
109
+ contactsOnly: boolean;
110
+ };
111
+ };
112
+ reply: {
113
+ template: string;
114
+ model: string;
115
+ asReply: boolean;
116
+ };
117
+ limits: {
118
+ perChat: string;
119
+ perPerson: string;
120
+ };
121
+ };
122
+ export declare const repliesPathFor: (app: AppIdentity, profile: string, env?: NodeJS.ProcessEnv) => string;
123
+ /** The owner's rules, in file order. No file is no rules; a file that does not check out refuses, naming the field. */
124
+ export declare const readReplyRules: (path: string) => ReplyRule[];
125
+ export declare const parseReplyRules: (parsed: unknown, path: string) => ReplyRule[];
126
+ export {};
127
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/replies/rules.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,eAAO,MAAM,aAAa,YAAI,OAAO,EAAE,MAAM,CAAU,CAAA;AACvD,eAAO,MAAM,YAAY,YAAI,WAAW,EAAE,YAAY,CAAU,CAAA;AAChE,yGAAyG;AACzG,eAAO,MAAM,WAAW,YAAI,QAAQ,EAAE,OAAO,CAAU,CAAA;AACvD,eAAO,MAAM,YAAY,YAAI,WAAW,EAAE,MAAM,CAAU,CAAA;AAC1D,eAAO,MAAM,IAAI,YAAI,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAA;AAE9E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,CAAA;AAMvC,gFAAgF;AAChF,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAED,6CAA6C;AAC7C,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,EAAE,EAAE,MAAM,CAAA;CACX;AAoED,QAAA,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sEAkBT,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,IAAI,CAAC,CAAA;AAUlD;;;GAGG;AACH,eAAO,MAAM,WAAW,OAAQ,MAAM;;;;;QAI3B,KAAK;QAAc,KAAK;QAAM,QAAQ;;;QAE7C,KAAK;QACL,KAAK;QACL,QAAQ;QACR,UAAU;QACV,IAAI;YAAI,MAAM;YAAM,SAAS;YAAM,YAAY;;;;QAExC,QAAQ;QAAM,KAAK;QAAe,OAAO;;;QACxC,OAAO;QAAU,SAAS;;CACpC,CAAA;AAEF,eAAO,MAAM,cAAc,QAAS,WAAW,WAAW,MAAM,QAAO,MAAM,CAAC,UAAU,KAAiB,MACG,CAAA;AAE5G,uHAAuH;AACvH,eAAO,MAAM,cAAc,SAAU,MAAM,KAAG,SAAS,EAStD,CAAA;AAED,eAAO,MAAM,eAAe,WAAY,OAAO,QAAQ,MAAM,KAAG,SAAS,EAIxE,CAAA"}
@@ -0,0 +1,119 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CliError, resolvePaths } from "@leemour/cli-core";
4
+ import * as v from "valibot";
5
+ export const REPLY_ACTIONS = ["reply", "task"];
6
+ export const REPLY_MODELS = ["fill-only", "may-reword"];
7
+ /** Channels, saved messages and chats of unknown kind are never answered, so a rule cannot name them. */
8
+ export const REPLY_KINDS = ["dialog", "group"];
9
+ export const PLACEHOLDERS = ["firstName", "name"];
10
+ export const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
11
+ const WINDOW = /^([01]\d|2[0-3]):([0-5]\d)-([01]\d|2[0-3]):([0-5]\d)$/;
12
+ const LIMIT = /^([1-9]\d*)\/([1-9]\d*)(m|h|d)$/;
13
+ const UNIT_MS = { m: 60_000, h: 3_600_000, d: 86_400_000 };
14
+ const minutes = (hours, mins) => Number(hours) * 60 + Number(mins);
15
+ const window = v.pipe(v.string(), v.regex(WINDOW, 'a window is written "09:00-19:00", 24-hour'), v.transform((typed) => {
16
+ const [, h1, m1, h2, m2] = WINDOW.exec(typed);
17
+ return { from: minutes(h1, m1), to: minutes(h2, m2) };
18
+ }), v.check((one) => one.from !== one.to, "a window has to start and end at different times"));
19
+ const dayOf = (typed) => DAYS.indexOf(typed);
20
+ /** `mon-fri`, `sat,sun`, `fri-mon` across the week's end. */
21
+ const days = v.pipe(v.string(), v.transform((typed) => {
22
+ const found = new Set();
23
+ for (const part of typed.split(",").map((one) => one.trim().toLowerCase())) {
24
+ const [first = "", last = first] = part.split("-");
25
+ const from = dayOf(first);
26
+ const to = dayOf(last);
27
+ if (from < 0 || to < 0)
28
+ return undefined;
29
+ for (let at = from;; at = (at + 1) % 7) {
30
+ found.add(DAYS[at]);
31
+ if (at === to)
32
+ break;
33
+ }
34
+ }
35
+ return DAYS.filter((day) => found.has(day));
36
+ }), v.check((one) => one !== undefined, `days are written "mon-fri" or "sat,sun", from ${DAYS.join(", ")}`), v.transform((one) => one));
37
+ const knownZone = (zone) => {
38
+ try {
39
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ };
46
+ const timezone = v.pipe(v.string(), v.check(knownZone, 'a time zone is written as "Europe/Madrid"'));
47
+ const limit = v.pipe(v.string(), v.regex(LIMIT, 'a limit is written "1/12h": this many replies in this long — m, h or d'), v.transform((typed) => {
48
+ const [, count, amount, unit] = LIMIT.exec(typed);
49
+ return { count: Number(count), ms: Number(amount) * (UNIT_MS[unit ?? ""] ?? 0) };
50
+ }));
51
+ const ids = v.array(v.pipe(v.string(), v.minLength(1)));
52
+ const template = v.pipe(v.string(), v.minLength(1, "a reply template cannot be empty"), v.check((text) => [...text.matchAll(/\{(\w*)\}/g)].every(([, name]) => PLACEHOLDERS.includes(name)), `a template fills only ${PLACEHOLDERS.map((name) => `{${name}}`).join(", ")}`));
53
+ const rule = v.pipe(v.strictObject({
54
+ id: v.pipe(v.string(), v.regex(/^[a-z0-9][a-z0-9-]*$/, "a rule id is lower-case letters, digits and -")),
55
+ on: v.boolean(),
56
+ do: v.pipe(v.array(v.picklist(REPLY_ACTIONS)), v.minLength(1, "a rule has to do something: reply")),
57
+ where: v.strictObject({ kinds: v.array(v.picklist(REPLY_KINDS)), chats: ids, notChats: ids }),
58
+ when: v.strictObject({
59
+ hours: v.nullable(v.strictObject({ outside: window, days, timezone })),
60
+ words: v.array(v.pipe(v.string(), v.trim(), v.minLength(1))),
61
+ question: v.boolean(),
62
+ mentionsMe: v.boolean(),
63
+ from: v.strictObject({ people: ids, notPeople: ids, contactsOnly: v.boolean() }),
64
+ }),
65
+ reply: v.strictObject({ template, model: v.picklist(REPLY_MODELS), asReply: v.boolean() }),
66
+ limits: v.strictObject({ perChat: limit, perPerson: limit }),
67
+ }),
68
+ // NEED-582: a rule may open a task, but there is no tasks package to open it in yet.
69
+ v.check((one) => !one.do.includes("task"), '"task" waits for the tasks package; only "reply" works now'));
70
+ const file = v.pipe(v.strictObject({ rules: v.array(rule) }), v.check(({ rules }) => new Set(rules.map((one) => one.id)).size === rules.length, "two rules share an id; each needs its own"));
71
+ /**
72
+ * Every key written out, so the file shows all there is to set, as moderation's does (max-cli
73
+ * `NEED-314`). Off until the owner turns it on, and the limits are the tightest worth having.
74
+ */
75
+ export const defaultRule = (id) => ({
76
+ id,
77
+ on: false,
78
+ do: ["reply"],
79
+ where: { kinds: ["dialog"], chats: [], notChats: [] },
80
+ when: {
81
+ hours: null,
82
+ words: [],
83
+ question: false,
84
+ mentionsMe: false,
85
+ from: { people: [], notPeople: [], contactsOnly: false },
86
+ },
87
+ reply: { template: "", model: "fill-only", asReply: true },
88
+ limits: { perChat: "1/1d", perPerson: "1/1d" },
89
+ });
90
+ export const repliesPathFor = (app, profile, env = process.env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).config, `${profile}.replies.json`);
91
+ /** The owner's rules, in file order. No file is no rules; a file that does not check out refuses, naming the field. */
92
+ export const readReplyRules = (path) => {
93
+ if (!existsSync(path))
94
+ return [];
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(readFileSync(path, "utf8"));
98
+ }
99
+ catch (error) {
100
+ throw broken(path, error instanceof Error ? error.message : String(error));
101
+ }
102
+ return parseReplyRules(parsed, path);
103
+ };
104
+ export const parseReplyRules = (parsed, path) => {
105
+ const checked = v.safeParse(file, parsed);
106
+ if (!checked.success)
107
+ throw broken(path, checked.issues.map(problem).join("; "));
108
+ return checked.output.rules;
109
+ };
110
+ const problem = (issue) => {
111
+ const at = v.getDotPath(issue) ?? "the file";
112
+ if (issue.kind === "schema" && issue.expected === "never")
113
+ return `${at} is not a field a rule has`;
114
+ if (issue.expected === "Object" && issue.received === "undefined")
115
+ return `${at} is missing`;
116
+ return `${at}: ${issue.message}`;
117
+ };
118
+ const broken = (path, why) => new CliError("configuration_error", `the reply rules ${path} cannot be read (${why}) — fix the file`);
119
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../../src/replies/rules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import * as v from "valibot";
2
+ import type { AppIdentity } from "../cli/app.js";
3
+ import type { Message } from "../domain/models.js";
4
+ import type { ReplyRule } from "./rules.js";
5
+ /** Enough to cover any restart's catch-up; older ids are past the catch-up start and never answered anyway. */
6
+ export declare const ANSWERED_KEPT = 1000;
7
+ declare const stateFile: v.ObjectSchema<{
8
+ readonly paused: v.BooleanSchema<undefined>;
9
+ /** `<chat>:<message>`, oldest first. */
10
+ readonly answered: v.ArraySchema<v.StringSchema<undefined>, undefined>;
11
+ /** Per rule, when it replied, by chat and by person: what its limits count. */
12
+ readonly sent: v.RecordSchema<v.StringSchema<undefined>, v.ObjectSchema<{
13
+ readonly chats: v.RecordSchema<v.StringSchema<undefined>, v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
14
+ readonly people: v.RecordSchema<v.StringSchema<undefined>, v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
15
+ }, undefined>, undefined>;
16
+ }, undefined>;
17
+ export type RepliesState = v.InferOutput<typeof stateFile>;
18
+ export declare const emptyState: () => RepliesState;
19
+ export declare const answeredKey: (message: Pick<Message, "chatId" | "id">) => string;
20
+ /**
21
+ * What the rules did, apart from the rules: editing the rules file never forgets what was already
22
+ * answered. Read on every message, so `pause` stops a running `serve` without a restart.
23
+ */
24
+ export declare const repliesStatePathFor: (app: AppIdentity, profile: string, env?: NodeJS.ProcessEnv) => string;
25
+ /** A file that does not check out refuses: guessing would forget what was answered, and answer it twice. */
26
+ export declare const readRepliesState: (path: string) => RepliesState;
27
+ export declare const writeRepliesState: (path: string, state: RepliesState) => void;
28
+ export declare const paused: (state: RepliesState, on: boolean) => RepliesState;
29
+ /**
30
+ * The state after `rule` replied to `message` at `now`: the message answered, the reply counted for
31
+ * its chat and its sender. Counts older than the rule's longest limit are dropped, since no limit
32
+ * reads them.
33
+ */
34
+ export declare const recordReply: (state: RepliesState, rule: Pick<ReplyRule, "id" | "limits">, message: Pick<Message, "chatId" | "id" | "senderId">, now: number) => RepliesState;
35
+ export {};
36
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/replies/state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,OAAO,KAAK,EAAM,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,+GAA+G;AAC/G,eAAO,MAAM,aAAa,OAAO,CAAA;AAIjC,QAAA,MAAM,SAAS;;IAEb,wCAAwC;;IAExC,+EAA+E;;;;;aAE/E,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,SAAS,CAAC,CAAA;AAE1D,eAAO,MAAM,UAAU,QAAO,YAA2D,CAAA;AAEzF,eAAO,MAAM,WAAW,YAAa,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC,KAAG,MAA2C,CAAA;AAEjH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAS,WAAW,WAAW,MAAM,QAAO,MAAM,CAAC,UAAU,KAAiB,MACG,CAAA;AAEjH,4GAA4G;AAC5G,eAAO,MAAM,gBAAgB,SAAU,MAAM,KAAG,YAa/C,CAAA;AAED,eAAO,MAAM,iBAAiB,SAAU,MAAM,SAAS,YAAY,KAAG,IACZ,CAAA;AAE1D,eAAO,MAAM,MAAM,UAAW,YAAY,MAAM,OAAO,KAAG,YAA0C,CAAA;AAEpG;;;;GAIG;AACH,eAAO,MAAM,WAAW,UACf,YAAY,QACb,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,QAAQ,CAAC,WAC7B,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,IAAI,GAAG,UAAU,CAAC,OAC/C,MAAM,KACV,YAsBF,CAAA"}
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CliError, resolvePaths, writeSecurely } from "@leemour/cli-core";
4
+ import * as v from "valibot";
5
+ /** Enough to cover any restart's catch-up; older ids are past the catch-up start and never answered anyway. */
6
+ export const ANSWERED_KEPT = 1000;
7
+ const times = v.record(v.string(), v.array(v.string()));
8
+ const stateFile = v.object({
9
+ paused: v.boolean(),
10
+ /** `<chat>:<message>`, oldest first. */
11
+ answered: v.array(v.string()),
12
+ /** Per rule, when it replied, by chat and by person: what its limits count. */
13
+ sent: v.record(v.string(), v.object({ chats: times, people: times })),
14
+ });
15
+ export const emptyState = () => ({ paused: false, answered: [], sent: {} });
16
+ export const answeredKey = (message) => `${message.chatId}:${message.id}`;
17
+ /**
18
+ * What the rules did, apart from the rules: editing the rules file never forgets what was already
19
+ * answered. Read on every message, so `pause` stops a running `serve` without a restart.
20
+ */
21
+ export const repliesStatePathFor = (app, profile, env = process.env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, `${profile}.replies-state.json`);
22
+ /** A file that does not check out refuses: guessing would forget what was answered, and answer it twice. */
23
+ export const readRepliesState = (path) => {
24
+ if (!existsSync(path))
25
+ return emptyState();
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(readFileSync(path, "utf8"));
29
+ }
30
+ catch (error) {
31
+ throw broken(path, error instanceof Error ? error.message : String(error));
32
+ }
33
+ const checked = v.safeParse(stateFile, parsed);
34
+ if (!checked.success) {
35
+ throw broken(path, checked.issues.map((issue) => `${v.getDotPath(issue) ?? "file"}: ${issue.message}`).join("; "));
36
+ }
37
+ return checked.output;
38
+ };
39
+ export const writeRepliesState = (path, state) => writeSecurely(path, `${JSON.stringify(state)}\n`, 0o600);
40
+ export const paused = (state, on) => ({ ...state, paused: on });
41
+ /**
42
+ * The state after `rule` replied to `message` at `now`: the message answered, the reply counted for
43
+ * its chat and its sender. Counts older than the rule's longest limit are dropped, since no limit
44
+ * reads them.
45
+ */
46
+ export const recordReply = (state, rule, message, now) => {
47
+ const oldest = now - Math.max(rule.limits.perChat.ms, rule.limits.perPerson.ms);
48
+ const kept = (all, key) => {
49
+ const recent = Object.fromEntries(Object.entries(all)
50
+ .map(([id, at]) => [id, at.filter((one) => Date.parse(one) > oldest)])
51
+ .filter(([, at]) => at.length > 0));
52
+ if (key === null)
53
+ return recent;
54
+ return { ...recent, [key]: [...(recent[key] ?? []), new Date(now).toISOString()] };
55
+ };
56
+ const before = state.sent[rule.id] ?? { chats: {}, people: {} };
57
+ return {
58
+ ...state,
59
+ answered: [...state.answered.filter((one) => one !== answeredKey(message)), answeredKey(message)].slice(-ANSWERED_KEPT),
60
+ sent: {
61
+ ...state.sent,
62
+ [rule.id]: { chats: kept(before.chats, message.chatId), people: kept(before.people, message.senderId) },
63
+ },
64
+ };
65
+ };
66
+ const broken = (path, why) => new CliError("configuration_error", `the reply state ${path} cannot be read (${why}) — fix or move the file`);
67
+ //# sourceMappingURL=state.js.map