@rackbops/plugin-tracker 0.31.2 → 0.31.4

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 CHANGED
@@ -559,6 +559,11 @@ It is served under `/tracker/api/v1/` on the same origin as the web area, and on
559
559
  `TRACKER_WEB_URL` is set. Every write goes through the same functions as the web editor and the
560
560
  slash commands -- the same fields, defaults, limits, caps and messages -- in the same write queue.
561
561
 
562
+ The Claude in Chrome route (#125) is one such program: this repo's `clerk-browser-search` skill
563
+ (`.claude/skills/clerk-browser-search/SKILL.md`) searches eBay and BoardGameGeek in the owner's own
564
+ Chrome, looking only, and sends listings to inbox watches and pages to research requests through
565
+ `scripts/clerk-browser.ts`, which reads the Access service token and the tracker token from files.
566
+
562
567
  **Tokens.** Make one on the web area's `/tokens` page (linked from Settings): a name, and an expiry
563
568
  of 30, 90 (the default) or 365 days -- every token expires. The token -- `trk_` and 43 characters --
564
569
  is sent to you once, by Discord DM from the bot; it never appears in any web page or response, and
package/dist/plugin.js CHANGED
@@ -2842,19 +2842,19 @@ var REVIEW_SCHEMA = {
2842
2842
  },
2843
2843
  answer: {
2844
2844
  ...ANSWER_SCHEMA,
2845
- description: "The answer to send: the draft unchanged on approve, corrected on revise."
2845
+ description: "The corrected answer, on revise only; left out on approve and reject."
2846
2846
  }
2847
2847
  },
2848
2848
  required: ["verdict", "problems"]
2849
2849
  };
