@rackbops/plugin-tracker 0.23.0 → 0.25.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 +120 -37
- package/dist/plugin.js +4991 -4313
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -20,8 +20,8 @@ things that fit someone's interests through a gift lens, DMs what it found, and
|
|
|
20
20
|
same link twice -- and the want-list watcher (#83, plan E9, category 2): a watch on one wanted thing
|
|
21
21
|
that reads a listing page you paste, or BoardGameGeek's marketplace through BGG's API once its
|
|
22
22
|
token is set, every few hours with no model, or, for eBay (and BGG without that token), the
|
|
23
|
-
listings you send in through the task API every hour -- the
|
|
24
|
-
|
|
23
|
+
listings you send in through the task API every hour -- the watcher never opens eBay, nor any site
|
|
24
|
+
for these; your own browser finds them, such as Claude in Chrome running in it -- and DMs each new
|
|
25
25
|
listing within your limits once, until you press Done, and, while the model runner is set up, has
|
|
26
26
|
the model look at each new listing first -- is it the thing, and what does it show about the
|
|
27
27
|
seller -- before DMing it -- and the daily "today and
|
|
@@ -41,15 +41,15 @@ as in a server (plan 5.5, item 39).
|
|
|
41
41
|
| `/register [hour] [zone]` | an admitted person | Signs up, or changes the preferred hour (0-23, default 9) and time zone (default `America/New_York`). The reply says an admin can see every task. With the usr link on (0.21.0), run in a server, it also carries a linked person's one-time usr sign-up link, until usr says they have signed up; in a DM it says to run it in the server. If usr says they are not allowed yet (a usr admin removed their link), it forgets their link and says to ask for `/allow` again. See **First admin and usr** below. |
|
|
42
42
|
| `/remind text [when] [repeat]` | a registered person | A reminder by DM. `when` is docket's `parseWhen` grammar (`in 20 minutes`, `tomorrow 9am`, `fri at 17:30`); `repeat` is once (the default), daily, weekly or monthly. A repeating one with no `when` starts today at the preferred hour. |
|
|
43
43
|
| `/renewal name amount currency renews [unit] [every] [lead] [note]` | registered | A subscription, domain, warranty or membership (docket's `renewal`, a `period` schedule). `renews` is the next renewal or expiry date, `YYYY-MM-DD`, today or later; `unit` yearly (the default), monthly, weekly or daily, `every` how many of those; the ask comes `lead` days before (default 7) at the preferred hour, with Keep, Cancel, Renewed and Snooze. If that ask is already past but the date is not, the first ask comes within a minute (a zone or hour change before it fires drops it for the next period's). Keep and Renewed record what was paid; Cancel ends it. |
|
|
44
|
-
| `/price url [name] [hours] [drop] [baseline] [near]` | registered | A price (docket's `price`, a `poll` schedule every `hours`, default 12, at most 168). The page is read once at once, and nothing is created unless a price is found in it; then the first check, within a minute, DMs the starting price, and a drop of `drop` percent or more (default 10) from the `baseline` -- the last (default), first or highest price seen -- DMs the owner once per crossing. `near` is the words just before the price, for a page with no structured price. At most 20 per person. |
|
|
44
|
+
| `/price url [name] [hours] [drop] [baseline] [near]` | registered | A price (docket's `price`, a `poll` schedule every `hours`, default 12, at most 168). `url` may be any public web page (Page reads, under What it does), eBay's and BGG's included, or a link that redirects to one; only the want-list watcher refuses eBay. The page is read once at once, and nothing is created unless a price is found in it; then the first check, within a minute, DMs the starting price, and a drop of `drop` percent or more (default 10) from the `baseline` -- the last (default), first or highest price seen -- DMs the owner once per crossing. `near` is the words just before the price, for a page with no structured price. At most 20 per person. |
|
|
45
45
|
| `/research question [context] [deadline] [at]` | registered | A one-off research request (docket's `research`, a `once` schedule at `at`, default now). A research run looks it up on the web (read-and-web tools only, plan 5.6), a reviewer run checks the draft against its sources, and only a passed answer is DMed to you (and accepted recipients); its claims are kept as findings on the task's page. `deadline` (docket's `parseWhen`) goes into the request; a run that would start past it makes no call and tells you. `question` must hold the question itself: fewer than 3 characters, or no letter at all (a stray `0`), is refused before anything is made. At most 5 waiting per person; every run counts against the daily budget (below). Answers that research is not available while the city-hall Executor is not set up. |
|
|
46
46
|
| `/scout new interests [lens] [for] [notes] [every]` | registered | An interest scout (docket-types' `scout` type, since 0.6.0; plan 1.2 row 1): every `every` days (default 1, at most 30) at your preferred hour, a model run looks on the web for 5 to 10 things published or available in the last 30 days that fit the `interests` (separated by commas, at most 20, each at most 80 characters) through the `lens` -- `general` (the default), `birthday` (fun or a little grandiose), `anniversary` (a romantic angle) or `christmas` (tied to their interests) -- for `for` (someone else, e.g. Anne; empty means you), weighing `notes`, and DMs them to you (and accepted recipients). A link it showed before is not shown again (it remembers the newest 300); fewer than five come with the run's reason. At most 3 scouts per person; each run counts against the daily budget (below), up to 1.50 USD. Answers that the scout is not available while the city-hall Executor is not set up. |
|
|
47
|
-
| `/want name source [target] [max] [currency] [hours] [judge]` | registered | A want-list watch (docket-types' `wantlist` type, or `wantjudge` when judged, since 0.6.0; plan 1.2 row 2). `source` is `page`: `target` is a shop's listing or search page, checked like `/price`'s page and read once at once -- nothing is made unless its structured product data (JSON-LD `ItemList`s and `Product`s) lists something; `bgg`: `target` is a BoardGameGeek game's address or id, read through BGG's XML API while `TRACKER_BGG_TOKEN` is set; or `ebay`: an **inbox watch** (since 0.19.0; docket-types' `inbox` source), whose listings are sent in rather than read -- the
|
|
47
|
+
| `/want name source [target] [max] [currency] [hours] [judge]` | registered | A want-list watch (docket-types' `wantlist` type, or `wantjudge` when judged, since 0.6.0; plan 1.2 row 2). `source` is `page`: `target` is a shop's listing or search page, checked like `/price`'s page and read once at once -- nothing is made unless its structured product data (JSON-LD `ItemList`s and `Product`s) lists something; `bgg`: `target` is a BoardGameGeek game's address or id, read through BGG's XML API while `TRACKER_BGG_TOKEN` is set; or `ebay`: an **inbox watch** (since 0.19.0; docket-types' `inbox` source), whose listings are sent in rather than read -- the watcher never opens eBay, page or API. `target` is the words to search eBay for (the name when empty); the answer gives the watch's id, how listings reach it -- you search eBay in your own browser, such as with Claude in Chrome, and send what it finds to `POST /api/v1/tasks/<id>/listings` (Task API below), or, while the alert-email route is on (0.25.0, Alert emails below), forward eBay's saved-search alert emails to your alert address, which the answer gives -- and the eBay search itself, with `max` as eBay's own top price, to save on eBay too. `bgg` while the token is unset (BGG declined the application) is an inbox watch the same way: `target` is then a BGG game (kept as its id) or the words to look for (the name when empty). An inbox watch needs the web area (`TRACKER_WEB_URL`), since its listings arrive through the task API; without it, it is refused. Every `hours` (default 12 for a page, 24 for BGG, 1 for an inbox watch, whose reads cost nothing, at most 168) on the `poll` tick, the watch DMs each listing at or under `max` (then only listings that show a price) and in `currency` (when set) that it has not DMed before, up to 5 lines with the rest in `/task history`'s second message (up to 20, as many as fit in one message, the newest kept; the task's page in the web area shows the newest 50), with a Done button that ends the watch. Each listing DMed is a finding. Three reads in a row that find nothing tell you once (a page whose list is empty is a search with no results, not nothing read). At most 20 new listings a run, the rest the next run. A redirect to eBay is refused on every hop. At most 20 watches per person, judged or not. eBay's pages are refused as a `page`. **Judged** (`judge`, default yes while the model runner is set up; `judge: true` without it is refused; fixed once made): the reads stay plain code, and new listings within the limits go to one `claude -p` Job first (docket-types' `wantjudge`, plan 5.4, item 63), which may open only those listings' pages on the watch's own site (never eBay, no web search) and says for each `match`, `maybe` or `no`, why, and what its page shows about the seller, as signals; the DM shows the `match` and `maybe` ones with those notes, and every listing is a finding with its verdict. 12 turns, 0.50 USD, 5 minutes a look; a look that fails for good sends the listings unchecked. Its reads are uncharged and its looks charged, like any model run; both wait while its owner is at a daily ceiling (with the budgets on). The judge may open only pages on the watch's own host (the pasted page's, or BGG's), and none at all for an inbox watch: it judges from what each listing says. |
|
|
48
48
|
| `/scout edit task [interests] [lens] [for] [notes] [every]` | registered | Changes your scout: `interests` replaces the whole list; anything left out stays; `for` or `notes` set to `-` clears it. A change of `every` keeps the start day. |
|
|
49
|
-
| `/tasks` | registered | Your active tasks and the ones you receive, each with its next run; then your paused ones. |
|
|
49
|
+
| `/tasks` | registered | Your active tasks and the ones you receive, each with its next run; then your paused ones, each naming up to three people it could not DM (then how many more). A list longer than one message (Discord's 2000 characters) is cut between whole lines and keeps your paused tasks in preference to the active ones that do not fit, since their ids are what `/task resume` needs (any paused ones that still do not fit are counted); it says how many active ones it left out and, when the web area is set up, points to `/web`, which lists them all. |
|
|
50
50
|
| `/task done task` / `/task snooze task [until]` | the task's owner | Answers the task's latest reminder (snooze: an hour, or until `until`). |
|
|
51
51
|
| `/task decide task choice [amount]` | the renewal's owner | Answers the renewal's latest ask -- keep, cancel or renewed -- with the amount actually paid when it changed (a button cannot carry one). Use it instead of the button, not after it: a run is answered once. |
|
|
52
|
-
| `/task history task` | the owner, an accepted recipient, or an admin | Every run -- due, status, how it was answered, text replies -- and the task's changes; for a renewal, what each period cost and the total; for a price, the last check and the readings with the low and the high. |
|
|
52
|
+
| `/task history task` | the owner, an accepted recipient, or an admin | Every run -- due, status, how it was answered, text replies -- and the task's changes; for a renewal, what each period cost and the total; for a price, the last check and the readings with the low and the high; for a want-list watch, how many listings it has kept, and once there are any, a second message with the newest of them, up to 20 and as many as fit in one message, then how many more there are (and, while the web area is set up, that the task's page there shows the newest 50). |
|
|
53
53
|
| `/task share task user` | the task's owner | Invites an admitted, registered member who can be DMed: they get one consent DM with accept and decline. |
|
|
54
54
|
| `/task resume task` | the task's owner | Resumes a paused task: one paused because a recipient could not be DMed goes on without that recipient; one its owner paused on the web simply goes on. |
|
|
55
55
|
| `/settings hour hour` | registered | The preferred hour; recurring reminders with no time of their own move to it. |
|
|
@@ -64,16 +64,35 @@ so anyone can always stop the messages; the Reply button checks membership when
|
|
|
64
64
|
|
|
65
65
|
**Buttons.** The owner's DM carries Done, Snooze 1h and Reply; a recipient's copy carries the
|
|
66
66
|
opt-out and Reply; the consent DM carries Accept and Decline. Reply opens a modal whose text is
|
|
67
|
-
kept on the task
|
|
68
|
-
|
|
67
|
+
kept on the task as one plain line: each run of line breaks, spaces, control characters and
|
|
68
|
+
invisible format characters becomes one space, but for a few format characters real text needs
|
|
69
|
+
(the zero-width joiner in an emoji, the non-joiner in a Persian or Indic word, the soft hyphen,
|
|
70
|
+
and the signs Arabic, Syriac and Kaithi write with a number). A reply of only spaces, line breaks,
|
|
71
|
+
zero-width characters or blank fillers is refused as empty. `/task history`, the web task page and
|
|
72
|
+
the task API show up to 80 characters of it, on one line; in `/task history` a masked link,
|
|
73
|
+
mention, timestamp, spoiler or code in it is shown as typed, except that a mention or timestamp
|
|
74
|
+
typed right after a bare link may still show as one. A mention there never pings. docket decides
|
|
75
|
+
who may answer what (the owner alone answers a run, item 34). A pressed DM is edited to say what
|
|
76
|
+
happened.
|
|
69
77
|
|
|
70
78
|
**Pause after failed DMs** (plan 5.5). Three DMs in a row that the host says cannot be delivered
|
|
71
79
|
pause the person's delivery and every active task that would DM them, theirs and the ones they
|
|
72
|
-
receive (a `paused` event in each task's history
|
|
80
|
+
receive (a `paused` event in each task's history; one for a recipient is shown only to that
|
|
81
|
+
recipient, the task's owner and admins -- a row from before 0.6.0, only to the owner and admins); a DM to them still owed waits for the resume.
|
|
82
|
+
A task that comes to DM them while they stay paused -- one made on the web or through the task API,
|
|
83
|
+
one its owner had paused and resumes by hand, one a pause stopped part way never reached -- pauses
|
|
84
|
+
the same way at its first DM, which then waits too. Only a send for a task already finished (a
|
|
85
|
+
research answer, say) is not held: docket's eight deferrals of it run out (about two hours), and it
|
|
86
|
+
fails; a research answer's claims are still on its task page (Findings, below).
|
|
73
87
|
The owner of a task paused for a recipient is DMed once and sees why in `/tasks`; the task
|
|
74
88
|
resumes when the recipient next uses the tracker, or `/task resume` goes on without them. A person
|
|
75
|
-
paused for their own DMs is
|
|
76
|
-
|
|
89
|
+
paused for their own DMs is resumed, with every task nothing else holds, by the next command or
|
|
90
|
+
button they use (all but Decline and opt-out), which tells them why (a command tells them above its
|
|
91
|
+
answer, or in a message of its own after it when the two would not fit in one); by Resume DMs, on
|
|
92
|
+
the banner every web page shows them while they are paused; by resuming a paused task of their own
|
|
93
|
+
(`/task resume`, or Resume on the web or through the task API); or by an admin's Resume delivery.
|
|
94
|
+
Nothing else on the web or through the task API resumes them.
|
|
95
|
+
A DM that goes through clears the count. Only a DM the host says cannot reach that person (Discord's
|
|
77
96
|
50007, or 10013 for an account that is gone) counts, once per run: docket fails it for good at once. A DM refused for
|
|
78
97
|
its content is retried by docket (three tries, a minute apart and doubling) and does not count; nor
|
|
79
98
|
does one whose outcome is unknown (see Delivery below).
|
|
@@ -100,20 +119,26 @@ done: one Job at a time across everyone (plan 5.3), on a tick of its own that ru
|
|
|
100
119
|
background, so a slow city-hall never holds up a reminder. While that tick waits on city-hall it
|
|
101
120
|
holds its tasks: the notify tick sends their DMs a minute later, and a command, button or web
|
|
102
121
|
action on one of them answers "That task is with the model runner right now; try again in a
|
|
103
|
-
minute." at once, so no one else's command waits behind it.
|
|
122
|
+
minute." at once, so no one else's command waits behind it. The one exception is a zone or hour
|
|
123
|
+
change by the owner of a scout the tick holds: it waits for the tick, and so does every command,
|
|
124
|
+
button and web write queued behind it (see **One task at a time** below). city-hall unreachable or
|
|
125
|
+
timed out (logged once an hour, naming only the error's kind, never its message), a 5xx, or a
|
|
104
126
|
refused credential or edge redirect (logged once an hour as such, and the admins told once per Job)
|
|
105
127
|
holds the run and asks again; a Job still not back after six hours is given up (docket's
|
|
106
128
|
`PENDING_LIMIT_MS`). A record in `executor_jobs` goes once its run is gone, or once it was
|
|
107
129
|
submitted over 30 days ago and its run is no longer queued or running.
|
|
108
130
|
|
|
109
131
|
**A runner that stops: city-hall requeues, the tracker waits.** city-hall puts a job back in its
|
|
110
|
-
queue when the runner's claim ends in `usage_limit` or `auth_failed`, or its lease expires
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
queue when the runner's claim ends in `usage_limit` or `auth_failed`, or its lease expires, so
|
|
133
|
+
docket does not see that result then and the job reads `queued` again. Both are capped: once
|
|
134
|
+
city-hall has requeued a job after `usage_limit` or `auth_failed` its configured maximum number of
|
|
135
|
+
times (ten by default, `CITY_HALL_MAX_REQUEUES`), the next claim that ends that way fails the job
|
|
136
|
+
instead, as does the expiry that brings its expired leases to their configured maximum (three by
|
|
137
|
+
default, `CITY_HALL_MAX_EXPIRED_LEASES`). The tracker reads how the last claim ended -- city-hall's
|
|
138
|
+
`job.lastOutcome`, or the last entry of `runs` where the field is absent -- and treats a queued job
|
|
139
|
+
whose last claim ended in any of those ways (`usage_limit`, `auth_failed` or an expired lease) as
|
|
140
|
+
"unavailable", not "pending": the run is held and asked again, never given up after six hours while
|
|
141
|
+
the runner is paused, and nothing new is submitted meanwhile. docket counts its
|
|
117
142
|
six hours from the submission, so once a Job has been seen paused (`executor_jobs.paused_at`) every
|
|
118
143
|
unfinished answer for it -- queued or running -- stays "unavailable": a runner that resumes after
|
|
119
144
|
six hours is collected, not given up and run a second time. That hold is capped at 48 hours from
|
|
@@ -121,9 +146,12 @@ the first sight (a judgement, not a measured figure): past it the Job answers "p
|
|
|
121
146
|
docket gives the run up, and the admins are told once. It logs "the model runner is paused" naming
|
|
122
147
|
the outcome once an hour per Job, and tells the admins once per Job and outcome. docket's own
|
|
123
148
|
usage-limit pause (which waits for the reset the CLI named) applies only to a `usage_limit` result
|
|
124
|
-
that reaches
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
that reaches docket, and that happens only when the job ends failed with that result still stored.
|
|
150
|
+
Mainly, that is the requeue cap (`CITY_HALL_MAX_REQUEUES`): the job fails with the `usage_limit`
|
|
151
|
+
(or `auth_failed`) result of the claim that went past it, and the tracker answers docket with that
|
|
152
|
+
result, if docket is still asking (within the 48-hour hold above). The other way is the lease cap
|
|
153
|
+
(`CITY_HALL_MAX_EXPIRED_LEASES`), when a job requeued after `usage_limit` or `auth_failed` later
|
|
154
|
+
fails at its maximum of expired leases with that earlier claim's result still stored.
|
|
127
155
|
The plugin never calls a model, holds no Claude credential and no `ANTHROPIC_*` variable; model
|
|
128
156
|
output is data, cleaned by docket before it reaches a DM or a finding, and every research DM goes
|
|
129
157
|
out with no allowed mentions: the host sends every `dm` with `allowedMentions: { parse: [] }`
|
|
@@ -183,7 +211,12 @@ edits (the editor, pause, resume, delete, a zone or hour change) take that task'
|
|
|
183
211
|
(`src/locks.ts`), so none of them overlap. The notify and poll ticks run one pass per task that has
|
|
184
212
|
work; an answer that lands while its own task runs waits for that pass. The execute tick is the
|
|
185
213
|
exception: it can wait on city-hall, so an answer or edit about a task it holds does not wait but
|
|
186
|
-
answers "That task is with the model runner right now; try again in a minute."
|
|
214
|
+
answers "That task is with the model runner right now; try again in a minute." A zone or hour
|
|
215
|
+
change (`/register`, `/settings hour`, the web area's settings page) is the one edit that waits
|
|
216
|
+
instead, since it re-times every recurring task that follows the zone or hour and may skip none.
|
|
217
|
+
Of the tasks the execute tick holds, only a scout is one of those: a zone or hour change by its
|
|
218
|
+
owner waits for the rest of that tick's pass, each city-hall call or page read in it bounded at 15
|
|
219
|
+
seconds, and every command, button and web write queued behind it waits too.
|
|
187
220
|
|
|
188
221
|
## Web area
|
|
189
222
|
|
|
@@ -200,6 +233,7 @@ served at a hashed path, cached for a year).
|
|
|
200
233
|
| `/new/reminder`, `/new/renewal`, `/new/price`, `/new/research`, `/new/scout` | The editor's new-task forms (GET), and making one (POST). `/new/research` and `/new/scout` are linked from My tasks only while the model runner is set up; without it the page says the type is not available instead of showing a form. A scout's interests go in a text box, separated by commas or one per line; its edit form shows the list one per line, and an emptied "Who it is for" or "Notes" clears it. |
|
|
201
234
|
| `/tasks/<id>/edit` | The owner's edit form (GET) and saving it (POST). |
|
|
202
235
|
| `/tasks/<id>/pause`, `/resume`, `/delete` | POST only. Delete answers a confirmation first; only a second post carrying `confirm=yes` deletes. |
|
|
236
|
+
| `/resume-dms` | POST only: Resume DMs, the button on the banner every page shows a person whose DMs are paused (Pause after failed DMs, above). It resumes them as their next command would, and My tasks then says how many tasks are back on; pressed when they are not paused, it changes nothing. |
|
|
203
237
|
| `/settings` | Preferred hour and time zone, checked as `/register` checks them. Links to API tokens and Forget me. |
|
|
204
238
|
| `/tokens` | Your API tokens (below): each one's name, when it was made, last used and expires, with Revoke; and the form to make one (POST), which sends the new token to you by Discord DM -- never on the page. |
|
|
205
239
|
| `/tokens/<id>/revoke` | POST only: revokes one of your own tokens. Anyone else's answers the same 404 as an unknown id. |
|
|
@@ -440,6 +474,50 @@ are among the rows, so they are signed out everywhere at once, and the browser's
|
|
|
440
474
|
The `__Host-` prefix, which would stop that, requires `Path=/` and so cannot be used with
|
|
441
475
|
`Path=/tracker/`. Put the bot on a host whose sibling subdomains are all yours.
|
|
442
476
|
|
|
477
|
+
## Alert emails
|
|
478
|
+
|
|
479
|
+
eBay's saved-search alerts reach eBay watches by mail (0.25.0), while `TRACKER_MAIL_DOMAIN` and
|
|
480
|
+
`TRACKER_MAIL_KEY` are set. The tracker still never opens eBay: it reads only the alert eBay mailed.
|
|
481
|
+
|
|
482
|
+
- **Each person has one alert address**, `m-` and 24 random hex digits at the domain, made the first
|
|
483
|
+
time it is shown: in `/want source: ebay`'s answer and on the web area's settings page. It is
|
|
484
|
+
theirs alone (whoever knows it can send listings to their eBay watches), and forget-me deletes it.
|
|
485
|
+
- **The route.** Cloudflare Email Routing receives the domain's mail and hands every message to the
|
|
486
|
+
Email Worker in `mail-worker/worker.js` (a catch-all rule, "Send to a Worker"). The Worker posts it,
|
|
487
|
+
as it came, to `POST /tracker/mail/inbound` with `Authorization: Bearer <TRACKER_MAIL_KEY>`,
|
|
488
|
+
`X-Envelope-To: <the address>` and, while the web area is behind Cloudflare Access, a service
|
|
489
|
+
token's `CF-Access-Client-Id` / `-Secret`; the Access application covering `/tracker/*` then
|
|
490
|
+
needs a Service Auth policy that includes that token, or Access answers with a redirect and every
|
|
491
|
+
message bounces as `status 302`. Up to 1 MiB; bigger is `413` and refused. An address that is no
|
|
492
|
+
one's is `404`. The Worker refuses back to the sender, with a permanent error, anything the
|
|
493
|
+
tracker does not answer `200` (a `503` while it starts too), so a mistyped forwarding address, or
|
|
494
|
+
an outage, bounces to the mailbox that forwarded it; that day's alert is not retried. The Worker
|
|
495
|
+
is deployed by pasting `mail-worker/worker.js` into the Cloudflare dashboard's Worker editor; it is
|
|
496
|
+
not in the plugin's npm bundle.
|
|
497
|
+
- **What a message does.** Gmail's forwarding confirmation (from `forwarding-noreply@google.com`):
|
|
498
|
+
its code is DMed, to enter in Gmail's Forwarding and POP/IMAP settings. An eBay alert: the
|
|
499
|
+
matches above "You might also like" (never eBay's suggestions) -- title, the item's bare
|
|
500
|
+
`/itm/<number>` address, the price's lower end -- go into the inbox of each of the person's live
|
|
501
|
+
eBay watches whose words are the saved search's (case and spacing aside), exactly as listings sent
|
|
502
|
+
through the task API; the watch's next look DMs the new ones. Words match with case, spacing and
|
|
503
|
+
the marks `` ` * _ ~ | < > @ # [ ] ( ) \ : / `` ignored. An alert no watch searches for gets a
|
|
504
|
+
DM saying so, with the `/want` that would watch it, each time eBay sends it. Anything else is
|
|
505
|
+
dropped. A message is never stored, only the listings read from it, and nothing from it is
|
|
506
|
+
logged.
|
|
507
|
+
- **Only eBay's own domains** (ebay.com, .co.uk, .de, .ca, .com.au and the rest of eBay's sites;
|
|
508
|
+
never a lookalike such as `ebay.com.de`) count as eBay, for the sender and for every item link.
|
|
509
|
+
The sender is its `From` address, not a checked signature: the private address is the guard, and
|
|
510
|
+
anyone who learns it can send listings to its owner's eBay watches. A price's own sign gives its
|
|
511
|
+
currency (`$` on ebay.com, `C $`, `AU $`, `£`, `€`); otherwise none is set. The HTML read is
|
|
512
|
+
capped at 512 KiB, in bounded steps.
|
|
513
|
+
- **Setting it up in Gmail.** Settings, Forwarding and POP/IMAP, Add a forwarding address: the alert
|
|
514
|
+
address. Gmail mails a confirmation code there, and the bot DMs it; enter it. Then add a filter --
|
|
515
|
+
from `ebay@ebay.com`, has the words `"Turn off alerts for"` -- that forwards to the alert address.
|
|
516
|
+
Never turn on "Forward a copy of incoming mail" for everything.
|
|
517
|
+
- **The format** was read from the first real alert (2026-10-13, one match), kept with its
|
|
518
|
+
tracking parameters and account names taken out as `src/fixtures/ebay-saved-search-alert.eml`.
|
|
519
|
+
An alert with several matches is read the same way, which is inferred until one arrives.
|
|
520
|
+
|
|
443
521
|
## Task API
|
|
444
522
|
|
|
445
523
|
A JSON API over your own tasks, for a program that acts as you -- a script, or the plain-language
|
|
@@ -495,15 +573,17 @@ web editor (`409 busy`).
|
|
|
495
573
|
its JSON type: text as a string, a whole number or a number as a JSON number. A field left out of a
|
|
496
574
|
create gets the command's default; a field left out of an edit keeps what the task has, and an empty
|
|
497
575
|
string does the same, except that an empty `note` clears a renewal's note and an empty `name` gives
|
|
498
|
-
a price back the page's address -- exactly as the web editor's empty fields do. `null` is
|
|
499
|
-
|
|
576
|
+
a price back the page's address -- exactly as the web editor's empty fields do. `null` is a value
|
|
577
|
+
only for a want-list watch's `max` in a `PATCH`, where it clears the top price; anywhere else it is
|
|
578
|
+
refused as the wrong JSON type (send `""` to clear a note).
|
|
500
579
|
|
|
501
580
|
Which types: `GET /tasks`, `GET /tasks/<id>`, pause, resume and `DELETE` work on any task you own,
|
|
502
|
-
whatever its type. `POST /tasks` takes the three editor types (reminder, renewal, price)
|
|
503
|
-
while the model runner is set up on this bot, `research`
|
|
504
|
-
(#83), by `/scout new`'s; while it is not, a `research`
|
|
505
|
-
`
|
|
506
|
-
|
|
581
|
+
whatever its type. `POST /tasks` takes the three editor types (reminder, renewal, price),
|
|
582
|
+
`wantlist`, by `/want`'s rules, and, while the model runner is set up on this bot, `research`
|
|
583
|
+
(#82), by `/research`'s rules, and `scout` (#83), by `/scout new`'s; while it is not, a `research`
|
|
584
|
+
or `scout` create answers `503 unavailable`. `PATCH` takes the editor types, a want-list watch
|
|
585
|
+
(`wantlist` or `wantjudge`) and `scout` (a scout made while the runner was set up stays editable):
|
|
586
|
+
a research request cannot be edited, here or in Discord, and a `PATCH` of one answers
|
|
507
587
|
`409 conflict`.
|
|
508
588
|
|
|
509
589
|
| Method and path | What |
|
|
@@ -511,7 +591,7 @@ editable): a research request cannot be edited, here or in Discord, and a `PATCH
|
|
|
511
591
|
| `GET /api/v1/me` | The token's owner (id, name, zone, preferred hour) and the token (id, name, made, expires). |
|
|
512
592
|
| `GET /api/v1/types` | Each type this bot makes now (research and scout only while the model runner is set up), whether a `PATCH` takes it (`editable`; false for research, whose `edit` is empty), and its create and edit fields: name, JSON type, required (never, for an edit), description, and limits (`maxLength`, `minimum`, `maximum`, `enum`). This describes the tracker's own editor fields -- what these endpoints take -- and is **not** docket-core's `TaskType.intake` (`IntakeSpec`), which describes a type's config. |
|
|
513
593
|
| `GET /api/v1/tasks` | Your tasks that are not deleted -- active, paused and done -- oldest first. Not the ones shared with you. |
|
|
514
|
-
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal`, `price`, `research` or `
|
|
594
|
+
| `POST /api/v1/tasks` | Makes one: `type` is `reminder`, `renewal`, `price`, `research`, `scout` or `wantlist`, and the rest are that type's create fields. `201`, with `Location`. A price's page is read first, and nothing is made unless a price is found in it. A research request's fields are `question` (required), `context`, `deadline` and `at`, all text. A scout's are `interests` (required, text, separated by commas or new lines), `lens`, `for` and `notes` (text) and `every` (a whole number of days). A want-list watch's (`wantlist`) are `name` (required), `target` (required but for an inbox watch), `source` (`page`, `bgg` or `ebay`), `max` (a number), `currency` (text), `hours` (a whole number) and `judge` (`"yes"` or `"no"`; `"yes"` without the model runner is a `503`); a judged one comes back as `type: "wantjudge"`, edited by the same fields but `judge`; a task's JSON shows its `source` and `target`, which no edit changes -- an inbox watch's `source` reads back as `inbox` and its `target` is its inbox key -- and for an inbox watch its `site` (`ebay` or `bgg`), its words to `search` for and a BGG `game` when one was named. |
|
|
515
595
|
| `GET /api/v1/tasks/<id>` | One of your tasks (a deleted one too), with its history: the newest runs and changes, as `/task history` shows them. |
|
|
516
596
|
| `PATCH /api/v1/tasks/<id>` | Edits it: the fields to change. A price's page is not editable. |
|
|
517
597
|
| `POST /api/v1/tasks/<id>/pause`, `/resume` | Body `{}`. As the web's Pause and Resume, and `/task resume`. |
|
|
@@ -578,12 +658,12 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
578
658
|
| Store | `src/store.ts`, `src/schema.ts` | docket's `Store` port on `bun:sqlite`, in `<dataDir>/tracker/tracker.sqlite` (WAL). A Discord id belongs to at most one user. Schema versioned by `PRAGMA user_version`; a shipped migration is never edited. Schema 5 (0.9.0) is docket 0.4.0's: a run's `record`, a series point's `key`, `deliveries` (from `delivery_claims`), `usage` and `notices`. Schema 6 (0.11.0) is docket 0.5.0's, all additive: `findings`, a charge's `usage.key`, and the Executor's `executor_jobs`. Forget-me erases a person's findings and their runs' Job records too. Schema 8 (0.19.0) adds `want_inbox`, the listings sent in for an inbox watch. Schema 9 (0.20.0) adds `users.usr_subject`, a person's usr user id, null until linked, unique when set. `store.test.ts` runs docket's `STORE_CONTRACT` against it. |
|
|
579
659
|
| Notify lane | `src/notify-lane.ts` | docket's `Lanes.tickNotify` on two host ticks, every 60 s, one pass per task with work (a due run, a run in flight, a DM owed or claimed), each under that task's lock through a view of the store limited to that task: `notify` runs every type but the page readers, `poll` runs only them (`price`, `wantlist`) with the Fetch port, so a slow page never holds up a reminder. A task the execute tick holds is left for the next notify tick, never waited on. Registers `reminder`, `renewal`, `price`, `research`, `scout`, `wantlist` and `wantjudge`. |
|
|
580
660
|
| Want-list watcher | `src/want.ts`, `src/inbox.ts`, `src/web/api-listings.ts`; the types and sources are `@rackbops/docket-types`' (0.7.0: `wantlist`, `wantjudge`, `pageSource`, `bggSource`, `inboxSource`) | `/want`'s rules; the `wantlist` type over a `Source` port; the `inbox` source over the `want_inbox` table (schema 8, 0.19.0: the listings sent in per watch, keyed `ebay-` or `bgg-` and 24 random hex digits, the newest 100 kept, as many as one look reads; forget-me erases them with the person's tasks), filled by the task API's listings route; the `page` source (JSON-LD listings through the fenced Fetch port, no HTML guessing) and the `bgg` source (BGG's XML API with the Bearer token, on `boardgamegeek.com`, never `www.`, at least 5 s between requests (a read due sooner is put back for the next tick, never waited on), read by a bounded `indexOf` parser, never a pattern over the whole body; a 401 or 403 is the token refused, a 202, 429 or 5xx a miss). The BGG fixture is hand-written to BGG's documented shape until a real response is captured with the token. The judged watch (`wantjudge`) is an execute-lane type: its `prepare` reads the source in plain code (an uncharged `NoJob` when nothing is new), keeps new listings in the task's state as `pending` and asks for a follow-up, whose `prepare` makes the judge Job; `finish` maps the verdicts back by number, so a Job collected after a restart still has its listings. On this lane a BGG read due too soon waits out the spacing once. |
|
|
581
|
-
| Execute lane | `src/execute-lane.ts`, `src/executor.ts`, `src/research.ts`, `src/scout.ts` (the `scout` type is docket-types') | Only when the city-hall Executor is configured: a third host tick, `execute`, that starts docket's `Lanes.tickExecute` in the background (one at a time, at most once a minute) over every task with a due execute-lane run, reserving those tasks and then holding their locks in id order, through one docket `Lanes` kept while the plugin is active (docket keeps its usage-limit pause on it). The Executor is city-hall#18's source pair (above), its I/O injected; `executor_jobs` keeps each Job key's city-hall id. `/research`'s and `/scout`'s rules, and the scout type. |
|
|
661
|
+
| Execute lane | `src/execute-lane.ts`, `src/executor.ts`, `src/research.ts`, `src/scout.ts` (the `scout` type is docket-types') | Only when the city-hall Executor is configured: a third host tick, `execute`, that starts docket's `Lanes.tickExecute` in the background (one at a time, at most once per host tick, about a minute) over every task with a due execute-lane run, reserving those tasks and then holding their locks in id order, through one docket `Lanes` kept while the plugin is active (docket keeps its usage-limit pause on it). The Executor is city-hall#18's source pair (above), its I/O injected; `executor_jobs` keeps each Job key's city-hall id. `/research`'s and `/scout`'s rules, and the scout type. |
|
|
582
662
|
| Budgets, usage | `src/ceilings.ts`, `src/usage.ts`; `src/web/ceiling-pages.ts`, `src/web/usage-pages.ts` | The execute lane's `BudgetPolicy` (docket's defaults plus each person's raise, or no ceiling at all while `TRACKER_BUDGET_UNLIMITED` is on), the ceiling changes, and the pure usage report `/admin/usage` renders from docket's `usage` table. |
|
|
583
|
-
| Page reads | `src/fetch.ts` | docket's `Fetch` port for `price
|
|
663
|
+
| Page reads | `src/fetch.ts` | docket's `Fetch` port for `price` and the want-list's `page` source: http or https on the default port, no credentials, every resolved address public (no loopback, private, link-local, CGNAT, multicast or reserved range, IPv4 or IPv6), redirects followed by hand and re-checked (at most 5; a redirect to something that is not a web address is refused), 15 s including the name lookup, at most 3 MB kept. The body is then rebuilt in linear time (`src/page.ts`) to just what extraction reads -- at most 200 JSON-LD blocks and 300 meta tags, the first ones in page order (room for a shop search page with one JSON-LD block per product beside its other blocks; the `page` source keeps at most 100 listings a page, docket-types' `MAX_LISTINGS`, and a look takes at most 20 new ones, the rest on the next look), and the page with every `<` blanked, so `near` still reads text, attributes and script data -- because docket's extraction patterns take quadratic time on a page of unclosed tags. Tag names are matched with only `A`-`Z` folded to lower case, so a letter that changes length when lower-cased (the Turkish `İ`) does not shift the tags after it out of reach. A read the tick's abort cuts short requeues its run instead of counting a miss. A DNS answer that changes between the check and the read is not caught here. A want-list page read (docket-types' `page` source) carries `NEVER_EBAY` and is refused on any hop to eBay; a price read is not. |
|
|
584
664
|
| Renewals, prices | `src/tracked.ts`, `src/price.ts`, `src/series.ts`, `src/page.ts` | `/renewal`, `/price` and `/task decide`; the series lines of `/task history`. The series (docket's `series` table, schema 1) holds a renewal's paid amounts and a price's readings. |
|
|
585
665
|
| Delivery | `src/notifier.ts`, `src/buttons.ts` | docket's `Notifier` over `host.dm`, with the buttons. docket claims each (run, person) in the store's `deliveries` before the DM and settles it after. The notifier maps the host's answers to docket's errors: cannot be messaged (50007) or an unknown user is `DeliveryFailedError(msg, true)` (failed for good; 50007 and 10013 count toward the pause); a message refused for its content is a plain `DeliveryFailedError` (retried); a paused person is `ExecutorUnavailableError` (deferred); anything else is rethrown, so docket settles it unconfirmed and never resends it. |
|
|
586
|
-
| Daily digest | `src/digest.ts` | Plan 5.5's "today and overdue" DM, on the `notify` tick after the runs due now, no model. For each person whose preferred hour has come in their zone, the day is claimed first (docket's `claimNotice`, key `digest:<user>:<YYYY-MM-DD>`, research-triage's exactly-once pattern; forget-me erases it), then their own active reminders and renewals are read: overdue is the latest fired run before today with no done or decision from the owner, due today is a run not yet fired today or the latest fired run today still unanswered. A snoozed run, an older run behind a newer one, other types and received tasks are left out. Nothing listed, no DM; a paused person is passed by unclaimed; a failed send is not retried. |
|
|
666
|
+
| Daily digest | `src/digest.ts` | Plan 5.5's "today and overdue" DM, on the `notify` tick after the runs due now, no model. For each person whose preferred hour has come in their zone, the day is claimed first (docket's `claimNotice`, key `digest:<user>:<YYYY-MM-DD>`, research-triage's exactly-once pattern; forget-me erases it), then their own active reminders and renewals are read: overdue is the latest fired run before today with no done or decision from the owner, due today is a run not yet fired today or the latest fired run today still unanswered. A snoozed run, an older run behind a newer one, other types and received tasks are left out. Nothing listed, no DM; too long for one message, it caps both sections at the same, smaller number of lines (the most that fit) and counts the rest; a paused person is passed by unclaimed; a failed send is not retried. |
|
|
587
667
|
| Task lock | `src/locks.ts` | One task at a time: its runs, answers and edits never overlap (docket 0.4.0 asks the host to serialize per task). |
|
|
588
668
|
| Pause | `src/delivery-health.ts` | The per-person failure count, the pause and the resume. |
|
|
589
669
|
| People | `src/people.ts`, `src/admissions.ts` | Discord id, time zone, preferred hour, admin flag -- in the tracker's store, with each person's usr user id once the usr link links them (`src/usr.ts`, 0.20.0); who admitted each person and when they registered. `TRACKER_ADMIN_DISCORD_IDS` only ever grants admin. |
|
|
@@ -610,6 +690,8 @@ $ curl -s https://clerk.example.com/tracker/api/v1/tasks/t9 -H "Authorization: B
|
|
|
610
690
|
| `TRACKER_BGG_TOKEN` | yes | BoardGameGeek's XML API Bearer token for the tracker's registered application. Set = `/want source: bgg` watches a game's BGG marketplace; unset or empty = `/want source: bgg` makes an inbox watch instead, whose listings are sent in (as for eBay). Never logged. To get one: register an application at boardgamegeek.com/applications (roshne applied 2026-09-29, non-commercial; approval takes a week or more), then put the token in the instance's `.env` and recreate the bot. BGG's terms ask for attribution: every BGG line in a DM says "via BoardGameGeek". |
|
|
611
691
|
| `TRACKER_USR_URL` | no | The https origin of our usr (Rackbops/usr), e.g. `https://id.example.com` (no path). With `TRACKER_USR_KEY` it turns the usr link on (0.20.0): people stay in the tracker, and each is linked to a usr account by usr's user id (`users.usr_subject`). Unset = off, and the tracker behaves as before (migration 9 still adds the empty column). `/allow` and `/register` use it since 0.21.0, and the web sign-in since 0.22.0 (Signing in through usr, above). |
|
|
612
692
|
| `TRACKER_USR_KEY` | yes | The tracker's usr API key, with the role `usr:discord` only (usr's `/api/discord/allow` and `/register-link`), never `tracker:admin` or `tracker:register`, and a usr Discord service (`PUT /api/discord/services/<keyId>`) set to the tracker's app. Required with `TRACKER_USR_URL`; set without it, or with whitespace in it, refuses to load. Never logged. |
|
|
693
|
+
| `TRACKER_MAIL_DOMAIN` | no | The domain the alert-email route (0.25.0, Alert emails below) receives mail at, e.g. `alerts.example.com`: lower case, at least one dot. With `TRACKER_MAIL_KEY`; both unset = off (`/tracker/mail/inbound` answers 404 and no address is shown). One without the other, a domain that is not one, or a key under 32 characters or with white space refuses to load, never echoing either. |
|
|
694
|
+
| `TRACKER_MAIL_KEY` | yes | The bearer the Email Worker posts to `/tracker/mail/inbound` with, at least 32 characters; the same value is the Worker's `CLERK_MAIL_KEY` secret. Never logged. |
|
|
613
695
|
| `TRACKER_USR_APP` | no | The usr app the tracker's roles live under (`tracker:member`, `tracker:register`): lowercase words joined by `-`. It must match the app on the key's usr Discord service row, which is what usr goes by. Unset = `tracker`. Needs `TRACKER_USR_URL`. |
|
|
614
696
|
|
|
615
697
|
**The execute lane is off until all three of `TRACKER_CITY_HALL_URL`, `_KEY` and `_CAPABILITY` are
|
|
@@ -649,10 +731,11 @@ From then on they `/allow` everyone else. (usr's own "Setup" step 3, in `docs/di
|
|
|
649
731
|
## Not yet
|
|
650
732
|
|
|
651
733
|
- The rest of the web area (E5): sharing a task from the web, and the admin view's grants and
|
|
652
|
-
retry. The task API makes reminders, renewals, prices and
|
|
653
|
-
|
|
734
|
+
retry. The task API makes reminders, renewals, prices, research requests, scouts and
|
|
735
|
+
want-list watches -- not `/task done`, `snooze`, `decide`, `share` or the settings -- and no intake agent uses it yet
|
|
654
736
|
(E10, deferred). Discord OAuth2 as a second sign-in
|
|
655
737
|
method, if chosen (plan item 41).
|
|
738
|
+
- For the alert emails: checking the sender's DKIM signature, not only its `From` address (the private address is the guard today), and a captured alert with several matches.
|
|
656
739
|
- For the want-list watcher (#83): a captured BGG response to replace the hand-written fixture once
|
|
657
740
|
the token exists, a live check that the CLI holds the judge to its `WebFetch(domain:...)` rules,
|
|
658
741
|
and giving the scout, wantlist and wantjudge types back to `Rackbops/docket`.
|
|
@@ -663,7 +746,7 @@ From then on they `/allow` everyone else. (usr's own "Setup" step 3, in `docs/di
|
|
|
663
746
|
[Lepid-Labs/city-hall#17](https://github.com/Lepid-Labs/city-hall/issues/17); the research lane
|
|
664
747
|
still needs a deployed city-hall with a runner).
|
|
665
748
|
- Editing in Discord (the editor is on the web only; `/scout edit` is the one exception), and changing a price tracker's page or `near` after it is made (make a new one); a free-form pattern for `price` (`near` is the safe subset: a user's regular expression run on a large page could hang the bot).
|
|
666
|
-
-
|
|
749
|
+
- Showing unconfirmed deliveries to admins anywhere but the log, or a run's deliveries in its history.
|
|
667
750
|
|
|
668
751
|
docket-core, docket-types and `@rackbops/styles` are `devDependencies`: `bun build` bundles them
|
|
669
752
|
into `dist/plugin.js` (the theme's CSS as text), and the bot loads that file without installing
|