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.
- package/README.md +128 -0
- package/SPEC.md +179 -0
- package/assets/pokemon-sprites/1.png +0 -0
- package/assets/pokemon-sprites/10.png +0 -0
- package/assets/pokemon-sprites/100.png +0 -0
- package/assets/pokemon-sprites/101.png +0 -0
- package/assets/pokemon-sprites/102.png +0 -0
- package/assets/pokemon-sprites/103.png +0 -0
- package/assets/pokemon-sprites/104.png +0 -0
- package/assets/pokemon-sprites/105.png +0 -0
- package/assets/pokemon-sprites/106.png +0 -0
- package/assets/pokemon-sprites/107.png +0 -0
- package/assets/pokemon-sprites/108.png +0 -0
- package/assets/pokemon-sprites/109.png +0 -0
- package/assets/pokemon-sprites/11.png +0 -0
- package/assets/pokemon-sprites/110.png +0 -0
- package/assets/pokemon-sprites/111.png +0 -0
- package/assets/pokemon-sprites/112.png +0 -0
- package/assets/pokemon-sprites/113.png +0 -0
- package/assets/pokemon-sprites/114.png +0 -0
- package/assets/pokemon-sprites/115.png +0 -0
- package/assets/pokemon-sprites/116.png +0 -0
- package/assets/pokemon-sprites/117.png +0 -0
- package/assets/pokemon-sprites/118.png +0 -0
- package/assets/pokemon-sprites/119.png +0 -0
- package/assets/pokemon-sprites/12.png +0 -0
- package/assets/pokemon-sprites/120.png +0 -0
- package/assets/pokemon-sprites/121.png +0 -0
- package/assets/pokemon-sprites/122.png +0 -0
- package/assets/pokemon-sprites/123.png +0 -0
- package/assets/pokemon-sprites/124.png +0 -0
- package/assets/pokemon-sprites/125.png +0 -0
- package/assets/pokemon-sprites/126.png +0 -0
- package/assets/pokemon-sprites/127.png +0 -0
- package/assets/pokemon-sprites/128.png +0 -0
- package/assets/pokemon-sprites/129.png +0 -0
- package/assets/pokemon-sprites/13.png +0 -0
- package/assets/pokemon-sprites/130.png +0 -0
- package/assets/pokemon-sprites/131.png +0 -0
- package/assets/pokemon-sprites/132.png +0 -0
- package/assets/pokemon-sprites/133.png +0 -0
- package/assets/pokemon-sprites/134.png +0 -0
- package/assets/pokemon-sprites/135.png +0 -0
- package/assets/pokemon-sprites/136.png +0 -0
- package/assets/pokemon-sprites/137.png +0 -0
- package/assets/pokemon-sprites/138.png +0 -0
- package/assets/pokemon-sprites/139.png +0 -0
- package/assets/pokemon-sprites/14.png +0 -0
- package/assets/pokemon-sprites/140.png +0 -0
- package/assets/pokemon-sprites/141.png +0 -0
- package/assets/pokemon-sprites/142.png +0 -0
- package/assets/pokemon-sprites/143.png +0 -0
- package/assets/pokemon-sprites/144.png +0 -0
- package/assets/pokemon-sprites/145.png +0 -0
- package/assets/pokemon-sprites/146.png +0 -0
- package/assets/pokemon-sprites/147.png +0 -0
- package/assets/pokemon-sprites/148.png +0 -0
- package/assets/pokemon-sprites/149.png +0 -0
- package/assets/pokemon-sprites/15.png +0 -0
- package/assets/pokemon-sprites/150.png +0 -0
- package/assets/pokemon-sprites/151.png +0 -0
- package/assets/pokemon-sprites/16.png +0 -0
- package/assets/pokemon-sprites/17.png +0 -0
- package/assets/pokemon-sprites/18.png +0 -0
- package/assets/pokemon-sprites/19.png +0 -0
- package/assets/pokemon-sprites/2.png +0 -0
- package/assets/pokemon-sprites/20.png +0 -0
- package/assets/pokemon-sprites/21.png +0 -0
- package/assets/pokemon-sprites/22.png +0 -0
- package/assets/pokemon-sprites/23.png +0 -0
- package/assets/pokemon-sprites/24.png +0 -0
- package/assets/pokemon-sprites/25.png +0 -0
- package/assets/pokemon-sprites/26.png +0 -0
- package/assets/pokemon-sprites/27.png +0 -0
- package/assets/pokemon-sprites/28.png +0 -0
- package/assets/pokemon-sprites/29.png +0 -0
- package/assets/pokemon-sprites/3.png +0 -0
- package/assets/pokemon-sprites/30.png +0 -0
- package/assets/pokemon-sprites/31.png +0 -0
- package/assets/pokemon-sprites/32.png +0 -0
- package/assets/pokemon-sprites/33.png +0 -0
- package/assets/pokemon-sprites/34.png +0 -0
- package/assets/pokemon-sprites/35.png +0 -0
- package/assets/pokemon-sprites/36.png +0 -0
- package/assets/pokemon-sprites/37.png +0 -0
- package/assets/pokemon-sprites/38.png +0 -0
- package/assets/pokemon-sprites/39.png +0 -0
- package/assets/pokemon-sprites/4.png +0 -0
- package/assets/pokemon-sprites/40.png +0 -0
- package/assets/pokemon-sprites/41.png +0 -0
- package/assets/pokemon-sprites/42.png +0 -0
- package/assets/pokemon-sprites/43.png +0 -0
- package/assets/pokemon-sprites/44.png +0 -0
- package/assets/pokemon-sprites/45.png +0 -0
- package/assets/pokemon-sprites/46.png +0 -0
- package/assets/pokemon-sprites/47.png +0 -0
- package/assets/pokemon-sprites/48.png +0 -0
- package/assets/pokemon-sprites/49.png +0 -0
- package/assets/pokemon-sprites/5.png +0 -0
- package/assets/pokemon-sprites/50.png +0 -0
- package/assets/pokemon-sprites/51.png +0 -0
- package/assets/pokemon-sprites/52.png +0 -0
- package/assets/pokemon-sprites/53.png +0 -0
- package/assets/pokemon-sprites/54.png +0 -0
- package/assets/pokemon-sprites/55.png +0 -0
- package/assets/pokemon-sprites/56.png +0 -0
- package/assets/pokemon-sprites/57.png +0 -0
- package/assets/pokemon-sprites/58.png +0 -0
- package/assets/pokemon-sprites/59.png +0 -0
- package/assets/pokemon-sprites/6.png +0 -0
- package/assets/pokemon-sprites/60.png +0 -0
- package/assets/pokemon-sprites/61.png +0 -0
- package/assets/pokemon-sprites/62.png +0 -0
- package/assets/pokemon-sprites/63.png +0 -0
- package/assets/pokemon-sprites/64.png +0 -0
- package/assets/pokemon-sprites/65.png +0 -0
- package/assets/pokemon-sprites/66.png +0 -0
- package/assets/pokemon-sprites/67.png +0 -0
- package/assets/pokemon-sprites/68.png +0 -0
- package/assets/pokemon-sprites/69.png +0 -0
- package/assets/pokemon-sprites/7.png +0 -0
- package/assets/pokemon-sprites/70.png +0 -0
- package/assets/pokemon-sprites/71.png +0 -0
- package/assets/pokemon-sprites/72.png +0 -0
- package/assets/pokemon-sprites/73.png +0 -0
- package/assets/pokemon-sprites/74.png +0 -0
- package/assets/pokemon-sprites/75.png +0 -0
- package/assets/pokemon-sprites/76.png +0 -0
- package/assets/pokemon-sprites/77.png +0 -0
- package/assets/pokemon-sprites/78.png +0 -0
- package/assets/pokemon-sprites/79.png +0 -0
- package/assets/pokemon-sprites/8.png +0 -0
- package/assets/pokemon-sprites/80.png +0 -0
- package/assets/pokemon-sprites/81.png +0 -0
- package/assets/pokemon-sprites/82.png +0 -0
- package/assets/pokemon-sprites/83.png +0 -0
- package/assets/pokemon-sprites/84.png +0 -0
- package/assets/pokemon-sprites/85.png +0 -0
- package/assets/pokemon-sprites/86.png +0 -0
- package/assets/pokemon-sprites/87.png +0 -0
- package/assets/pokemon-sprites/88.png +0 -0
- package/assets/pokemon-sprites/89.png +0 -0
- package/assets/pokemon-sprites/9.png +0 -0
- package/assets/pokemon-sprites/90.png +0 -0
- package/assets/pokemon-sprites/91.png +0 -0
- package/assets/pokemon-sprites/92.png +0 -0
- package/assets/pokemon-sprites/93.png +0 -0
- package/assets/pokemon-sprites/94.png +0 -0
- package/assets/pokemon-sprites/95.png +0 -0
- package/assets/pokemon-sprites/96.png +0 -0
- package/assets/pokemon-sprites/97.png +0 -0
- package/assets/pokemon-sprites/98.png +0 -0
- package/assets/pokemon-sprites/99.png +0 -0
- package/dist/ai.js +193 -0
- package/dist/bot.d.ts +22 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +547 -0
- package/dist/bot.js.map +1 -0
- package/dist/commands/debug-fail.js +8 -0
- package/dist/commands/hello-world.js +12 -0
- package/dist/commands/hello.d.ts +3 -0
- package/dist/commands/hello.d.ts.map +1 -0
- package/dist/commands/hello.js +8 -0
- package/dist/commands/hello.js.map +1 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +12 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/pokemon.js +132 -0
- package/dist/commands/print.d.ts +9 -0
- package/dist/commands/print.d.ts.map +1 -0
- package/dist/commands/print.js +46 -0
- package/dist/commands/print.js.map +1 -0
- package/dist/commands/sudoku.js +79 -0
- package/dist/commands/util.js +13 -0
- package/dist/commands/wordsearch.js +109 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +149 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/printer.js +245 -0
- package/dist/printing/cups.js +89 -0
- package/dist/printing/escp.js +147 -0
- package/dist/printing/imagefeed.js +212 -0
- package/dist/printing/index.js +54 -0
- package/dist/reactions.js +5 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +312 -0
- package/dist/server.js.map +1 -0
- package/dist/tests/config.test.js +78 -0
- package/dist/tests/urls.test.js +54 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/web/app.js +325 -0
- package/package.json +45 -0
- package/scripts/download-pokemon-sprites.mjs +38 -0
- package/src/ai.ts +214 -0
- package/src/bot.ts +657 -0
- package/src/commands/debug-fail.ts +11 -0
- package/src/commands/hello-world.ts +15 -0
- package/src/commands/index.ts +35 -0
- package/src/commands/pokemon.ts +146 -0
- package/src/commands/sudoku.ts +94 -0
- package/src/commands/util.ts +14 -0
- package/src/commands/wordsearch.ts +118 -0
- package/src/config.ts +166 -0
- package/src/index.ts +98 -0
- package/src/printer.ts +268 -0
- package/src/printing/cups.ts +104 -0
- package/src/printing/escp.ts +158 -0
- package/src/printing/imagefeed.ts +246 -0
- package/src/printing/index.ts +63 -0
- package/src/reactions.ts +6 -0
- package/src/server.ts +355 -0
- package/src/tests/config.test.ts +88 -0
- package/src/tests/urls.test.ts +64 -0
- package/src/types.ts +120 -0
- package/src/web/app.tsx +690 -0
- package/tsconfig.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Windsor: Your Friendly Household Helper Bot
|
|
2
|
+
|
|
3
|
+
You'll need:
|
|
4
|
+
* A Raspberry Pi (or equivalent)
|
|
5
|
+
* Thermal printer connected to its USB port
|
|
6
|
+
|
|
7
|
+
## Initial Setup
|
|
8
|
+
|
|
9
|
+
### Create a Discord Bot
|
|
10
|
+
|
|
11
|
+
<!-- Write instructions on how to create a Discord bot -->
|
|
12
|
+
|
|
13
|
+
### Invite to Server
|
|
14
|
+
|
|
15
|
+
<!-- Write instructions on how to invite the bot to your server -->
|
|
16
|
+
|
|
17
|
+
### Set up Raspberry Pi
|
|
18
|
+
|
|
19
|
+
<!-- Write high-level instructions on how to set up a headless Raspberry Pi -->
|
|
20
|
+
|
|
21
|
+
### Configure Credentials
|
|
22
|
+
|
|
23
|
+
<!-- Write instructions on how to get the right kind of discord API token here -->
|
|
24
|
+
|
|
25
|
+
## Raspbery Pi Setup
|
|
26
|
+
|
|
27
|
+
Windsor runs great on a Raspberry Pi.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Automatic Behaviors
|
|
31
|
+
|
|
32
|
+
Windsor will automatically convert all URLs in messages to QR codes. These always appear after the message text, regardless of where they appeared in the message. If the URL was in the middle of the message, it'll be replaced with `[link]`.
|
|
33
|
+
|
|
34
|
+
## Channel Configuration
|
|
35
|
+
|
|
36
|
+
Windsor is primarily configured by assigning a *channel behaviors* to your Discord server's channels.
|
|
37
|
+
You don't need to assign a behavior for every channel; channels without a behavior won't be monitored.
|
|
38
|
+
|
|
39
|
+
The following channel behaviors are supported.
|
|
40
|
+
|
|
41
|
+
### 🖨️ Immediate Print
|
|
42
|
+
|
|
43
|
+
In *immediate print* mode, messages posted to the channel are immediately printed off.
|
|
44
|
+
This is ideal for "to do" items.
|
|
45
|
+
|
|
46
|
+
<!-- TODO (human): photo of a sample -->
|
|
47
|
+
|
|
48
|
+
You can enable these settings in the configuration panel:
|
|
49
|
+
* **Header**: Include this text at the top of every printout.
|
|
50
|
+
* **Icon**: Includes an image based on the message content. Requires OpenAI key.
|
|
51
|
+
* **Metadata Footer**: Include footer lines with timestamp and message initiator.
|
|
52
|
+
* **Footer**: Include this text at the bottom of every printout.
|
|
53
|
+
|
|
54
|
+
### 🛒 Accumulating List
|
|
55
|
+
|
|
56
|
+
In *accumulating list* mode, each printout is a list of messages since the prior printout.
|
|
57
|
+
This is ideal for shopping lists.
|
|
58
|
+
|
|
59
|
+
Trigger a printout of all unprinted items by posting the message "print" in the channel.
|
|
60
|
+
|
|
61
|
+
<!-- TODO (human): photo of a sample -->
|
|
62
|
+
|
|
63
|
+
You can enable these settings in the configuration panel:
|
|
64
|
+
|
|
65
|
+
* **Header**: Include this text at the top of every printout.
|
|
66
|
+
* **Checklist**: Include checkmark boxes next to each line
|
|
67
|
+
* **Metadata Footer**: Include footer lines with timestamp and message initiator
|
|
68
|
+
* **Footer**: Include this text at the bottom of every printout.
|
|
69
|
+
|
|
70
|
+
### 🔄 Recurring Print
|
|
71
|
+
|
|
72
|
+
*Recurring print* lets you set up automatic recurring printouts.
|
|
73
|
+
These work the same way as *immediate print*, but with a repeating schedule.
|
|
74
|
+
This feature requires an OpenAI key.
|
|
75
|
+
|
|
76
|
+
If you have an OpenAI key, the recurrence schedule will be determined automatically based on the text of the message. For example, you can write:
|
|
77
|
+
|
|
78
|
+
> Take out the trash every other Tuesday at 7 PM
|
|
79
|
+
|
|
80
|
+
AI will understand almost any syntax, e.g. you can write messges like any of these:
|
|
81
|
+
|
|
82
|
+
> Every other Tuesday, run the dishwasher clean cycle
|
|
83
|
+
>
|
|
84
|
+
> Change out the batteries on the 3rd of the month at 2 PM
|
|
85
|
+
>
|
|
86
|
+
> Call Bob a week before his birthday (6/12)
|
|
87
|
+
|
|
88
|
+
If time of day isn't specified, the default is 8:00 AM local time.
|
|
89
|
+
|
|
90
|
+
Windsor will reply to you with its understood schedule. If you were misinterpreted, delete your message and rephrase in a new one.
|
|
91
|
+
|
|
92
|
+
### On-Demand
|
|
93
|
+
|
|
94
|
+
In on-demand channels, only specific commands will be processed. Other messages will be ignored. See the list in this document for supported commands.
|
|
95
|
+
|
|
96
|
+
## On-Demand Commands
|
|
97
|
+
|
|
98
|
+
Windsor includes many fun commands for printing one-off items
|
|
99
|
+
|
|
100
|
+
### `/sudoku [kid | easy | medium | hard]`
|
|
101
|
+
|
|
102
|
+
Prints a Sudoku puzzle at the specified difficulty (defaults to `easy`).
|
|
103
|
+
|
|
104
|
+
### `/wordsearch`
|
|
105
|
+
|
|
106
|
+
Prints a themed word search puzzle.
|
|
107
|
+
Themes are chosen randomly and are always kid-appropriate.
|
|
108
|
+
|
|
109
|
+
## Local Development
|
|
110
|
+
|
|
111
|
+
## Control panel and local config
|
|
112
|
+
|
|
113
|
+
Run the bot with:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm run dev
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Then open `http://localhost:8080` to use the web control panel.
|
|
120
|
+
|
|
121
|
+
The control panel is a Preact app served by the diagnostics server and can:
|
|
122
|
+
|
|
123
|
+
1. Show runtime bot status and recent events.
|
|
124
|
+
2. Save basic setup into a local `windsor.config.json` file (Discord token, server id, etc).
|
|
125
|
+
|
|
126
|
+
The browser app source lives in `src/web/app.tsx` and is bundled on-demand by the server using the esbuild API.
|
|
127
|
+
|
|
128
|
+
`windsor.config.json` is gitignored because it can contain secrets.
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Windsor Specification: Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Channel Behavior Mapping
|
|
4
|
+
|
|
5
|
+
The server UI always immediately saves all config changes.
|
|
6
|
+
|
|
7
|
+
In the server config UI webpage, we see a list of channel->behavior mapping rows. Clicking on a row allows the user to see sub-configuration for that behavior (e.g. footer text). They can also delete a row. There's adjacent UI with a list of unmapped channels and list of behaviors; the user can pick one of each and then click "Add" to create the behavior mapping.
|
|
8
|
+
|
|
9
|
+
Internally, we store configuration in terms of both the channel name and ID, and match based on what we see on server startup or refresh: If the ID matches, use that and update our name. If none of the IDs match, see if a channel with the right name exists, update the ID instead. If neither exist, just delete that configuraiton entry. Be careful to not accidently nuke all config if we're encountering a server availabiliy blip - if *all* channels are missing, probably something has gone wrong, don't delete the config.
|
|
10
|
+
|
|
11
|
+
Channels may only have one behavior.
|
|
12
|
+
|
|
13
|
+
## URL Detection and Transformation
|
|
14
|
+
|
|
15
|
+
URLs are detected as any `https://...` text; we don't need to support other protocols. Trailing punctuation is ignored.
|
|
16
|
+
|
|
17
|
+
If multiple URLs are in the same message, write `[link 1]`, `[link 2]`, etc, instead of `[link]`. Don't try to dedupe URLs. There is a maximum of 5 links per printout. If URL parsing fails, assume it wasn't a URL in the first pace. If a URL is too long to generate a printable QR code for, assume it wasn't a URL in the first place.
|
|
18
|
+
|
|
19
|
+
## Idempotency
|
|
20
|
+
|
|
21
|
+
The bot does not require or assume 24/7 uptime. On startup you should scan prior messages and see what needs acting on. You only need to look at the last 100 messages per channel (500 for recurrences - assume we do not have more than that); messages older than this can be assumed to already be actioned.
|
|
22
|
+
|
|
23
|
+
For non-recurring items, use **reactions** to indicate that you've complied (e.g. printed or whatever) to a request. Use the ✅ reaction upon successfully fulfilling the request.
|
|
24
|
+
|
|
25
|
+
If printing fails, react with ⏸️ and post a reply that explains what went wrong.
|
|
26
|
+
|
|
27
|
+
The intended user workflow on fixing a printer is to power-cycle the Raspberry Pi, so you should attempt to re-process these messages on startup.
|
|
28
|
+
|
|
29
|
+
## Print Layout
|
|
30
|
+
|
|
31
|
+
In general we assume an 80mm printer. Long text should be wrapped. Extremely long messages (800 characters (?) after stripping URLs) should be ignored and reacted with ⁉️.
|
|
32
|
+
|
|
33
|
+
For message content, we generally want to print at quite a large font, for readability. This should scale down for longer messages; our ideal is that we are printing something that is modestly square to rectangular (not a tiny strip, not a CVS receipt). Images should span the full width of the paper minus a small margin.
|
|
34
|
+
|
|
35
|
+
General format is:
|
|
36
|
+
|
|
37
|
+
* User-specified header (large font)
|
|
38
|
+
* Primary text (medium-large font, depending)
|
|
39
|
+
* Icon
|
|
40
|
+
* QR codes
|
|
41
|
+
* User-specified footer (medium font)
|
|
42
|
+
* Metadata footer (small font)
|
|
43
|
+
|
|
44
|
+
Timestamps are in local time, in the format `2026-08-17 7:34 PM`
|
|
45
|
+
|
|
46
|
+
## Message Ingestion
|
|
47
|
+
|
|
48
|
+
Ignore user attachments, images, embeds, etc.. Only look at primary message content.
|
|
49
|
+
Ignore replies.
|
|
50
|
+
Ignore messages from your own account.
|
|
51
|
+
|
|
52
|
+
If parsing fails, react with ⁉️ and reply with something sensible related to the parsing failure that occurred.
|
|
53
|
+
|
|
54
|
+
Editing a message does not re-trigger bot activity.
|
|
55
|
+
|
|
56
|
+
## Accumulating List Behavior
|
|
57
|
+
|
|
58
|
+
The message `print` (any casing, all spacing / punctuation ignored) triggers a print.
|
|
59
|
+
Print all items between this message and the prior print message (or 100 messages, whichever is shorter).
|
|
60
|
+
If this would yield 0 items, instead print the "previous" list (this would normally occur if the printer had run out of paper and the user is retrying).
|
|
61
|
+
|
|
62
|
+
Don't try to deduplicate items.
|
|
63
|
+
Ignore deleted messages in the chat.
|
|
64
|
+
Use the latest version of any edited message.
|
|
65
|
+
|
|
66
|
+
## Recurring Print Behavior
|
|
67
|
+
|
|
68
|
+
We need to extract two main things from the user's message:
|
|
69
|
+
* The *message* part
|
|
70
|
+
* The *schedule* part
|
|
71
|
+
|
|
72
|
+
Use structured output using a zod schema to implement this.
|
|
73
|
+
|
|
74
|
+
Send the AI a message in this form:
|
|
75
|
+
|
|
76
|
+
> The user has asked for a recurring printout. It is currently [the current date and time]. 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: [the user message]
|
|
77
|
+
|
|
78
|
+
You'll want to get back a JSON blob like this:
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"message without schedule": "take out the trash",
|
|
82
|
+
"next occurrence": "2025-08-02 07:30 PM"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
Be flexible about date parsing.
|
|
86
|
+
|
|
87
|
+
Use majority-reasoning method of asking 5 times. You must get 60% consistency in the message portion and 60% consistency in the next occurrence portion (not necessarily from the same responses); if this doesn't happen, treat this as a failure to parse and react appropriately.
|
|
88
|
+
|
|
89
|
+
React to the message with the standard green checkmark and reply with:
|
|
90
|
+
|
|
91
|
+
> Got it. I will print ⟪take out the trash⟫ at 2025-08-02 07:30 PM
|
|
92
|
+
|
|
93
|
+
You'll ingest this message on re-starts to establish what scheduled tasks there are.
|
|
94
|
+
Enqueue a background task that checks for scheduled items maturing (check every 30s).
|
|
95
|
+
|
|
96
|
+
When a task matures, we'll of course print it as requested.
|
|
97
|
+
When print finishes, react to the reply with the checkmark emoji.
|
|
98
|
+
|
|
99
|
+
Then we'll return to the AI to ask for what the next occurrence is.
|
|
100
|
+
Note that a task might have finite occurrences, e.g. the user prompt might be "for the next five weeks, remind me at 2 PM to take my medicine on Tuesdays".
|
|
101
|
+
|
|
102
|
+
So we'll post a reply either like
|
|
103
|
+
|
|
104
|
+
> This occurrence has expired and no more prints are scheduled
|
|
105
|
+
|
|
106
|
+
*or*
|
|
107
|
+
|
|
108
|
+
> Printed at 2025-08-02 07:30 PM. The next print will be at 2025-08-09 07:30 PM.
|
|
109
|
+
|
|
110
|
+
You can thus interpret the absence of such a reply (but the presence of the checkmark) as meaning the prior run failed to reach the AI server, and needs a retry.
|
|
111
|
+
|
|
112
|
+
## OpenAI Notes
|
|
113
|
+
|
|
114
|
+
Use `gpt-5.4-nano`, low reasoning, with structured outputs with zod v4 JSON schema for all tasks.
|
|
115
|
+
|
|
116
|
+
Keep a global rolling window of never consuming more than 200,000 tokens during any 24 hour period (something would be EXTREMELY wrong if this happens). If this happens, literally delete the AI key from your config.
|
|
117
|
+
|
|
118
|
+
## Icon Feature
|
|
119
|
+
|
|
120
|
+
For our auto-generated icon, we'll use OpenAI API. Use `gpt-image-1-mini`, 1024x1024, auto quality, transparency on, png format, with a prompt like
|
|
121
|
+
|
|
122
|
+
> Black-on-transparent line drawing icon for the TODO item: "take out the trash". Do not produce any text. Use big, thick lines. No fine detailing.
|
|
123
|
+
|
|
124
|
+
Once you get an image back, store it somewhere sensible on disk with a name like `1837c1a3e.png` where the name is derived from a SHA of the prompt. Check this cache first before calling out to the API.
|
|
125
|
+
|
|
126
|
+
If you're trying to print an item and the AI API is not working for some reason, retry up to 3 times, then just give up and skip the icon, it's never mandatory.
|
|
127
|
+
|
|
128
|
+
## On-Demand Commands and Interaction Model
|
|
129
|
+
|
|
130
|
+
Both `!command` and `/command` are supported.
|
|
131
|
+
Follow our standard idempotency model.
|
|
132
|
+
All users can run commands.
|
|
133
|
+
|
|
134
|
+
## Persistence and Data Model
|
|
135
|
+
|
|
136
|
+
We have three primary data sources:
|
|
137
|
+
* Messages in the channel itself
|
|
138
|
+
* Our own messages and reactions, which indicate work we've already done
|
|
139
|
+
* Local config JSON file
|
|
140
|
+
* In-memory log. This is where we persist logging, etc - we do not need to persist logging between server restarts
|
|
141
|
+
|
|
142
|
+
## Security and Access Control
|
|
143
|
+
|
|
144
|
+
In the server configuration page, users can optionally set a password (hash this and store it in the config file). Once this is set, the config server can only be accessed using that password; use WWW-Authenticate and ignore the username portion.
|
|
145
|
+
|
|
146
|
+
Channel security is up to Discord config; any user posting in a behavior-configured channel is assumed to be authorized to trigger whatever they're doing.
|
|
147
|
+
|
|
148
|
+
## Printer Integration
|
|
149
|
+
|
|
150
|
+
We only support ESC/P protocol, and there must be at most one printer installed.
|
|
151
|
+
The server configuration page should show what printer is connected (if any), offer appropriate printer config options, and have a "print test page button" that prints a small test page demonstrating printer capabilities (fonts, images, alignments, etc).
|
|
152
|
+
|
|
153
|
+
## Diagnostics
|
|
154
|
+
|
|
155
|
+
The server log, visible in the server config page, should show enough events to allow basic diagnostic health, e.g.
|
|
156
|
+
* Successful discord gateway connection
|
|
157
|
+
* Print success/fail
|
|
158
|
+
|
|
159
|
+
Do not include keepalive pings or other routine "non-events".
|
|
160
|
+
|
|
161
|
+
## Testing
|
|
162
|
+
|
|
163
|
+
Write tasteful unit tests at module boundaries. Test for behavorial acceptance, not hardcoded results, and don't test things that TypeScript is already covering for us. Tests must be typechecked. Don't test things that are too much of a pain in the ass.
|
|
164
|
+
|
|
165
|
+
## Revision notes: alpha
|
|
166
|
+
|
|
167
|
+
Apply these revisions to the spec or readme IF APPROPRIATE
|
|
168
|
+
|
|
169
|
+
* It should be more apparent in the server config whether a Discord token has been set yet. Same for OpenAI. Don't actually hide these, just always show them
|
|
170
|
+
* Use "server" in all user-facing config, never "guild"
|
|
171
|
+
* Server ID is required
|
|
172
|
+
* Setting the discord token and server ID should reload the channel list in the server UI
|
|
173
|
+
* Fix this: "(node:49701) DeprecationWarning: The ready event has been renamed to clientReady to distinguish it from the gateway READY event and will only emit under that name in v15. Please use clientReady instead."
|
|
174
|
+
* Add a "refresh channels" button to the server UI, next to the channel list
|
|
175
|
+
* Remove all "back compat" code, you don't need to be backward compatible with anything, this is an alpha project
|
|
176
|
+
* Command invocation should automatically trigger a log event without extra per-code command
|
|
177
|
+
* Don't use `class` in this project, use a function/revealing-module pattern instead
|
|
178
|
+
* Server config UI should have a "Restart Server" button
|
|
179
|
+
* Don't show voice channels or forum channels in the channel listing
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|