2850
- function parseReview(value) {
2850
+ function parseReview(value, draft) {
2851
2851
  if (!isObj2(value) || typeof value.verdict !== "string")
2852
2852
  return null;
2853
2853
  const verdict = VERDICTS.includes(value.verdict) ? value.verdict : null;
2854
2854
  if (!verdict)
2855
2855
  return null;
2856
2856
  const problems = (Array.isArray(value.problems) ? value.problems : []).filter((p) => typeof p === "string").map((p) => clean(p, MAX_PROBLEM_CHARS, true)).filter((p) => p.length > 0).slice(0, MAX_PROBLEMS);
2857
- const answer = parseAnswer(value.answer);
2857
+ const answer = parseAnswer(value.answer) ?? (verdict === "approve" ? draft ?? null : null);
2858
2858
  if (verdict !== "reject" && !answer)
2859
2859
  return null;
2860
2860
  return { verdict, problems, answer: verdict === "reject" ? null : answer };
@@ -3094,6 +3094,57 @@ function sourcesExpiry(now, deadline) {
3094
3094
  };
3095
3095
  }
3096
3096
 
3097
+ // node_modules/@rackbops/docket-types/dist/research-web.js
3098
+ var FIRECRAWL_TOOLS = [
3099
+ "mcp__firecrawl__firecrawl_search",
3100
+ "mcp__firecrawl__firecrawl_scrape"
3101
+ ];
3102
+ var WEB_ALLOWED_TOOLS = [...DEFAULT_ALLOWED_TOOLS, ...FIRECRAWL_TOOLS];
3103
+ function firecrawlLines() {
3104
+ return [
3105
+ "Read pages with WebFetch first. When a page refuses WebFetch (a 403, a block, a CAPTCHA " + "or challenge page), read it with `firecrawl_scrape` instead. Always pass " + '`proxy: "basic"` to it; never "stealth", "enhanced" or "auto", and nothing else that ' + "disguises the request. Scrape one page at a time: never several pages of the same site " + "in parallel. Never pass any eBay URL to any tool, Firecrawl included. You may use " + "`firecrawl_search` as you would web search. If Firecrawl is refused too, the page did " + "not open: handle it as a page that refused you, as above."
3106
+ ];
3107
+ }
3108
+ function firecrawlChecks() {
3109
+ return [
3110
+ "- To open a source, use WebFetch first; when a page refuses it (a 403, a block, a CAPTCHA), " + 'read it with `firecrawl_scrape`, always with `proxy: "basic"` (never "stealth", ' + '"enhanced" or "auto", nothing that disguises the request), one page at a time, never ' + "several pages of one site in parallel. Never pass any eBay URL to any tool. You may use " + "`firecrawl_search` as you would web search."
3111
+ ];
3112
+ }
3113
+ var BGG_CATEGORIES = [
3114
+ { name: "Strategy", familyId: 5497 },
3115
+ { name: "Family", familyId: 5499 },
3116
+ { name: "Thematic", familyId: 5496 },
3117
+ { name: "Party", familyId: 5498 },
3118
+ { name: "Abstract", familyId: 4666 },
3119
+ { name: "Customizable", familyId: 4667 },
3120
+ { name: "Children's", familyId: 4665 },
3121
+ { name: "Wargames", familyId: 4664 }
3122
+ ];
3123
+ function bggCategoryUrl(familyId, year) {
3124
+ return "https://boardgamegeek.com/search/boardgame?sort=bggrating&advsearch=1" + `&range[yearpublished][min]=${year}&range[yearpublished][max]=${year}` + `&range[numvoters][min]=30&nosubtypes[0]=boardgameexpansion&familyids[0]=${familyId}`;
3125
+ }
3126
+ function categoryNames() {
3127
+ const names = BGG_CATEGORIES.map((c) => c.name);
3128
+ return `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
3129
+ }
3130
+ function categoryLines(second = false) {
3131
+ return [
3132
+ 'When the question asks about categories (or "each of the major" kinds of something) and ' + "the best source defines its own categories, use that site's categories and say so" + (second ? "" : " in the summary") + "; do not invent genres, and do not say that no source defines them. For " + `board games these are BoardGameGeek's ${BGG_CATEGORIES.length} category rankings: ` + `${categoryNames()}. A category with no entry that qualifies is answered "none yet", ` + "with the reason; never leave it out." + (second ? " In this update, the summary names any category whose pick changed." : ` ${COMPACT_RUN}`)
3133
+ ];
3134
+ }
3135
+ var COMPACT_RUN = `The summary itself names every category's pick in one compact run, such as "Strategy: X; ` + `Family: Y; ...; Children's: none yet (no 2026 game has 30 ratings)", so none is lost when ` + "a long answer is cut; the findings carry each pick's source.";
3136
+ function bggSearchHint() {
3137
+ const ids = BGG_CATEGORIES.map((c) => `${c.name} ${c.familyId}`).join(", ");
3138
+ return [
3139
+ "A hint you may use: a BoardGameGeek advanced search shows one category for one year, such " + `as ${bggCategoryUrl(5497, 2026)} (Strategy, 2026). ` + `Change the year and the \`familyids[0]\` value for the category: ${ids}. These ids were ` + "seen on the site, not documented by it: if one shows the wrong category, use that " + "category's own ranking page."
3140
+ ];
3141
+ }
3142
+ function categoryChecks() {
3143
+ return [
3144
+ "- Where the question asks about categories and the best source defines its own (for " + `board games, BoardGameGeek's ${BGG_CATEGORIES.length}: ${categoryNames()}), the answer ` + "uses them and says so. An answer that invents its own categories, or says no source " + "defines them, while the source does, is `revise`d onto the source's categories. A " + 'category with no qualifying entry is answered "none yet" with the reason, never left ' + "out: `revise` an answer that skips one. The summary names every category's pick in " + 'one compact run ("Strategy: X; Family: Y; ..."), since the findings may be cut from ' + "the DM: `revise` a summary that leaves a category to the findings alone."
3145
+ ];
3146
+ }
3147
+
3097
3148
  // node_modules/@rackbops/docket-types/dist/research.js
