windsor-bot 0.1.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 (226) hide show
  1. package/README.md +128 -0
  2. package/SPEC.md +179 -0
  3. package/assets/pokemon-sprites/1.png +0 -0
  4. package/assets/pokemon-sprites/10.png +0 -0
  5. package/assets/pokemon-sprites/100.png +0 -0
  6. package/assets/pokemon-sprites/101.png +0 -0
  7. package/assets/pokemon-sprites/102.png +0 -0
  8. package/assets/pokemon-sprites/103.png +0 -0
  9. package/assets/pokemon-sprites/104.png +0 -0
  10. package/assets/pokemon-sprites/105.png +0 -0
  11. package/assets/pokemon-sprites/106.png +0 -0
  12. package/assets/pokemon-sprites/107.png +0 -0
  13. package/assets/pokemon-sprites/108.png +0 -0
  14. package/assets/pokemon-sprites/109.png +0 -0
  15. package/assets/pokemon-sprites/11.png +0 -0
  16. package/assets/pokemon-sprites/110.png +0 -0
  17. package/assets/pokemon-sprites/111.png +0 -0
  18. package/assets/pokemon-sprites/112.png +0 -0
  19. package/assets/pokemon-sprites/113.png +0 -0
  20. package/assets/pokemon-sprites/114.png +0 -0
  21. package/assets/pokemon-sprites/115.png +0 -0
  22. package/assets/pokemon-sprites/116.png +0 -0
  23. package/assets/pokemon-sprites/117.png +0 -0
  24. package/assets/pokemon-sprites/118.png +0 -0
  25. package/assets/pokemon-sprites/119.png +0 -0
  26. package/assets/pokemon-sprites/12.png +0 -0
  27. package/assets/pokemon-sprites/120.png +0 -0
  28. package/assets/pokemon-sprites/121.png +0 -0
  29. package/assets/pokemon-sprites/122.png +0 -0
  30. package/assets/pokemon-sprites/123.png +0 -0
  31. package/assets/pokemon-sprites/124.png +0 -0
  32. package/assets/pokemon-sprites/125.png +0 -0
  33. package/assets/pokemon-sprites/126.png +0 -0
  34. package/assets/pokemon-sprites/127.png +0 -0
  35. package/assets/pokemon-sprites/128.png +0 -0
  36. package/assets/pokemon-sprites/129.png +0 -0
  37. package/assets/pokemon-sprites/13.png +0 -0
  38. package/assets/pokemon-sprites/130.png +0 -0
  39. package/assets/pokemon-sprites/131.png +0 -0
  40. package/assets/pokemon-sprites/132.png +0 -0
  41. package/assets/pokemon-sprites/133.png +0 -0
  42. package/assets/pokemon-sprites/134.png +0 -0
  43. package/assets/pokemon-sprites/135.png +0 -0
  44. package/assets/pokemon-sprites/136.png +0 -0
  45. package/assets/pokemon-sprites/137.png +0 -0
  46. package/assets/pokemon-sprites/138.png +0 -0
  47. package/assets/pokemon-sprites/139.png +0 -0
  48. package/assets/pokemon-sprites/14.png +0 -0
  49. package/assets/pokemon-sprites/140.png +0 -0
  50. package/assets/pokemon-sprites/141.png +0 -0
  51. package/assets/pokemon-sprites/142.png +0 -0
  52. package/assets/pokemon-sprites/143.png +0 -0
  53. package/assets/pokemon-sprites/144.png +0 -0
  54. package/assets/pokemon-sprites/145.png +0 -0
  55. package/assets/pokemon-sprites/146.png +0 -0
  56. package/assets/pokemon-sprites/147.png +0 -0
  57. package/assets/pokemon-sprites/148.png +0 -0
  58. package/assets/pokemon-sprites/149.png +0 -0
  59. package/assets/pokemon-sprites/15.png +0 -0
  60. package/assets/pokemon-sprites/150.png +0 -0
  61. package/assets/pokemon-sprites/151.png +0 -0
  62. package/assets/pokemon-sprites/16.png +0 -0
  63. package/assets/pokemon-sprites/17.png +0 -0
  64. package/assets/pokemon-sprites/18.png +0 -0
  65. package/assets/pokemon-sprites/19.png +0 -0
  66. package/assets/pokemon-sprites/2.png +0 -0
  67. package/assets/pokemon-sprites/20.png +0 -0
  68. package/assets/pokemon-sprites/21.png +0 -0
  69. package/assets/pokemon-sprites/22.png +0 -0
  70. package/assets/pokemon-sprites/23.png +0 -0
  71. package/assets/pokemon-sprites/24.png +0 -0
  72. package/assets/pokemon-sprites/25.png +0 -0
  73. package/assets/pokemon-sprites/26.png +0 -0
  74. package/assets/pokemon-sprites/27.png +0 -0
  75. package/assets/pokemon-sprites/28.png +0 -0
  76. package/assets/pokemon-sprites/29.png +0 -0
  77. package/assets/pokemon-sprites/3.png +0 -0
  78. package/assets/pokemon-sprites/30.png +0 -0
  79. package/assets/pokemon-sprites/31.png +0 -0
  80. package/assets/pokemon-sprites/32.png +0 -0
  81. package/assets/pokemon-sprites/33.png +0 -0
  82. package/assets/pokemon-sprites/34.png +0 -0
  83. package/assets/pokemon-sprites/35.png +0 -0
  84. package/assets/pokemon-sprites/36.png +0 -0
  85. package/assets/pokemon-sprites/37.png +0 -0
  86. package/assets/pokemon-sprites/38.png +0 -0
  87. package/assets/pokemon-sprites/39.png +0 -0
  88. package/assets/pokemon-sprites/4.png +0 -0
  89. package/assets/pokemon-sprites/40.png +0 -0
  90. package/assets/pokemon-sprites/41.png +0 -0
  91. package/assets/pokemon-sprites/42.png +0 -0
  92. package/assets/pokemon-sprites/43.png +0 -0
  93. package/assets/pokemon-sprites/44.png +0 -0
  94. package/assets/pokemon-sprites/45.png +0 -0
  95. package/assets/pokemon-sprites/46.png +0 -0
  96. package/assets/pokemon-sprites/47.png +0 -0
  97. package/assets/pokemon-sprites/48.png +0 -0
  98. package/assets/pokemon-sprites/49.png +0 -0
  99. package/assets/pokemon-sprites/5.png +0 -0
  100. package/assets/pokemon-sprites/50.png +0 -0
  101. package/assets/pokemon-sprites/51.png +0 -0
  102. package/assets/pokemon-sprites/52.png +0 -0
  103. package/assets/pokemon-sprites/53.png +0 -0
  104. package/assets/pokemon-sprites/54.png +0 -0
  105. package/assets/pokemon-sprites/55.png +0 -0
  106. package/assets/pokemon-sprites/56.png +0 -0
  107. package/assets/pokemon-sprites/57.png +0 -0
  108. package/assets/pokemon-sprites/58.png +0 -0
  109. package/assets/pokemon-sprites/59.png +0 -0
  110. package/assets/pokemon-sprites/6.png +0 -0
  111. package/assets/pokemon-sprites/60.png +0 -0
  112. package/assets/pokemon-sprites/61.png +0 -0
  113. package/assets/pokemon-sprites/62.png +0 -0
  114. package/assets/pokemon-sprites/63.png +0 -0
  115. package/assets/pokemon-sprites/64.png +0 -0
  116. package/assets/pokemon-sprites/65.png +0 -0
  117. package/assets/pokemon-sprites/66.png +0 -0
  118. package/assets/pokemon-sprites/67.png +0 -0
  119. package/assets/pokemon-sprites/68.png +0 -0
  120. package/assets/pokemon-sprites/69.png +0 -0
  121. package/assets/pokemon-sprites/7.png +0 -0
  122. package/assets/pokemon-sprites/70.png +0 -0
  123. package/assets/pokemon-sprites/71.png +0 -0
  124. package/assets/pokemon-sprites/72.png +0 -0
  125. package/assets/pokemon-sprites/73.png +0 -0
  126. package/assets/pokemon-sprites/74.png +0 -0
  127. package/assets/pokemon-sprites/75.png +0 -0
  128. package/assets/pokemon-sprites/76.png +0 -0
  129. package/assets/pokemon-sprites/77.png +0 -0
  130. package/assets/pokemon-sprites/78.png +0 -0
  131. package/assets/pokemon-sprites/79.png +0 -0
  132. package/assets/pokemon-sprites/8.png +0 -0
  133. package/assets/pokemon-sprites/80.png +0 -0
  134. package/assets/pokemon-sprites/81.png +0 -0
  135. package/assets/pokemon-sprites/82.png +0 -0
  136. package/assets/pokemon-sprites/83.png +0 -0
  137. package/assets/pokemon-sprites/84.png +0 -0
  138. package/assets/pokemon-sprites/85.png +0 -0
  139. package/assets/pokemon-sprites/86.png +0 -0
  140. package/assets/pokemon-sprites/87.png +0 -0
  141. package/assets/pokemon-sprites/88.png +0 -0
  142. package/assets/pokemon-sprites/89.png +0 -0
  143. package/assets/pokemon-sprites/9.png +0 -0
  144. package/assets/pokemon-sprites/90.png +0 -0
  145. package/assets/pokemon-sprites/91.png +0 -0
  146. package/assets/pokemon-sprites/92.png +0 -0
  147. package/assets/pokemon-sprites/93.png +0 -0
  148. package/assets/pokemon-sprites/94.png +0 -0
  149. package/assets/pokemon-sprites/95.png +0 -0
  150. package/assets/pokemon-sprites/96.png +0 -0
  151. package/assets/pokemon-sprites/97.png +0 -0
  152. package/assets/pokemon-sprites/98.png +0 -0
  153. package/assets/pokemon-sprites/99.png +0 -0
  154. package/dist/ai.js +193 -0
  155. package/dist/bot.d.ts +22 -0
  156. package/dist/bot.d.ts.map +1 -0
  157. package/dist/bot.js +547 -0
  158. package/dist/bot.js.map +1 -0
  159. package/dist/commands/debug-fail.js +8 -0
  160. package/dist/commands/hello-world.js +12 -0
  161. package/dist/commands/hello.d.ts +3 -0
  162. package/dist/commands/hello.d.ts.map +1 -0
  163. package/dist/commands/hello.js +8 -0
  164. package/dist/commands/hello.js.map +1 -0
  165. package/dist/commands/index.d.ts +3 -0
  166. package/dist/commands/index.d.ts.map +1 -0
  167. package/dist/commands/index.js +12 -0
  168. package/dist/commands/index.js.map +1 -0
  169. package/dist/commands/pokemon.js +132 -0
  170. package/dist/commands/print.d.ts +9 -0
  171. package/dist/commands/print.d.ts.map +1 -0
  172. package/dist/commands/print.js +46 -0
  173. package/dist/commands/print.js.map +1 -0
  174. package/dist/commands/sudoku.js +79 -0
  175. package/dist/commands/util.js +13 -0
  176. package/dist/commands/wordsearch.js +109 -0
  177. package/dist/config.d.ts +8 -0
  178. package/dist/config.d.ts.map +1 -0
  179. package/dist/config.js +149 -0
  180. package/dist/config.js.map +1 -0
  181. package/dist/index.d.ts +2 -0
  182. package/dist/index.d.ts.map +1 -0
  183. package/dist/index.js +85 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/printer.js +245 -0
  186. package/dist/printing/cups.js +89 -0
  187. package/dist/printing/escp.js +147 -0
  188. package/dist/printing/imagefeed.js +212 -0
  189. package/dist/printing/index.js +54 -0
  190. package/dist/reactions.js +5 -0
  191. package/dist/server.d.ts +5 -0
  192. package/dist/server.d.ts.map +1 -0
  193. package/dist/server.js +312 -0
  194. package/dist/server.js.map +1 -0
  195. package/dist/tests/config.test.js +78 -0
  196. package/dist/tests/urls.test.js +54 -0
  197. package/dist/types.d.ts +55 -0
  198. package/dist/types.d.ts.map +1 -0
  199. package/dist/types.js +1 -0
  200. package/dist/types.js.map +1 -0
  201. package/dist/web/app.js +325 -0
  202. package/package.json +45 -0
  203. package/scripts/download-pokemon-sprites.mjs +38 -0
  204. package/src/ai.ts +214 -0
  205. package/src/bot.ts +657 -0
  206. package/src/commands/debug-fail.ts +11 -0
  207. package/src/commands/hello-world.ts +15 -0
  208. package/src/commands/index.ts +35 -0
  209. package/src/commands/pokemon.ts +146 -0
  210. package/src/commands/sudoku.ts +94 -0
  211. package/src/commands/util.ts +14 -0
  212. package/src/commands/wordsearch.ts +118 -0
  213. package/src/config.ts +166 -0
  214. package/src/index.ts +98 -0
  215. package/src/printer.ts +268 -0
  216. package/src/printing/cups.ts +104 -0
  217. package/src/printing/escp.ts +158 -0
  218. package/src/printing/imagefeed.ts +246 -0
  219. package/src/printing/index.ts +63 -0
  220. package/src/reactions.ts +6 -0
  221. package/src/server.ts +355 -0
  222. package/src/tests/config.test.ts +88 -0
  223. package/src/tests/urls.test.ts +64 -0
  224. package/src/types.ts +120 -0
  225. package/src/web/app.tsx +690 -0
  226. package/tsconfig.json +25 -0
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/ai.js ADDED
@@ -0,0 +1,193 @@
1
+ import { createHash } from 'crypto';
2
+ import { mkdir, writeFile, readFile, access } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import OpenAI from 'openai';
5
+ import { zodResponseFormat } from 'openai/helpers/zod.js';
6
+ import { z } from 'zod/v4';
7
+ import { getCurrentConfig, updateConfig } from './config.js';
8
+ import { logEvent } from './server.js';
9
+ const MODEL = 'gpt-5.4-nano';
10
+ const IMAGE_MODEL = 'gpt-image-1-mini';
11
+ const TOKEN_LIMIT_24H = 200_000;
12
+ export function trackTokenUsage(tokens) {
13
+ const config = getCurrentConfig();
14
+ const entry = { timestamp: new Date().toISOString(), tokens };
15
+ const usage = [...config.tokenUsage, entry];
16
+ // Keep only entries from the last 24 hours
17
+ const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
18
+ const pruned = usage.filter(e => e.timestamp >= cutoff);
19
+ void updateConfig({ tokenUsage: pruned });
20
+ }
21
+ export function getTokenUsage24h() {
22
+ const config = getCurrentConfig();
23
+ const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
24
+ return config.tokenUsage
25
+ .filter(e => e.timestamp >= cutoff)
26
+ .reduce((sum, e) => sum + e.tokens, 0);
27
+ }
28
+ export async function isOverTokenLimit() {
29
+ const used = getTokenUsage24h();
30
+ if (used >= TOKEN_LIMIT_24H) {
31
+ logEvent('error', `Token limit exceeded (${used} tokens in last 24h). Deleting OpenAI key.`);
32
+ const config = getCurrentConfig();
33
+ const updated = { ...config };
34
+ delete updated.openaiKey;
35
+ await updateConfig(updated);
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ function getClient() {
41
+ const key = getCurrentConfig().openaiKey;
42
+ if (!key)
43
+ return null;
44
+ return new OpenAI({ apiKey: key });
45
+ }
46
+ const ScheduleSchema = z.object({
47
+ 'message without schedule': z.string(),
48
+ 'next occurrence': z.string(),
49
+ });
50
+ function parseOccurrenceDate(str) {
51
+ try {
52
+ const d = new Date(str);
53
+ if (!isNaN(d.getTime()))
54
+ return d;
55
+ }
56
+ catch {
57
+ // ignore
58
+ }
59
+ return null;
60
+ }
61
+ export async function parseRecurringSchedule(userMessage, now) {
62
+ const client = getClient();
63
+ if (!client)
64
+ return null;
65
+ if (await isOverTokenLimit())
66
+ return null;
67
+ const nowStr = formatScheduleDate(now);
68
+ const prompt = `The user has asked for a recurring printout. It is currently ${nowStr}. Tell me the next time that I should do this, and what the non-schedule part of the message was (verbatim). If the user didn't specify a time of day, use 8:00 AM. Here's the user's message: ${userMessage}`;
69
+ const results = [];
70
+ const ATTEMPTS = 5;
71
+ for (let i = 0; i < ATTEMPTS; i++) {
72
+ try {
73
+ const completion = await client.chat.completions.create({
74
+ model: MODEL,
75
+ reasoning_effort: 'low',
76
+ messages: [{ role: 'user', content: prompt }],
77
+ response_format: zodResponseFormat(ScheduleSchema, 'schedule'),
78
+ });
79
+ const content = completion.choices[0]?.message?.content;
80
+ if (content) {
81
+ const parsed = ScheduleSchema.parse(JSON.parse(content));
82
+ results.push(parsed);
83
+ const tokens = completion.usage?.total_tokens ?? 0;
84
+ trackTokenUsage(tokens);
85
+ }
86
+ }
87
+ catch (err) {
88
+ logEvent('error', `Schedule parsing attempt ${i + 1} failed: ${err}`);
89
+ }
90
+ }
91
+ if (results.length < 3)
92
+ return null;
93
+ // Majority voting: need 60% consistency for each field
94
+ const messageCounts = new Map();
95
+ const occurrenceCounts = new Map();
96
+ for (const r of results) {
97
+ messageCounts.set(r['message without schedule'], (messageCounts.get(r['message without schedule']) ?? 0) + 1);
98
+ occurrenceCounts.set(r['next occurrence'], (occurrenceCounts.get(r['next occurrence']) ?? 0) + 1);
99
+ }
100
+ const threshold = ATTEMPTS * 0.6;
101
+ let bestMessage = null;
102
+ for (const [msg, count] of messageCounts) {
103
+ if (count >= threshold) {
104
+ bestMessage = msg;
105
+ break;
106
+ }
107
+ }
108
+ let bestOccurrence = null;
109
+ for (const [occ, count] of occurrenceCounts) {
110
+ if (count >= threshold) {
111
+ bestOccurrence = occ;
112
+ break;
113
+ }
114
+ }
115
+ if (!bestMessage || !bestOccurrence)
116
+ return null;
117
+ const occDate = parseOccurrenceDate(bestOccurrence);
118
+ if (!occDate)
119
+ return null;
120
+ return { message: bestMessage, nextOccurrence: occDate };
121
+ }
122
+ function formatScheduleDate(date) {
123
+ const year = date.getFullYear();
124
+ const month = String(date.getMonth() + 1).padStart(2, '0');
125
+ const day = String(date.getDate()).padStart(2, '0');
126
+ let hours = date.getHours();
127
+ const minutes = String(date.getMinutes()).padStart(2, '0');
128
+ const ampm = hours >= 12 ? 'PM' : 'AM';
129
+ hours = hours % 12 || 12;
130
+ return `${year}-${month}-${day} ${hours}:${minutes} ${ampm}`;
131
+ }
132
+ export { formatScheduleDate };
133
+ export async function getNextOccurrence(originalMessage, now) {
134
+ const result = await parseRecurringSchedule(originalMessage, now);
135
+ return result ? result.nextOccurrence : null;
136
+ }
137
+ export async function generateIcon(text, cacheDir) {
138
+ const prompt = `Black-on-transparent line drawing icon for the TODO item: "${text}". Do not produce any text. Use big, thick lines. No fine detailing.`;
139
+ const hash = createHash('sha1').update(prompt).digest('hex').slice(0, 9);
140
+ const filename = `${hash}.png`;
141
+ const cachePath = join(cacheDir, filename);
142
+ // Check cache first
143
+ try {
144
+ await access(cachePath);
145
+ return cachePath;
146
+ }
147
+ catch {
148
+ // not cached
149
+ }
150
+ const client = getClient();
151
+ if (!client)
152
+ return null;
153
+ if (await isOverTokenLimit())
154
+ return null;
155
+ for (let attempt = 0; attempt < 3; attempt++) {
156
+ try {
157
+ const response = await client.images.generate({
158
+ model: IMAGE_MODEL,
159
+ prompt,
160
+ n: 1,
161
+ size: '1024x1024',
162
+ quality: 'low',
163
+ background: 'transparent',
164
+ output_format: 'png',
165
+ });
166
+ const imageData = response.data?.[0];
167
+ if (!imageData)
168
+ throw new Error('No image data returned');
169
+ let imageBuffer;
170
+ if ('b64_json' in imageData && imageData.b64_json) {
171
+ imageBuffer = Buffer.from(imageData.b64_json, 'base64');
172
+ }
173
+ else if ('url' in imageData && imageData.url) {
174
+ const res = await fetch(imageData.url);
175
+ imageBuffer = Buffer.from(await res.arrayBuffer());
176
+ }
177
+ else {
178
+ throw new Error('No image URL or base64 data');
179
+ }
180
+ await mkdir(cacheDir, { recursive: true });
181
+ await writeFile(cachePath, imageBuffer);
182
+ return cachePath;
183
+ }
184
+ catch (err) {
185
+ logEvent('error', `Icon generation attempt ${attempt + 1} failed: ${err}`);
186
+ if (attempt === 2)
187
+ return null;
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+ // suppress unused import warning
193
+ void readFile;
package/dist/bot.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export declare class HouseholdBot {
2
+ private readonly client;
3
+ private readonly getConfiguredServerId;
4
+ constructor(getConfiguredServerId: () => string | null);
5
+ start(token: string): Promise<void>;
6
+ private onReady;
7
+ /**
8
+ * On startup, fetch the last 100 messages in every channel that has at
9
+ * least one applicable command and process any that the bot hasn't already
10
+ * reacted to.
11
+ */
12
+ private scanForUnhandledMessages;
13
+ private getTargetGuilds;
14
+ private onMessage;
15
+ /**
16
+ * Parses and executes the command in `message` if applicable.
17
+ * Returns true if an action was executed, false if skipped (not a command,
18
+ * wrong channel, or already handled).
19
+ */
20
+ private processMessage;
21
+ }
22
+ //# sourceMappingURL=bot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../src/bot.ts"],"names":[],"mappings":"AAeA,qBAAa,YAAY;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAsB;IAE5D,YAAY,qBAAqB,EAAE,MAAM,MAAM,GAAG,IAAI,EAarD;IAEK,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExC;YAEa,OAAO;IAgBrB;;;;OAIG;YACW,wBAAwB;IA+BtC,OAAO,CAAC,eAAe;YAiBT,SAAS;IAIvB;;;;OAIG;YACW,cAAc;CAgD/B"}