@rackbops/plugin-tracker 0.37.0 → 0.38.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 +3 -3
- package/dist/plugin.js +53 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ as in a server (plan 5.5, item 39).
|
|
|
46
46
|
| `/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. |
|
|
47
47
|
| `/research question [context] [deadline] [at] [sources]` | registered | A one-off research request (docket's `research`, a `once` schedule at `at`, default now). A research run splits the question into its parts and looks each up on the web (read-and-web tools only, plan 5.6), naming each part's best source and trying it -- BoardGameGeek included, read honestly, a page that refuses it listed as one it could not open (with the web area; without it, named as a gap) -- and never opening eBay (eBay's User Agreement, section 3; since 0.30.0, docket-types 0.10.0), 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 (a request waiting for pages for a fuller pass, below, not counted: its first answer is delivered); every run counts against the daily budget (below). `sources: browser` (0.28.0, docket-types 0.8.0's `sources: "sent"`; default `web`) answers from pages your own browser gathers instead -- ask Claude in Chrome to gather them and send them in through the task API (`POST /api/v1/tasks/<id>/sources`, then `/sources/ready`; Task API below), for pages a web run cannot or may not read, such as a BoardGameGeek page that refuses it or eBay, which the research never opens: it answers only from the pages sent in, and the request waits for them, its run queued 14 days out or 10 minutes before the deadline, whichever is sooner (a deadline less than 20 minutes away is refused; the margin makes a missed deadline unlikely, not impossible, since a run held by the budget or behind another Job can still start late), and moved to now by `ready`; a request whose `ready` never comes runs then on whatever pages are in, and with none makes no model call and DMs you so. `at` does not go with it, and it needs the web area. **A fuller pass** (0.29.0, docket-types 0.9.0; with the web area only): when the research run lists pages it needed and could not open (a best source that refused it, BoardGameGeek's included), the reviewed answer is DMed with a "Couldn't open:" list, and the request waits for pages for up to 14 days (or until 10 minutes before its deadline), sending, running and charging nothing; ask Claude in Chrome to gather them and send them in as for a browser request, and `ready` starts a second pass that DMs an "Update to ..." of what it adds or corrects. With no `ready` by then, the second pass runs on whatever pages are in, and with none the request ends and the first answer stands. Answers that research is not available while the city-hall Executor is not set up. |
|
|
48
48
|
| `/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. |
|
|
49
|
-
| `/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; `shopify` (since 0.32.0): `target` is the words to search Shopify stores for (the name when empty), one Shopify catalog search per look through Firecrawl's Alexandria while `TRACKER_FIRECRAWL_KEY` is set (refused without it), for new copies in stock at or under `max`; a product counts only when its title has every word, and each look costs 5 Firecrawl credits, so `hours` is 24 to 168 for it; or `ebay`: with the eBay keys (`TRACKER_EBAY_CLIENT_ID` and `TRACKER_EBAY_CLIENT_SECRET`, since 0.33.0), one search per look through eBay's own Browse API (docket-types' `ebaySource`; eBay US, the newest fifty, narrowed by `max`), for listings whose title has every word of `target` (the words, the name when empty), new and used, an auction at its current bid (or its Buy It Now price, when it has one), the seller's name, feedback percentage and score shown; every hour by default, and the watcher still never opens an eBay page. Without the keys, `ebay` is an **inbox watch** (since 0.19.0; docket-types' `inbox` source), whose listings are sent in rather than read -- the watcher never opens eBay. A watch made before the keys were set stays an inbox watch; one made with them that outlives the keys fails each look and tells its owner once. An eBay API watch takes no alert emails: an alert for its words is left without a DM. It keeps no eBay data, so that the keyset may opt out of eBay's account-deletion notices: no finding is stored, its listings are in the DM only, six hours after a run its stored copy of the DM keeps only its first line and a link to eBay's own search (`ebay-forget.ts`; a send still owed then sends that), and it is never judged, whatever `judge` says, since a judged watch's Job is kept by city-hall's queue. For an inbox eBay watch, `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. It also needs the bot's `HTTP_PORT`, which the tracker cannot see: with `TRACKER_WEB_URL` set and `HTTP_PORT` unset, the watch is made but nothing can send it listings. Every `hours` (default 12 for a page, 24 for BGG and Shopify, 1 for eBay's API and 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 (an eBay API watch's excepted, above: its extra listings are left to eBay's own search). 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, Shopify or eBay watch: it judges from what each listing says. |
|
|
49
|
+
| `/want name source [target] [max] [currency] [exclude] [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; `shopify` (since 0.32.0): `target` is the words to search Shopify stores for (the name when empty), one Shopify catalog search per look through Firecrawl's Alexandria while `TRACKER_FIRECRAWL_KEY` is set (refused without it), for new copies in stock at or under `max`; a product counts only when its title has every word, and each look costs 5 Firecrawl credits, so `hours` is 24 to 168 for it; or `ebay`: with the eBay keys (`TRACKER_EBAY_CLIENT_ID` and `TRACKER_EBAY_CLIENT_SECRET`, since 0.33.0), one search per look through eBay's own Browse API (docket-types' `ebaySource`; eBay US, the newest fifty, narrowed by `max`), for listings whose title has every word of `target` (the words, the name when empty), new and used, an auction at its current bid (or its Buy It Now price, when it has one), the seller's name, feedback percentage and score shown; every hour by default, and the watcher still never opens an eBay page. Without the keys, `ebay` is an **inbox watch** (since 0.19.0; docket-types' `inbox` source), whose listings are sent in rather than read -- the watcher never opens eBay. A watch made before the keys were set stays an inbox watch; one made with them that outlives the keys fails each look and tells its owner once. An eBay API watch takes no alert emails: an alert for its words is left without a DM. It keeps no eBay data, so that the keyset may opt out of eBay's account-deletion notices: no finding is stored, its listings are in the DM only, six hours after a run its stored copy of the DM keeps only its first line and a link to eBay's own search (`ebay-forget.ts`; a send still owed then sends that), and it is never judged, whatever `judge` says, since a judged watch's Job is kept by city-hall's queue. For an inbox eBay watch, `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. It also needs the bot's `HTTP_PORT`, which the tracker cannot see: with `TRACKER_WEB_URL` set and `HTTP_PORT` unset, the watch is made but nothing can send it listings. Every `hours` (default 12 for a page, 24 for BGG and Shopify, 1 for eBay's API and 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 (an eBay API watch's excepted, above: its extra listings are left to eBay's own search). 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, Shopify or eBay watch: it judges from what each listing says. **`exclude`** (since 0.38.0, docket#77): words or phrases separated by commas, at most 20 of 80 characters each (1,000 characters in all); a listing whose title has any of them, as whole words in that order (case, accents and punctuation ignored, docket-types' `isExcluded`), is left out of every source before it is DMed or judged, and the confirmation names them. For a Shopify or eBay-API watch, a one-word entry that is one of the words searched for is refused, since every listing it keeps has that word. A field with no words in it is refused. |
|
|
50
50
|
| `/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. |
|
|
51
51
|
| `/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. |
|
|
52
52
|
| `/task done task` / `/task snooze task [until]` | the task's owner | Answers the task's latest reminder (snooze: an hour, or until `until`). |
|
|
@@ -235,7 +235,7 @@ served at a hashed path, cached for a year).
|
|
|
235
235
|
|---|---|
|
|
236
236
|
| `/` | My tasks: the same list as `/tasks` (active tasks owned and received, next run in your zone; paused ones and why), each linking to its history, and links to make a new one. |
|
|
237
237
|
| `/tasks/<id>` | A task's history: the same as `/task history`, for the owner, an accepted recipient or an admin. Anyone else gets the same 404 as an unknown id. The owner also sees Edit (not for a research request), Pause or Resume, and Delete. |
|
|
238
|
-
| `/new/wantlist` | A new want-list watch (GET, POST), by `/want`'s rules (`judge` too, yes or no): a page, a BGG game, or eBay (an inbox watch: `target` is the words to search for, the name when empty). Its edit changes the name, top price, currency and hours; an emptied top price or
|
|
238
|
+
| `/new/wantlist` | A new want-list watch (GET, POST), by `/want`'s rules (`judge` too, yes or no): a page, a BGG game, or eBay (an inbox watch: `target` is the words to search for, the name when empty). Its edit changes the name, top price, currency, exclude words and hours; an emptied top price, currency or exclude field clears it, and where it looks -- an inbox watch's key and words too -- stays as made. |
|
|
239
239
|
| `/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. |
|
|
240
240
|
| `/tasks/<id>/edit` | The owner's edit form (GET) and saving it (POST). |
|
|
241
241
|
| `/tasks/<id>/pause`, `/resume`, `/delete` | POST only. Delete answers a confirmation first; only a second post carrying `confirm=yes` deletes. |
|
|
@@ -725,7 +725,7 @@ a research request cannot be edited, here or in Discord, and a `PATCH` of one an
|
|
|
725
725
|
| `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. |
|
|
726
726
|
| `GET /api/v1/capabilities` | What this bot can make for you, in one read: `types` (as `GET /types`), `owner` (your zone and preferred hour), `limits` (`liveTasks`, `priceTrackers`, `researchWaiting`, `scouts`, `wantWatches`, `pageSize`, `requestsBurst`, `requestsPerSecond`) and `sources`, which say whether each is set up on this bot -- `runner` (research and scouts), `bgg`, `ebay` (eBay's own API), `shopify`, `mail` (alert email) and `webArea` (the web area, `TRACKER_WEB_URL`, which browser research needs) -- as true or false only, never a key, token or address. (0.37.0, #369.) |
|
|
727
727
|
| `GET /api/v1/tasks` | Your tasks that are not deleted -- active, paused and done -- oldest first. Not the ones shared with you. With `?limit=N` (1 to 200) one page of them, and `nextCursor` (null on the last page): send it back as `?limit=N&cursor=<nextCursor>` for the next. A task made or deleted between two pages moves no other across the cut, so each one still there is listed exactly once. Without `limit`, all of them, and no `nextCursor`. Any other query parameter is ignored; `limit` or `cursor` given twice is `400 invalid`. (0.37.0, #369.) |
|
|
728
|
-
| `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`, `at` and `sources` (`web`, the default, or `browser`: it waits for the pages your browser sends in, below), 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. |
|
|
728
|
+
| `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`, `at` and `sources` (`web`, the default, or `browser`: it waits for the pages your browser sends in, below), 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), `exclude` (text: words or phrases separated by commas; an empty string clears it in an edit, and `settings.exclude` reads them back joined by `, `), `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. |
|
|
729
729
|
| `POST /api/v1/tasks/validate` | A dry run of `POST /tasks`: the same body, checked, and nothing made or written. A reminder, a renewal or a scout gets every check its create runs before it writes (the type's rules, the live-task cap, the scout cap) and `{"valid": true, "checked": "full", "timeZone", "cadence", "firstAt", "thenAt", "message"}`: its first run and the run after, as UTC instants, placed in your zone as the scheduler places them (a renewal already within its lead is asked at once: its `firstAt` is then the ask's own date, already past, as the made task's `nextAt` will be). A price, a want-list watch or a research request gets its fields checked and the live-task cap (`"checked": "fields"`), and none of its type's own checks: its create reads a page or a source, or asks the model runner, so the rest is known only when it is made, and its create may refuse first for another reason (a bad address, the price-tracker cap). A refusal is the create's own (`400 invalid`, `unknown_field`, `409 limit_reached`, `503 unavailable`). Advice, not a reservation: a create made after it can still meet a cap. Takes no `If-Match`; an `Idempotency-Key` is ignored, as nothing is written. (0.37.0, #369.) |
|
|
730
730
|
| `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. Each run carries `delivery` (0.37.0, #369): `state` is `not_run` (not finished yet: queued or running, so no copy is planned), `nothing_to_send` (no copy: it fired with no message for anyone, was skipped, or failed before it had one), `sending` (a copy still owed: not tried, out now, or retrying), `delivered` (every copy went out), `unconfirmed` (a send failed in a way that does not say whether it went out; never resent) or `failed` (a copy failed for good); when copies differ, `failed` outranks `unconfirmed`, which outranks `sending`, then `delivered`. `reason` is the error of the failed or unconfirmed copy; `sends` lists each copy, `to`, `status` and `error`. |
|
|
731
731
|
| `PATCH /api/v1/tasks/<id>` | Edits it: the fields to change. A price's page is not editable. |
|
package/dist/plugin.js
CHANGED
|
@@ -6825,6 +6825,7 @@ var MAX_REMINDER_TEXT = 1500;
|
|
|
6825
6825
|
var MAX_WHEN = 100;
|
|
6826
6826
|
var MAX_NOTE = 300;
|
|
6827
6827
|
var MAX_URL = 1000;
|
|
6828
|
+
var MAX_EXCLUDE_TEXT = 1000;
|
|
6828
6829
|
var MAX_NEAR = 100;
|
|
6829
6830
|
var MAX_AMOUNT = 1e6;
|
|
6830
6831
|
var CURRENCY_LENGTH = 3;
|
|
@@ -8297,6 +8298,33 @@ function limitsPlan(input, defaultHours, minHours = 1) {
|
|
|
8297
8298
|
limits: { name, hours, ...input.max !== undefined ? { maxPrice: input.max } : {}, ...currency ? { currency } : {} }
|
|
8298
8299
|
};
|
|
8299
8300
|
}
|
|
8301
|
+
function excludePlan(text, words) {
|
|
8302
|
+
const raw = (text ?? "").trim();
|
|
8303
|
+
if (raw === "" || raw === "-")
|
|
8304
|
+
return { ok: true, exclude: [] };
|
|
8305
|
+
if (raw.length > MAX_EXCLUDE_TEXT)
|
|
8306
|
+
return { ok: false, error: `\`exclude\` is longer than ${MAX_EXCLUDE_TEXT} characters.` };
|
|
8307
|
+
const entries = raw.split(/[,\n]/).map((e) => e.trim()).filter((e) => e !== "");
|
|
8308
|
+
const long = entries.find((e) => e.length > EXCLUDE_CHARS);
|
|
8309
|
+
if (long !== undefined)
|
|
8310
|
+
return { ok: false, error: `Each word or phrase in \`exclude\` is at most ${EXCLUDE_CHARS} characters.` };
|
|
8311
|
+
const distinct = new Set(entries.map((e) => searchWords(e).join(" ")).filter((k) => k !== ""));
|
|
8312
|
+
if (distinct.size > MAX_EXCLUDE)
|
|
8313
|
+
return { ok: false, error: `\`exclude\` takes at most ${MAX_EXCLUDE} words or phrases, separated by commas.` };
|
|
8314
|
+
if (distinct.size === 0)
|
|
8315
|
+
return { ok: false, error: "`exclude` needs words or phrases, separated by commas, such as `investigator`." };
|
|
8316
|
+
const exclude = parseExclude(raw);
|
|
8317
|
+
if (words !== null) {
|
|
8318
|
+
const searched = new Set(searchWords(words));
|
|
8319
|
+
const every = exclude.find((e) => {
|
|
8320
|
+
const w = searchWords(e);
|
|
8321
|
+
return w.length === 1 && searched.has(w[0]);
|
|
8322
|
+
});
|
|
8323
|
+
if (every !== undefined)
|
|
8324
|
+
return { ok: false, error: `\`exclude\` has "${every}", a word this watch searches for, so every listing it finds has it and none would be left.` };
|
|
8325
|
+
}
|
|
8326
|
+
return { ok: true, exclude };
|
|
8327
|
+
}
|
|
8300
8328
|
function ebaySearchUrl(words, max) {
|
|
8301
8329
|
const q = new URLSearchParams({ _nkw: words });
|
|
8302
8330
|
if (max !== undefined)
|
|
@@ -8429,11 +8457,15 @@ async function startWant(d, user, input) {
|
|
|
8429
8457
|
const capped = await liveTaskCap(d, user);
|
|
8430
8458
|
if (capped)
|
|
8431
8459
|
return { ok: false, error: capped };
|
|
8460
|
+
const excluded = excludePlan(input.exclude, words);
|
|
8461
|
+
if (!excluded.ok)
|
|
8462
|
+
return excluded;
|
|
8432
8463
|
const where = words !== null ? { source: source === "ebay" ? "ebay" : "shopify", target: words } : site && inbox ? { source: "inbox", target: newInboxKey(site), search: inbox.search, ...inbox.game !== null ? { game: String(inbox.game) } : {} } : { source, target: source === "bgg" ? String(parseBggThingId(target)) : target };
|
|
8433
8464
|
const config = {
|
|
8434
8465
|
...where,
|
|
8435
8466
|
...limits.maxPrice !== undefined ? { maxPrice: limits.maxPrice } : {},
|
|
8436
|
-
...limits.currency ? { currency: limits.currency } : {}
|
|
8467
|
+
...limits.currency ? { currency: limits.currency } : {},
|
|
8468
|
+
...excluded.exclude.length > 0 ? { exclude: excluded.exclude } : {}
|
|
8437
8469
|
};
|
|
8438
8470
|
return { ok: true, start: { title: limits.name, hours: limits.hours, config, judge: judged.judge } };
|
|
8439
8471
|
}
|
|
@@ -8442,8 +8474,9 @@ async function previewWant(d, start) {
|
|
|
8442
8474
|
return { ok: true, text: "" };
|
|
8443
8475
|
try {
|
|
8444
8476
|
const listings = await pageSource.search(start.config.target, d.fetch ?? undefined);
|
|
8445
|
-
const within = listings.filter((l) =>
|
|
8446
|
-
const
|
|
8477
|
+
const within = listings.filter((l) => wanted(l, start.config)).length;
|
|
8478
|
+
const c = start.config;
|
|
8479
|
+
const limited = c.maxPrice !== undefined || c.currency || (c.exclude?.length ?? 0) > 0 ? `, ${within} within your limits` : "";
|
|
8447
8480
|
return { ok: true, text: `The page lists ${plural4(listings.length, "thing")} now${limited}.` };
|
|
8448
8481
|
} catch (err) {
|
|
8449
8482
|
const cause = err instanceof Error ? err.cause : undefined;
|
|
@@ -8494,7 +8527,8 @@ async function finishWant(d, asked, start, seen) {
|
|
|
8494
8527
|
start.judge ? `I look at ${where} every ${plural4(start.hours, "hour")}, and the model looks at each listing${limits.length ? ` ${limits.join(" ")}` : ""} I have not shown you before -- whether it is the thing, and ${shows} -- then I DM you the ones worth a look, with its notes; the first look starts within a minute. Those looks are model runs, charged like research; \`judge: false\` makes a watch without them. Press Done on a DM once you have it, or \`/task done ${task.id}\`.` : `I look at ${where} every ${plural4(start.hours, "hour")} and DM you each listing${limits.length ? ` ${limits.join(" ")}` : ""} I have not shown you before; ${first}. Press Done on a DM once you have it, or \`/task done ${task.id}\`.`,
|
|
8495
8528
|
...inboxNote(c, task.id, d.mail && watchSite(c) === "ebay" ? d.mail.addressFor(user.id, now) : null),
|
|
8496
8529
|
...c.source === "shopify" ? ["Shopify's search finds new copies at Shopify stores only (not eBay or used copies), and I keep a product only when its title has every word you are watching for. Each look is a Firecrawl search (5 credits)."] : [],
|
|
8497
|
-
...c.source === "ebay" ? ["I search eBay US through eBay's own API, newest listings first, new and used, an auction at its current bid (or its Buy It Now price), and count a listing only when its title has every word you are watching for. I never open an eBay page, and the model does not check these listings. eBay's terms let me keep its listings only briefly, so they are in the DM alone, and my own copy of it is cleared after 6 hours."] : []
|
|
8530
|
+
...c.source === "ebay" ? ["I search eBay US through eBay's own API, newest listings first, new and used, an auction at its current bid (or its Buy It Now price), and count a listing only when its title has every word you are watching for. I never open an eBay page, and the model does not check these listings. eBay's terms let me keep its listings only briefly, so they are in the DM alone, and my own copy of it is cleared after 6 hours."] : [],
|
|
8531
|
+
...c.exclude && c.exclude.length > 0 ? [`I leave out any listing whose title has ${c.exclude.map((e) => `"${e}"`).join(", ")}.`] : []
|
|
8498
8532
|
].join(`
|
|
8499
8533
|
`))
|
|
8500
8534
|
};
|
|
@@ -8530,13 +8564,18 @@ async function editWantLocked(d, user, taskId, input) {
|
|
|
8530
8564
|
const { limits } = planned;
|
|
8531
8565
|
if (config.source === "ebay" && limits.currency && limits.currency !== "USD")
|
|
8532
8566
|
return { ok: false, error: EBAY_USD };
|
|
8567
|
+
const searchedBy = config.source === "shopify" || config.source === "ebay" ? config.target : null;
|
|
8568
|
+
const excluded = input.exclude === undefined ? { ok: true, exclude: config.exclude ?? [] } : excludePlan(input.exclude, searchedBy);
|
|
8569
|
+
if (!excluded.ok)
|
|
8570
|
+
return excluded;
|
|
8533
8571
|
const nextConfig = {
|
|
8534
8572
|
source: config.source,
|
|
8535
8573
|
target: config.target,
|
|
8536
8574
|
...config.search !== undefined ? { search: config.search } : {},
|
|
8537
8575
|
...config.game !== undefined ? { game: config.game } : {},
|
|
8538
8576
|
...limits.maxPrice !== undefined ? { maxPrice: limits.maxPrice } : {},
|
|
8539
|
-
...limits.currency ? { currency: limits.currency } : {}
|
|
8577
|
+
...limits.currency ? { currency: limits.currency } : {},
|
|
8578
|
+
...excluded.exclude.length > 0 ? { exclude: excluded.exclude } : {}
|
|
8540
8579
|
};
|
|
8541
8580
|
const patch = {};
|
|
8542
8581
|
if (limits.name !== task.title)
|
|
@@ -11333,12 +11372,13 @@ function createSurface(w) {
|
|
|
11333
11372
|
},
|
|
11334
11373
|
{
|
|
11335
11374
|
name: "want",
|
|
11336
|
-
build: (b) => b.setDescription("Watch for something you want: I DM you new listings until you have it").addStringOption((o) => o.setName("name").setDescription("What you want, e.g. Wingspan Oceania expansion").setRequired(true).setMaxLength(MAX_TITLE)).addStringOption((o) => o.setName("source").setDescription("Where to look").setRequired(true).addChoices({ name: "a listing page you paste", value: "page" }, { name: "BoardGameGeek's marketplace", value: "bgg" }, { name: "eBay (through eBay's own API when set up, else from listings you send in)", value: "ebay" }, { name: "Shopify stores (new copies in stock, at most once a day)", value: "shopify" })).addStringOption((o) => o.setName("target").setDescription("The page's address, or the BGG game's address or id; for eBay or Shopify, the words to search").setMaxLength(MAX_URL)).addNumberOption((o) => o.setName("max").setDescription("Only listings at or under this price").setMinValue(0.01)).addStringOption((o) => o.setName("currency").setDescription("Only listings in this currency, e.g. USD").setMinLength(CURRENCY_LENGTH).setMaxLength(CURRENCY_LENGTH)).addIntegerOption((o) => o.setName("hours").setDescription(`Hours between looks (default: page ${DEFAULT_PAGE_HOURS}, BGG API ${DEFAULT_BGG_HOURS}, eBay ${DEFAULT_EBAY_HOURS}, BGG without it ${DEFAULT_INBOX_HOURS}, Shopify ${DEFAULT_SHOPIFY_HOURS})`).setMinValue(1).setMaxValue(MAX_WANT_HOURS)).addBooleanOption((o) => o.setName("judge").setDescription("Have the model check each new listing and its seller first (default: yes, when it can)")),
|
|
11375
|
+
build: (b) => b.setDescription("Watch for something you want: I DM you new listings until you have it").addStringOption((o) => o.setName("name").setDescription("What you want, e.g. Wingspan Oceania expansion").setRequired(true).setMaxLength(MAX_TITLE)).addStringOption((o) => o.setName("source").setDescription("Where to look").setRequired(true).addChoices({ name: "a listing page you paste", value: "page" }, { name: "BoardGameGeek's marketplace", value: "bgg" }, { name: "eBay (through eBay's own API when set up, else from listings you send in)", value: "ebay" }, { name: "Shopify stores (new copies in stock, at most once a day)", value: "shopify" })).addStringOption((o) => o.setName("target").setDescription("The page's address, or the BGG game's address or id; for eBay or Shopify, the words to search").setMaxLength(MAX_URL)).addNumberOption((o) => o.setName("max").setDescription("Only listings at or under this price").setMinValue(0.01)).addStringOption((o) => o.setName("currency").setDescription("Only listings in this currency, e.g. USD").setMinLength(CURRENCY_LENGTH).setMaxLength(CURRENCY_LENGTH)).addStringOption((o) => o.setName("exclude").setDescription("Leave out listings whose title has any of these words, separated by commas").setMaxLength(MAX_EXCLUDE_TEXT)).addIntegerOption((o) => o.setName("hours").setDescription(`Hours between looks (default: page ${DEFAULT_PAGE_HOURS}, BGG API ${DEFAULT_BGG_HOURS}, eBay ${DEFAULT_EBAY_HOURS}, BGG without it ${DEFAULT_INBOX_HOURS}, Shopify ${DEFAULT_SHOPIFY_HOURS})`).setMinValue(1).setMaxValue(MAX_WANT_HOURS)).addBooleanOption((o) => o.setName("judge").setDescription("Have the model check each new listing and its seller first (default: yes, when it can)")),
|
|
11337
11376
|
handle: (interaction) => run(interaction, "registered", (d, user) => {
|
|
11338
11377
|
const target = interaction.options.getString("target");
|
|
11339
11378
|
const judge = interaction.options.getBoolean("judge");
|
|
11340
11379
|
const max = interaction.options.getNumber("max");
|
|
11341
11380
|
const currency = interaction.options.getString("currency");
|
|
11381
|
+
const exclude = interaction.options.getString("exclude");
|
|
11342
11382
|
const hours = interaction.options.getInteger("hours");
|
|
11343
11383
|
return wantCommand(d, user, {
|
|
11344
11384
|
name: interaction.options.getString("name", true),
|
|
@@ -11346,6 +11386,7 @@ function createSurface(w) {
|
|
|
11346
11386
|
...target !== null ? { target } : {},
|
|
11347
11387
|
...max !== null ? { max } : {},
|
|
11348
11388
|
...currency !== null ? { currency } : {},
|
|
11389
|
+
...exclude !== null ? { exclude } : {},
|
|
11349
11390
|
...hours !== null ? { hours } : {},
|
|
11350
11391
|
...judge !== null ? { judge } : {}
|
|
11351
11392
|
});
|
|
@@ -13070,6 +13111,7 @@ var JUDGE_OPTIONS = [["yes", "yes: the model checks each new listing and its sel
|
|
|
13070
13111
|
var wantLimits = (mode, surface) => [
|
|
13071
13112
|
{ name: "max", label: "Top price", kind: "text", decimal: true, help: "Only listings at or under it, and so only listings that show a price. Leave it empty for any price." },
|
|
13072
13113
|
{ name: "currency", label: "Currency", kind: "text", maxlength: CURRENCY_LENGTH, help: "Only listings in it, such as USD. Leave it empty for any." },
|
|
13114
|
+
{ name: "exclude", label: "Leave out titles with", kind: "text", maxlength: MAX_EXCLUDE_TEXT, help: "Words or phrases, separated by commas: a listing whose title has any of them is left out, such as investigator for a campaign box. At most 20. Leave it empty to leave nothing out." },
|
|
13073
13115
|
{ name: "hours", label: "Hours between looks", kind: "number", min: 1, max: MAX_WANT_HOURS, help: `1 to ${MAX_WANT_HOURS}, and at least ${MIN_SHOPIFY_HOURS} for Shopify, since each Shopify look is a paid Firecrawl search. ${empty(mode, `When empty, ${DEFAULT_PAGE_HOURS} for a page, ${DEFAULT_BGG_HOURS} for BGG, ${DEFAULT_EBAY_HOURS} for eBay, ${DEFAULT_INBOX_HOURS} for BGG when this bot has no BGG access, whose listings are sent in, and ${DEFAULT_SHOPIFY_HOURS} for Shopify.`, surface)}` }
|
|
13074
13116
|
];
|
|
13075
13117
|
function fieldsFor(type, mode, surface = "web") {
|
|
@@ -13317,6 +13359,7 @@ function scoutEdit(form) {
|
|
|
13317
13359
|
}
|
|
13318
13360
|
function wantInput(form) {
|
|
13319
13361
|
const target = opt(form, "target");
|
|
13362
|
+
const exclude = opt(form, "exclude");
|
|
13320
13363
|
const max = decimal(form, "max");
|
|
13321
13364
|
const currency = opt(form, "currency");
|
|
13322
13365
|
const hours = int(form, "hours");
|
|
@@ -13328,6 +13371,7 @@ function wantInput(form) {
|
|
|
13328
13371
|
...target !== undefined ? { target } : {},
|
|
13329
13372
|
...max !== undefined ? { max } : {},
|
|
13330
13373
|
...currency !== undefined ? { currency } : {},
|
|
13374
|
+
...exclude !== undefined ? { exclude } : {},
|
|
13331
13375
|
...hours !== undefined ? { hours } : {}
|
|
13332
13376
|
};
|
|
13333
13377
|
}
|
|
@@ -13338,6 +13382,7 @@ function wantEdit(form) {
|
|
|
13338
13382
|
...name !== undefined ? { name } : {},
|
|
13339
13383
|
...form.has("max") ? { max: decimal(form, "max") ?? null } : {},
|
|
13340
13384
|
...form.has("currency") ? { currency: str3(form, "currency") } : {},
|
|
13385
|
+
...form.has("exclude") ? { exclude: str3(form, "exclude") } : {},
|
|
13341
13386
|
...hours !== undefined ? { hours } : {}
|
|
13342
13387
|
};
|
|
13343
13388
|
}
|
|
@@ -13431,7 +13476,8 @@ function editValues(d, user, task) {
|
|
|
13431
13476
|
if (task.type === "wantlist" || task.type === "wantjudge") {
|
|
13432
13477
|
const c = task.config;
|
|
13433
13478
|
const every = task.schedule?.kind === "poll" ? task.schedule.every : "";
|
|
13434
|
-
|
|
13479
|
+
const exclude = Array.isArray(c.exclude) ? c.exclude.filter((e) => typeof e === "string").join(", ") : "";
|
|
13480
|
+
return { name: task.title, max: c.maxPrice !== undefined ? String(c.maxPrice) : "", currency: c.currency ?? "", exclude, hours: String(every) };
|
|
13435
13481
|
}
|
|
13436
13482
|
if (task.type !== "price")
|
|
13437
13483
|
return {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rackbops/plugin-tracker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "The task tracker for Rackbops Clerk: reminders, renewals, price drops, one-off research (on the web, or on pages your own browser gathers and sends in through the task API, with a fuller pass on pages the research could not open) and an interest scout (through city-hall, with their findings), a want-list watcher (listing pages you paste, BoardGameGeek through its API once its token is set, Shopify stores' new copies in stock through Firecrawl once its key is set, eBay through eBay's own Browse API once its keys are set, and eBay and BoardGameGeek listings you send in through the task API or that eBay's saved-search alert emails forwarded to your alert address carry, which the watcher never opens; with the model checking each new listing and its seller first, through city-hall) by slash command, delivered by DM with buttons, and a daily DM at each person's hour of what is due today and overdue, on docket's SQLite store (/allow, /register, /remind, /renewal, /price, /research, /scout, /want, /tasks, /task, /settings, /web), with a web area for your tasks, history and settings, a web editor to make, change, pause and delete your own tasks (research requests too, made but not changed, and scouts, made and changed), an admin view (with the DMs that did not arrive, raising a person's daily model ceiling, and daily model usage per person), an operator switch that turns the daily budgets off for the alpha, forget-me, a JSON task API with personal API tokens, and a plain-language intake: DM Clerk what you want in your own words, answer its questions, and press Save on the draft it shows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/plugin.js",
|