3098
3149
  function usesSentSources(config) {
3099
3150
  return typeof config.sources === "string" && config.sources.trim().toLowerCase() === "sent";
@@ -3187,9 +3238,13 @@ function earlierBlock(earlier) {
3187
3238
  function disallowed(sent) {
3188
3239
  return [...DEFAULT_DISALLOWED_TOOLS, ...sent ? SENT_DISALLOWED_TOOLS : WEB_DISALLOWED_TOOLS];
3189
3240
  }
3241
+ function allowed(webFirst) {
3242
+ return [...webFirst ? WEB_ALLOWED_TOOLS : DEFAULT_ALLOWED_TOOLS];
3243
+ }
3190
3244
  function researchJob(config, pages, earlier, options = {}) {
3191
3245
  const canWait = options.canWait === true && pages === undefined && earlier === undefined;
3192
3246
  const sent = pages !== undefined;
3247
+ const webFirst = !sent && earlier === undefined;
3193
3248
  const prompt = [
3194
3249
  "You are researching a one-off question for a person, who will read your answer as a " + "Discord DM.",
3195
3250
  "",
@@ -3203,8 +3258,10 @@ function researchJob(config, pages, earlier, options = {}) {
3203
3258
  ""
3204
3259
  ] : [],
3205
3260
  ...partsLines(earlier !== undefined),
3261
+ ...categoryLines(earlier !== undefined),
3206
3262
  "",
3207
3263
  ...bestSourceLines(sent, canWait),
3264
+ ...webFirst ? ["", ...firecrawlLines(), ...bggSearchHint()] : [],
3208
3265
  "",
3209
3266
  "Use web search and fetch pages to check facts. Prefer primary sources (manufacturers, " + "government, utilities, standards bodies). Every claim that a reader might act on needs a " + "source you actually opened. Say plainly where sources disagree or where you are unsure.",
3210
3267
  "",
@@ -3220,7 +3277,7 @@ function researchJob(config, pages, earlier, options = {}) {
3220
3277
  return {
3221
3278
  prompt,
3222
3279
  jsonSchema: ANSWER_SCHEMA,
3223
- allowedTools: [...DEFAULT_ALLOWED_TOOLS],
3280
+ allowedTools: allowed(webFirst),
3224
3281
  disallowedTools: disallowed(sent),
3225
3282
  maxTurns: RESEARCH_MAX_TURNS,
3226
3283
  maxBudgetUsd: RESEARCH_MAX_BUDGET_USD,
@@ -3238,7 +3295,7 @@ function partsLines(second) {
3238
3295
  function bestSourceLines(pages, canWait) {
3239
3296
  return [
3240
3297
  "For each part, decide first which source is the best one for it: the site people in that " + "field treat as the reference (for board games, BoardGameGeek; for listings and sold " + "prices, eBay; otherwise the maker, the standards body or the official record). The " + "answer should rest on it.",
3241
- (pages ? "BoardGameGeek and eBay are not opened from this run: do not try. When the best source " + "is one of them and no gathered page is from it, or a page refused you (a 403, a " + "CAPTCHA or challenge page, an empty or blocked body, a sign-in wall)" : "Open BoardGameGeek like any other site: an ordinary read, with nothing to get round a " + "refusal. eBay is not opened from this run: do not try it. When the best source is " + "eBay, or its page refused you (BoardGameGeek or any other: a 403, a CAPTCHA or " + "challenge page, an empty or blocked body, a sign-in wall)") + (canWait ? ", list it in `blocked` with its site, its URL only when that is the page's real " + "address (such as BoardGameGeek's ranking or browse page), never a guessed one, and " + "why; then answer from the best sources you could open. Never write in `summary` or " + '`uncertain` that you "didn\'t check" it: `blocked` is where that goes, and the ' + "person's own browser can gather it for a fuller pass." : ", answer from the best sources you could open and name the missing source in one " + "short `uncertain` line, such as " + (pages ? '"BoardGameGeek was not among the pages read"' : '"BoardGameGeek refused the request, so its ranking is not used"') + ": no fuller pass follows this answer, so that line is how the person learns of it.")
3298
+ (pages ? "BoardGameGeek and eBay are not opened from this run: do not try. When the best source " + "is one of them and no gathered page is from it, or a page refused you (a 403, a " + "CAPTCHA or challenge page, an empty or blocked body, a sign-in wall)" : "Open BoardGameGeek like any other site: an ordinary read, with nothing that disguises " + "the request; a page that refuses WebFetch gets one more try through " + "`firecrawl_scrape`, as below (0.11.0). eBay is not opened from this run: do not try it. When the best source is " + "eBay, or its page refused you (BoardGameGeek or any other: a 403, a CAPTCHA or " + "challenge page, an empty or blocked body, a sign-in wall)") + (canWait ? ", list it in `blocked` with its site, its URL only when that is the page's real " + "address (such as BoardGameGeek's ranking or browse page), never a guessed one, and " + "why; then answer from the best sources you could open. Never write in `summary` or " + '`uncertain` that you "didn\'t check" it: `blocked` is where that goes, and the ' + "person's own browser can gather it for a fuller pass." : ", answer from the best sources you could open and name the missing source in one " + "short `uncertain` line, such as " + (pages ? '"BoardGameGeek was not among the pages read"' : '"BoardGameGeek refused the request, so its ranking is not used"') + ": no fuller pass follows this answer, so that line is how the person learns of it.")
3242
3299
  ];
3243
3300
  }
3244
3301
  function reviewJob(config, draft, pages, earlier, options = {}) {
@@ -3262,16 +3319,16 @@ function reviewJob(config, draft, pages, earlier, options = {}) {
3262
3319
  "- A claim citing a gathered page's `url`: check it against that page's text above; " + "do not try to open it, and do not open BoardGameGeek or eBay at all.",
3263
3320
  "- A claim resting only on a BoardGameGeek or eBay address that is not one of the " + "gathered pages cannot be checked: move it to `uncertain`."
3264
3321
  ] : [],
3265
- ...earlier ? updateChecks() : answerChecks(),
3266
- ...earlier || sent ? [] : bestSourceChecks(canWait),
3322
+ ...earlier ? updateChecks() : [...answerChecks(), ...categoryChecks()],
3323
+ ...earlier || sent ? [] : [...bestSourceChecks(canWait), ...firecrawlChecks()],
3267
3324
  "",
3268
- "Then give a verdict: `approve` if the draft stands as is and covers every part (return it " + "unchanged in `answer`); `revise` if it stands once corrected (return the corrected " + "answer in `answer`); `reject` if editing cannot fix it. List each problem you found in " + "`problems`, one line each."
3325
+ "Then give a verdict: `approve` if the draft stands as is and covers every part (leave " + "`answer` out: the draft is sent as it is); `revise` if it stands once corrected (return the corrected " + "answer in `answer`); `reject` if editing cannot fix it. List each problem you found in " + "`problems`, one line each."
3269
3326
  ].join(`
3270
3327
  `);
3271
3328
  return {
3272
3329
  prompt,
3273
3330
  jsonSchema: REVIEW_SCHEMA,
3274
- allowedTools: [...DEFAULT_ALLOWED_TOOLS],
3331
+ allowedTools: allowed(!sent && earlier === undefined),
3275
3332
  disallowedTools: disallowed(sent),
3276
3333
  maxTurns: REVIEW_MAX_TURNS,
3277
3334
  maxBudgetUsd: REVIEW_MAX_BUDGET_USD,
@@ -3289,7 +3346,7 @@ function answerChecks() {
3289
3346
  function bestSourceChecks(canWait) {
3290
3347
  return [
3291
3348
  "- Name the best source for each part: the site its field treats as the reference (for " + "board games, BoardGameGeek; for listings and sold prices, eBay). If the answer leans on " + "secondary sources while that source is neither among its findings' sources nor " + (canWait ? "in `blocked`, the verdict is `revise`: add it to `blocked` (its site, its URL only " + "when that is the page's real address, such as BoardGameGeek's ranking or browse " + "page, never a guessed one, and why) and keep the answer from the sources it has." : "named in `uncertain`, the verdict is `revise`: add one short `uncertain` line naming " + "it, and keep the answer from the sources it has."),
3292
- "- You may open BoardGameGeek like any site. eBay is never opened by these runs: a claim " + "resting only on an eBay address came from a search listing, not a page anyone opened, " + "so move it to `uncertain`.",
3349
+ "- You may open BoardGameGeek like any site. eBay is never opened by these runs, through " + "any tool (WebFetch, Firecrawl or another): a claim resting only on an eBay address, " + "whichever tool listed or fetched it, is not one an answer may rest on, so move it to " + "`uncertain`.",
3293
3350
  ...canWait ? [
3294
3351
  "- A page that refused is not read, so a claim must not say a source was checked " + `unless a finding cites it. A line saying a source was not checked ("I didn't ` + 'check BoardGameGeek") is no answer: move it to `blocked` on a `revise`.'
3295
3352
  ] : []
@@ -3361,7 +3418,7 @@ function afterResearch(ctx, state, output) {
3361
3418
  };
3362
3419
  }
3363
3420
  function afterReview(ctx, state, output, canWait) {
3364
- const review = parseReview(output);
3421
+ const review = parseReview(output, state.draft ?? undefined);
3365
3422
  if (!review)
3366
3423
  return failed(ctx, state, "malformed", "no usable verdict in the structured output");
3367
3424
  if (state.pass === 2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rackbops/plugin-tracker",
3
- "version": "0.31.2",
3
+ "version": "0.31.4",
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, 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, and a JSON task API with personal API tokens",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",
@@ -11,8 +11,8 @@
11
11
  "directory": "plugins/tracker"
12
12
  },
13
13
  "devDependencies": {
14
- "@rackbops/docket-core": "^0.10.0",
15
- "@rackbops/docket-types": "^0.10.0",
14
+ "@rackbops/docket-core": "^0.11.1",
15
+ "@rackbops/docket-types": "^0.11.1",
16
16
  "@rackbops/styles": "^0.2.42"
17
17
  },
18
18
  "peerDependencies": {