fix(cli): validate follower and inbox options before querying - #133
Conversation
Number("abc") is NaN and typeof NaN is "number", so NaN passed every
guard and reached the query layer.
search dms and dms list bound NaN into a SQL comparison on
followers_count, which returns no rows and exit code 0.
inbox passed NaN to listInboxItems, whose minScore and limit are default
parameters and do not fire for NaN. The score filter and the slice both
returned nothing.
Validate the four options with the parser already used in these files.
The influence score options already reject NaN in
getMinFollowersForInfluenceScore and getMaxFollowersForInfluenceScore, so
they are unchanged.
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs maintainer review before merge. Reviewed August 30, 2026, 10:39 PM ET / August 31, 2026, 02:39 UTC. ClawSweeper reviewWhat this changesThe PR validates DM follower filters and inbox score/limit arguments as non-negative integers before querying Birdclaw’s local archive, with regression cases for rejected and accepted values. Regression provenancePossible regression — probable (reviewed change; failure trace). No predecessor PR is attributed. Merge readinessThe branch still fixes a current-main CLI defect: malformed numeric filters can become NaN and silently produce empty archive results. The focused patch, targeted tests, successful checks, and supplied before/after terminal transcript support ordinary maintainer merge review. Priority: P2 Review scores
Verification
How this fits togetherBirdclaw’s CLI converts user-supplied archive filters into parameters for local DM and inbox read models. Each command either rejects invalid input or passes typed filters to query and ranking code that formats the operator’s results. flowchart LR
User[CLI user] --> Flags[Numeric filter flags]
Flags --> Parse[Option validation]
Parse --> Decision{Valid input?}
Decision -->|No| Error[Structured error and exit 1]
Decision -->|Yes| Read[DM or inbox read model]
Read --> Output[Results output]
Before merge
Agent review detailsSecurityNone. Review metrics
Technical reviewBest possible solution: Merge the CLI-boundary validation so malformed filters consistently fail with a structured error before Birdclaw queries the local archive. Do we have a high-confidence way to reproduce the issue? Yes—current main directly converts the affected strings with Number(), and the read models accept the resulting numeric NaN path; the supplied terminal transcript also exercises the changed CLI behavior. Is this the best way to solve the issue? Yes—the patch reuses Birdclaw’s existing non-negative-integer parser at the command boundary, preventing invalid values from reaching otherwise valid read-model code. AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against c184cf5f1b5e. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (27 earlier review cycles; latest 8 shown)
|
|
Added the real CLI transcript ClawSweeper asked for, in the description.
Before, all four bad values returned an empty result with exit 0. After, each is rejected with a structured error and exit 1, and a valid |
|
Maintainer triage: LAND recommended with the prepared compatibility fix, not the current head unchanged. Commit 9b1ebf4 on triage/133-numeric-filter-proof follows the original contributor commits and includes regression coverage, docs, and a changelog entry. The original fork branch has not been rewritten; nothing has been merged. The reported bug reproduces against a populated demo database: malformed follower filters and inbox scores become The maintainer follow-up rejects non-finite thresholds while retaining their existing finite-number semantics, including fractions and signed values. Inbox result limits remain non-negative safe integers, with the existing numeric spellings accepted. The limit helper matches the one in #132 so the eventual combined tree should retain one implementation. Both PRs also add tests at the same insertion point; preserve both sets when combining them. Real built CLI proof, using the checksum-pinned Bun runtime and an isolated bundled demo database: $ ./scripts/bun-canary.sh scripts/build-cli.mjs
Done
$ export BIRDCLAW_HOME="$(mktemp -d)"
$ BIRDCLAW_BACKUP_AUTO_SYNC=0 ./scripts/bun-canary.sh bin/birdclaw.mjs init --demo --json
# demo seeded: 2 accounts, 6 profiles, 6 tweets, 4 conversations, 8 messages
$ BIRDCLAW_BACKUP_AUTO_SYNC=0 ./scripts/bun-canary.sh bin/birdclaw.mjs inbox --min-score abc --json
{"error":"--min-score must be a finite number"}
# exit 1; main returned an empty inbox, exit 0
$ BIRDCLAW_BACKUP_AUTO_SYNC=0 ./scripts/bun-canary.sh bin/birdclaw.mjs inbox --min-score 3.5 --json | jq '.stats.total'
3
$ BIRDCLAW_BACKUP_AUTO_SYNC=0 ./scripts/bun-canary.sh bin/birdclaw.mjs inbox --limit 1e3 --json | jq '.stats.total'
3
$ BIRDCLAW_BACKUP_AUTO_SYNC=0 ./scripts/bun-canary.sh bin/birdclaw.mjs dms list --min-followers 1e3 --json | jq length
4The 56-invocation before/after matrix showed only the six intended malformed-option rejections and fractional inbox-limit rejection changing; the tested valid thresholds and numeric forms matched main. Validation: $ ./scripts/bun-canary.sh run --bun check
# format, lint, and typecheck passed
$ TMPDIR=/Volumes/BirdclawTriage0831/fixtures BIRDCLAW_BACKUP_AUTO_SYNC=0 BIRDCLAW_DISABLE_LIVE_WRITES=1 ./scripts/bun-canary.sh ./scripts/run-vitest.mjs run src/cli.test.ts
Test Files 1 passed (1)
Tests 81 passed (81)
Duration 4.62sThe temporary RAM-backed fixture directory resolved local I/O timeouts that also reproduced on unchanged main; no assertions or timeouts were changed. Codex autoreview returned scoped-clean at the skill's default P0 threshold. The original head's hosted checks are green; the orchestrator should apply this follow-up to the original PR and run CI on the resulting head before landing. Thanks @devYRPauli for the original fix. |
Keep Number() spellings and fractional thresholds when rejecting non-finite follower and inbox filters. Validate inbox limits without narrowing their numeric spelling grammar. Refs steipete#133. Co-authored-by: Yash Raj Pandey <yashpn62@gmail.com>
Retain both PR regression blocks and one shared parseLimitOption while merging current main. Co-authored-by: Yash Raj Pandey <yashpn62@gmail.com>
|
ClawSweeper status: review started. I am starting a fresh review of this pull request: fix(cli): validate follower and inbox options before querying This is item 1/1 in the current shard. Shard 0/1. This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking. Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted. |
Both sync install commands passed the option straight through Number(). A non-numeric value became NaN, and the library default uses `??`, which only replaces null and undefined. NaN reached the plist writer, which emits <integer>NaN</integer>. macOS rejects that plist, so the agent was written but never ran. Validate at the CLI boundary with parsePositiveIntegerOption, the helper added in steipete#133. The library keeps its current behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
Four options accept any text and turn it into
NaN.typeof NaNis"number", soNaNpasses everytypeof x === "number"guard and reaches the query layer. The command then returns nothing and exits 0, which is indistinguishable from "no matches".search dms <query>--min-followers,--max-followersdms list--min-followers,--max-followersinbox--min-score,--limitThe handlers do
options.minFollowers ? Number(options.minFollowers) : undefined. A non-empty string is truthy, so--min-followers 10kyieldsNaN.For the DM commands,
listDmConversationsdoestypeof minFollowers === "number"(true forNaN), thenMath.max(NaN, 0)isNaN, then binds it:SQLite returns zero rows for that comparison.
For
inbox,listInboxItemsdeclaresminScore = 0andlimit = 20as default parameters. Default parameters do not fire forNaN, only forundefined. SoNaNflows intoitem.score >= lowSignalFloor, which is false for every item, and into.slice(0, limit), which returns[].src/lib/inbox.tshas noNumber.isFiniteguard anywhere. A triage command silently reports an empty inbox while unreplied mentions and DMs are waiting.A realistic typo is enough:
--min-followers 10k,--min-score high.Change
Each of the four options is parsed with
parseNonNegativeIntegerOption, the helper already defined insrc/cli/command-context.tsand already used for other options in these same files. The handler returns early when the value is rejected, before any read model runs. Follower counts, the inbox rank and the inbox limit are all non-negative integers.Scope: the influence-score options are NOT changed, on purpose
--min-influence-scoreand--max-influence-scorelook like the same defect and are not.getMinFollowersForInfluenceScoreandgetMaxFollowersForInfluenceScore(src/lib/dm-read-model.ts) both begin with:so
NaNis already rejected there and never reaches SQL through that path. I checked before writing the patch and left them alone rather than claim a fix that is not one.Real behavior proof
Real CLI, built from this branch and from
origin/main, run against an empty local archive.Exit codes are the process exit codes.
Before, every bad value returned an empty result with exit 0, which a caller cannot tell apart from "no matches". After, each one is rejected with a structured error and a nonzero exit, and a valid value still runs.
Note on the build:
bun run clidoes not start here. The repo pins a Bun 1.4 canary and my local Bun is 1.3.14, which has nonode:sqlite. I bundledsrc/cli.tswith the repository's own esbuild and ran it on Node 24, which does havenode:sqlite. No source file was changed to produce this.Proof
Source changes reverted, tests kept:
The tests drive the real registered Commander actions through
runCliwith mocked read models, so a passing test also shows the rejected value never reaches a query. Each option is covered twice: an invalid value is rejected before the read model is called, and a valid value still reaches it unchanged.oxlintwith the repository's own flags: exit 0.