Skip to content

feat: add JSONC support for preset files#21

Closed
mynameistito wants to merge 7 commits intomainfrom
0.0.1
Closed

feat: add JSONC support for preset files#21
mynameistito wants to merge 7 commits intomainfrom
0.0.1

Conversation

@mynameistito
Copy link
Copy Markdown
Owner

@mynameistito mynameistito commented Apr 23, 2026

Summary

  • Adds .discord-search-presets.jsonc as a supported preset file (parsed via jsonc-parser, allowing comments)
  • Falls back to .discord-search-presets.json when no .jsonc file exists
  • Save and delete operations write back to whichever file was found, defaulting to .json for new installs

Test plan

  • Create a .discord-search-presets.jsonc with comments — verify presets load correctly
  • Save/delete a preset with .jsonc present — verify it writes back to .jsonc
  • No preset file present — verify saves create .discord-search-presets.json
  • Only .json present — verify behaviour unchanged

Summary by cubic

Adds support for .discord-search-presets.jsonc so preset files can include comments.

  • New Features

    • Parse .discord-search-presets.jsonc using jsonc-parser (comment support).
    • Fall back to .discord-search-presets.json when no .jsonc file exists.
    • Save/delete writes back to the format found; new installs create .json.
  • Migration

    • No action needed.
    • To use comments, rename .discord-search-presets.json to .discord-search-presets.jsonc.

Written for commit b8b8aef. Summary will update on new commits.

Note

Add JSONC support for preset files with fallback to JSON format

  • Introduces support for reading preset files in JSONC format (.discord-search-presets.jsonc) using jsonc-parser, with fallback to .discord-search-presets.json if no JSONC file exists
  • Save and delete operations target whichever format file is detected via the new resolvePresetsFile function in src/presets.ts
  • This is part of a larger initial commit establishing the full Discord Search CLI, including interactive/non-interactive modes, Discord API pagination, CSV/JSON export, and a complete GitHub Actions CI/CD pipeline
📊 Macroscope summarized b8b8aef. 51 files reviewed, 20 issues evaluated, 5 issues filtered, 9 comments posted

🗂️ Filtered Issues

src/cli/browser.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 26: Accessing msg.content.length on line 26 will throw a TypeError if msg.content is null or undefined. The fallback || "(no text content)" on line 28 only executes after the length check passes. Discord messages containing only embeds or attachments can have null/undefined content. The fix should check for content existence first: const content = !msg.content ? "(no text content)" : msg.content.length > 100 ? \${msg.content.slice(0, 100)}...` : msg.content;` [ Failed validation ]
src/cli/prompts.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 25: When input contains only commas or whitespace between commas (e.g., "," or ", , "), the function returns an empty array [] instead of undefined. Since empty arrays are truthy in JavaScript, the caller's check if (authorIdList) passes, causing params.authorId = [] to be set. This may cause the API to interpret an empty array as "match no authors" rather than "no author filter", potentially returning zero results when the user intended no filter. [ Failed validation ]
src/handlers/export.ts — 1 comment posted, 3 evaluated, 1 filtered
  • line 26: If an unrecognized format value is passed (e.g., "none" or a typo), the function silently creates an empty directory and returns success without exporting any files or indicating an error. Callers have no way to know their format was invalid. [ Cross-file consolidated ]
src/handlers/search.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 221: Inconsistent JSON output schema between empty and non-empty message cases. When messages.length === 0, line 221 outputs {"totalMessages":0,"messages":[]} with a messages array. However, when messages exist, line 229 outputs JSON.stringify(collated) which, based on displaySummary's usage of collateResults, returns an object with fields like totalMessages, totalEmbeds, byAuthor, embedsByType, etc. — but no messages array. JSON consumers expecting a consistent schema will break when switching between empty and non-empty results. [ Failed validation ]
src/handlers/settings.ts — 0 comments posted, 4 evaluated, 1 filtered
  • line 41: When updating the bot token via password(), an empty input is stored as "" without any trim or fallback to undefined. Compare with lines 51 and 61 which use (input as string).trim() || undefined for clientId and defaultGuildId. An empty state.token can cause issues downstream when the token is expected to be a valid string or undefined. [ Cross-file consolidated ]

…iscriminator error

Multiple preset and settings schemas share the same command literal value,
which z.discriminatedUnion does not allow.
…time support

Adds src/fs.ts with fileExists, readTextFile, readJsonFile, and writeTextFile
helpers backed by node:fs/promises, replacing all Bun.file/Bun.write usage.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e5873503-3ee3-4b40-b40b-0eefe0232803

📥 Commits

Reviewing files that changed from the base of the PR and between f9406e5 and b8b8aef.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (67)
  • .changeset/README.md
  • .changeset/config.json
  • .changeset/jsonc-preset-support.md
  • .claude/CLAUDE.md
  • .claude/commands/adopt-better-result.md
  • .claude/settings.json
  • .claude/skills/better-result-adopt/SKILL.md
  • .claude/skills/better-result-adopt/references/tagged-errors.md
  • .env.example
  • .github/CODEOWNERS
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/ISSUE_TEMPLATE/config.yml
  • .github/ISSUE_TEMPLATE/feature_request.yml
  • .github/ISSUE_TEMPLATE/question.yml
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/dependabot.yml
  • .github/labeler.yml
  • .github/workflows/ci.yml
  • .github/workflows/codeql.yml
  • .github/workflows/pr-labels.yml
  • .github/workflows/pr-triage.yml
  • .github/workflows/release.yml
  • .github/workflows/stale.yml
  • .gitignore
  • .npmrc
  • .v8rrc.yml
  • .zed/settings.json
  • AGENTS.md
  • CLAUDE.md
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • SECURITY.md
  • biome.jsonc
  • lefthook.yml
  • package.json
  • scripts/cleanup.ts
  • src/AGENTS.md
  • src/CLAUDE.md
  • src/cli/ansi.ts
  • src/cli/args-help.ts
  • src/cli/args-parse.ts
  • src/cli/args-types.ts
  • src/cli/browser.ts
  • src/cli/keys.ts
  • src/cli/prompts.ts
  • src/collate.ts
  • src/config.ts
  • src/discord/AGENTS.md
  • src/discord/CLAUDE.md
  • src/discord/client.ts
  • src/discord/schemas.ts
  • src/discord/search.ts
  • src/errors.ts
  • src/export.ts
  • src/fs.ts
  • src/handlers/AGENTS.md
  • src/handlers/CLAUDE.md
  • src/handlers/export.ts
  • src/handlers/presets.ts
  • src/handlers/search.ts
  • src/handlers/settings.ts
  • src/index.ts
  • src/paths.ts
  • src/presets.ts
  • src/types.ts
  • tsconfig.json
  • tsdown.config.ts

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a complete Discord message search CLI with interactive and non-interactive modes
    • Added preset management for saved search configurations
    • Implemented multi-format data export (JSON, CSV variants for messages/embeds/fields)
    • Added interactive message browser with live JSON viewing during searches
    • Integrated bot settings management with token, guild, and client ID persistence
  • Documentation

    • Added comprehensive contributor guide with setup and testing instructions
    • Added security policy and code of conduct
    • Added architecture and development convention documentation
  • Configuration

    • Configured CI/CD pipelines with GitHub Actions workflows
    • Set up automated dependency management and release processes
    • Configured code quality tools and linting standards

Walkthrough

This PR establishes the complete foundational structure and implementation for a Discord message search CLI application. It adds project configuration (package.json, TypeScript, Biome, build tools), GitHub workflows and templates, comprehensive documentation, CLI argument parsing and handlers, Discord API integration with rate-limit handling, data collation and multi-format export utilities, preset management, and supporting configuration/filesystem utilities.

Changes

Cohort / File(s) Summary
Changesets Configuration
.changeset/README.md, .changeset/config.json, .changeset/jsonc-preset-support.md
Introduces Changesets documentation and configuration for changelog generation and automated release management, plus a release note for JSONC preset support.
Claude Agent Configuration
.claude/CLAUDE.md, .claude/settings.json, .claude/commands/adopt-better-result.md, .claude/skills/better-result-adopt/SKILL.md, .claude/skills/better-result-adopt/references/tagged-errors.md
Defines code standards, Biome configuration hooks, and comprehensive guides for adopting better-result error handling patterns with TaggedError typed result flows.
Root Project Configuration
package.json, tsconfig.json, tsdown.config.ts, biome.jsonc, lefthook.yml, .npmrc, .gitignore, .env.example, .v8rrc.yml, .zed/settings.json
Configures package metadata, TypeScript compilation, build/bundling, code formatting/linting, pre-commit hooks, npm registry, environment templates, and editor settings.
GitHub Configuration & Workflows
.github/CODEOWNERS, .github/ISSUE_TEMPLATE/bug_report.yml, .github/ISSUE_TEMPLATE/feature_request.yml, .github/ISSUE_TEMPLATE/question.yml, .github/ISSUE_TEMPLATE/config.yml, .github/PULL_REQUEST_TEMPLATE.md, .github/dependabot.yml, .github/labeler.yml, .github/workflows/ci.yml, .github/workflows/codeql.yml, .github/workflows/pr-labels.yml, .github/workflows/pr-triage.yml, .github/workflows/release.yml, .github/workflows/stale.yml
Establishes code ownership, structured issue/PR templates, dependency and stale-issue management, PR labeling automation, code quality scanning (CodeQL), CI/CD pipelines, and automated release workflows.
Root Documentation
AGENTS.md, CLAUDE.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md
Provides project overview and architecture guidance, community standards, contribution guidelines including coding conventions and PR requirements, and security vulnerability reporting policies.
CLI Argument Parsing & Help
src/cli/ansi.ts, src/cli/args-help.ts, src/cli/args-parse.ts, src/cli/args-types.ts
Implements ANSI formatting constants, CLI help/diagnostics text generator, multi-stage argument parser with subcommand dispatch and validation, and Zod-based type definitions for all parsed arguments.
CLI Interaction & UI
src/cli/keys.ts, src/cli/prompts.ts, src/cli/browser.ts
Provides interactive key listening from stdin, prompting utilities for token/search parameters using @clack/prompts, and message browser UI with toggle modes for preview/JSON and navigation controls.
Discord API Integration
src/discord/AGENTS.md, src/discord/CLAUDE.md, src/discord/schemas.ts, src/discord/client.ts, src/discord/search.ts
Defines Discord message/embed/field Zod validation schemas, authenticated API client with rate-limit awareness and result-based error handling, and paginated search with progress callbacks supporting both offset and snowflake-boundary pagination.
Data Processing & Export
src/collate.ts, src/export.ts
Collates Discord messages into structured reports with aggregate counts by channel/author/embed type/domain, and exports to JSON and CSV formats (messages/embeds/fields) with field escaping.
Handlers & Business Logic
src/handlers/AGENTS.md, src/handlers/CLAUDE.md, src/handlers/search.ts, src/handlers/presets.ts, src/handlers/settings.ts, src/handlers/export.ts
Implements orchestration layer for search (interactive/non-interactive with live JSON streaming and progress), preset management (list/run/save/delete), settings UI and token resolution, and export format selection with concurrent file writing.
Core Utilities & Configuration
src/types.ts, src/paths.ts, src/config.ts, src/fs.ts, src/presets.ts, src/errors.ts, src/AGENTS.md, src/CLAUDE.md
Defines AppState type, filesystem path constants with app directory initialization, configuration loading/saving via JSON with env fallback, filesystem I/O wrappers, preset persistence with JSONC/JSON support, and six TaggedError subclasses for typed error handling.
CLI Entry Point
src/index.ts, scripts/cleanup.ts
Main CLI program routing interactive vs. non-interactive modes to appropriate handlers with token resolution and app state management; cleanup utility for pre-commit temporary file removal.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • mynameistito/discord-search#2: Adds identical core configuration and utility modules (src/config.ts, src/errors.ts, src/paths.ts, src/types.ts) with overlapping type definitions and initialization logic.
  • mynameistito/discord-search#6: Modifies the same data-processing pipeline modules (src/collate.ts, src/export.ts, src/presets.ts) with overlapping implementations for result collation and multi-format export.
  • mynameistito/discord-search#7: Adds the same handler layer and CLI entrypoint modules (src/handlers/*, src/index.ts) with overlapping handler implementations for search, presets, settings, and export workflows.

Suggested labels

source, configuration, documentation

Poem

🐰 A CLI takes shape with care and thought,
Discord messages now searchable—oh what joy!
With presets, exports, rate limits fought,
Better errors, typed results—the dev's new toy!
From config to handlers, the architecture's bright,
Search, save, and explore Discord treasures tonight! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 0.0.1
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch 0.0.1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/handlers/export.ts
Comment on lines +67 to +70
const outputDir = await text({
message: "Output directory:",
initialValue: defaultDir,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium handlers/export.ts:67

When the user clears the default directory value and submits an empty or whitespace-only string, outputDir.trim() returns "", causing paths like ${dir}/data.json to resolve to /data.json and attempt writing to the filesystem root. The text prompt lacks a validate function to prevent empty input, unlike the similar pattern elsewhere in the codebase.

Suggested change
const outputDir = await text({
message: "Output directory:",
initialValue: defaultDir,
});
const outputDir = await text({
message: "Output directory:",
initialValue: defaultDir,
validate: (v) => {
if (!v?.trim()) return "Output directory is required";
},
});
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/handlers/export.ts around lines 67-70:

When the user clears the default directory value and submits an empty or whitespace-only string, `outputDir.trim()` returns `""`, causing paths like `${dir}/data.json` to resolve to `/data.json` and attempt writing to the filesystem root. The `text` prompt lacks a `validate` function to prevent empty input, unlike the similar pattern elsewhere in the codebase.

Evidence trail:
src/handlers/export.ts:67-72 (text prompt without validate, trim to dir variable), src/handlers/export.ts:78-89 (mkdir and path construction with `${dir}/`), src/cli/prompts.ts:45-48 (example of validate function checking `!v?.trim()`), src/handlers/presets.ts:100-103 (another validate example for required field)

Comment thread src/cli/args-parse.ts
Comment on lines +259 to +274
const parsePresetRunAllAction = (
remaining: string[],
global: GlobalFlags
): PresetRunAllArgs => {
const restArgs = remaining.slice(2);
const names: string[] = [];
let all = false;
const flags = parseOutputFlags(restArgs);

for (const arg of restArgs) {
if (arg === "--all") {
all = true;
} else if (!arg.startsWith("-")) {
names.push(arg);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High cli/args-parse.ts:259

In parsePresetRunAllAction, flag values for --export and --output-dir are incorrectly captured as preset names. The function calls parseOutputFlags(restArgs) which reads those values, but they remain in restArgs. When iterating to collect names, any non-flag argument is added, so preset run-all --export json myPreset produces names: ["json", "myPreset"] instead of ["myPreset"]. Consider filtering out consumed flag values before collecting names, or iterate only over arguments not consumed by parseOutputFlags.

-  const restArgs = remaining.slice(2);
+  const restArgs = remaining.slice(2).filter(arg => arg !== "--export" && arg !== "--output-dir" && arg !== "--json");
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/cli/args-parse.ts around lines 259-274:

In `parsePresetRunAllAction`, flag values for `--export` and `--output-dir` are incorrectly captured as preset names. The function calls `parseOutputFlags(restArgs)` which reads those values, but they remain in `restArgs`. When iterating to collect `names`, any non-flag argument is added, so `preset run-all --export json myPreset` produces `names: ["json", "myPreset"]` instead of `["myPreset"]`. Consider filtering out consumed flag values before collecting names, or iterate only over arguments not consumed by `parseOutputFlags`.

Evidence trail:
src/cli/args-parse.ts lines 150-172: `parseOutputFlags` function reads flag values but does NOT modify the input array.
src/cli/args-parse.ts lines 259-282: `parsePresetRunAllAction` calls `parseOutputFlags(restArgs)` then iterates over the same unmodified `restArgs`, pushing any arg that doesn't start with `-` to `names`.
Line 271-272: The condition `!arg.startsWith("-")` would match flag values like "json" (from `--export json`) since they don't start with a hyphen.

Comment thread src/discord/search.ts
const data = result.value;
const pageSize = params.limit ? Math.min(params.limit, 25) : 25;

if (offset === 0 && !maxId) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical discord/search.ts:130

When params.offset is non-zero, state.totalResults is never initialized because the condition on line 130 (offset === 0 && !maxId) fails. This leaves totalResults at 0, causing state.allMessages.length >= state.totalResults on line 160 to immediately evaluate true and return "done", truncating pagination to just the first page.

-  if (offset === 0 && !maxId) {
+  if ((offset === 0 || offset === config.startOffset) && !maxId) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/discord/search.ts around line 130:

When `params.offset` is non-zero, `state.totalResults` is never initialized because the condition on line 130 (`offset === 0 && !maxId`) fails. This leaves `totalResults` at 0, causing `state.allMessages.length >= state.totalResults` on line 160 to immediately evaluate true and return `"done"`, truncating pagination to just the first page.

Evidence trail:
src/discord/search.ts lines 130-135 (condition that guards totalResults initialization), line 160 (comparison that checks completion), line 229 (state initialization with totalResults: 0), line 221 (startOffset from params.offset), lines 186-188 (fetchPartition using startOffset as initialOffset when maxId is undefined)

Comment thread src/index.ts
Comment on lines +176 to +180
const buildStateFromConfig = (
configResult: ConfigResult,
cliToken?: string
): AppState => ({
token: configResult.isOk() ? configResult.value.token : (cliToken ?? ""),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/index.ts:176

The cliToken parameter is silently ignored when configResult.isOk() is true, so the --token CLI flag has no effect when a config file exists. Compare with runInteractive at line 86, which uses resolveToken(cliArgs.token, configResult) to properly prioritize CLI arguments over config file values.

  token: configResult.isOk() ? configResult.value.token : (cliToken ?? ""),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/index.ts around lines 176-180:

The `cliToken` parameter is silently ignored when `configResult.isOk()` is true, so the `--token` CLI flag has no effect when a config file exists. Compare with `runInteractive` at line 86, which uses `resolveToken(cliArgs.token, configResult)` to properly prioritize CLI arguments over config file values.

Evidence trail:
src/index.ts lines 176-184: `buildStateFromConfig` uses `configResult.isOk() ? configResult.value.token : (cliToken ?? "")` which ignores cliToken when config exists.

src/index.ts line 86: `runInteractive` uses `resolveToken(cliArgs.token, configResult)` which properly prioritizes CLI token.

src/handlers/settings.ts lines 105-110, 138-143: `resolveToken` and `resolveTokenNonInteractive` both check `if (cliToken) { return cliToken; }` first, correctly prioritizing CLI over config.

src/index.ts lines 128, 145, 157: Other non-interactive handlers use `resolveTokenNonInteractive` which properly handles CLI token priority.

Comment thread src/presets.ts
Comment on lines +49 to +50
const presetsResult = await loadPresets();
const presets = presetsResult.isOk() ? presetsResult.value : [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/presets.ts:49

When loadPresets() returns an Err, the code silently falls back to an empty array on line 50 and then overwrites the presets file with only the new preset. If loading fails due to a transient I/O error or permission issue, all existing presets are lost. Consider propagating the error instead of proceeding with an empty array.

+      const presetsResult = await loadPresets();
+      if (!presetsResult.isOk()) {
+        return presetsResult;
+      }
+      const presets = presetsResult.value;
Also found in 1 other location(s)

src/handlers/export.ts:26

If an unrecognized format value is passed (e.g., "none" or a typo), the function silently creates an empty directory and returns success without exporting any files or indicating an error. Callers have no way to know their format was invalid.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/presets.ts around lines 49-50:

When `loadPresets()` returns an `Err`, the code silently falls back to an empty array on line 50 and then overwrites the presets file with only the new preset. If loading fails due to a transient I/O error or permission issue, all existing presets are lost. Consider propagating the error instead of proceeding with an empty array.

Evidence trail:
src/presets.ts lines 22-39 (loadPresets function - returns Ok([]) for missing file, Err for actual errors), line 50 (falls back to empty array on error: `presetsResult.isOk() ? presetsResult.value : []`), line 59 (overwrites file: `await writeTextFile(file, JSON.stringify(presets, null, 2))`). Commit: REVIEWED_COMMIT

Also found in 1 other location(s):
- src/handlers/export.ts:26 -- If an unrecognized `format` value is passed (e.g., `"none"` or a typo), the function silently creates an empty directory and returns success without exporting any files or indicating an error. Callers have no way to know their format was invalid.

Comment thread biome.jsonc
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["ultracite/biome/core"],
"files": {
"includes": ["!output"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium biome.jsonc:5

The files.includes array contains only a negation pattern ["!output"], so Biome matches no files — all linting and formatting is silently disabled. Biome requires at least one positive glob (e.g., "**") for negation patterns to work. Consider adding "**" before the negation, or move the exclusion to files.ignores instead.

-    "includes": ["!output"]
+    "includes": ["**", "!output"]
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file biome.jsonc around line 5:

The `files.includes` array contains only a negation pattern `["!output"]`, so Biome matches no files — all linting and formatting is silently disabled. Biome requires at least one positive glob (e.g., `"**"`) for negation patterns to work. Consider adding `"**"` before the negation, or move the exclusion to `files.ignores` instead.

Comment thread src/discord/client.ts
return bodyResult;
}

const retryAfter = bodyResult.value.retry_after;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High discord/client.ts:99

handle429 extracts retry_after from Discord's 429 response without validation. If the field is missing or not a number, sleep(undefined * 1000) becomes sleep(NaN), which resolves immediately and burns through all retries in a tight loop. Consider adding a fallback default like handle202 does with || 2.

-  const retryAfter = bodyResult.value.retry_after;
+  const retryAfter = bodyResult.value.retry_after || 2;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/discord/client.ts around line 99:

`handle429` extracts `retry_after` from Discord's 429 response without validation. If the field is missing or not a number, `sleep(undefined * 1000)` becomes `sleep(NaN)`, which resolves immediately and burns through all retries in a tight loop. Consider adding a fallback default like `handle202` does with `|| 2`.

Evidence trail:
src/discord/client.ts lines 99-109 (REVIEWED_COMMIT): `handle429` extracts `retry_after` directly without validation: `const retryAfter = bodyResult.value.retry_after;` and uses it as `await sleep(retryAfter * 1000);`

src/discord/client.ts line 136 (REVIEWED_COMMIT): `handle202` uses fallback: `const retryAfter = parsed.success ? parsed.data.retry_after || 2 : 2;`

src/discord/client.ts lines 25-26 (REVIEWED_COMMIT): sleep function implementation uses setTimeout which coerces NaN to 0, causing immediate resolution.

Comment thread src/discord/client.ts
Comment on lines +150 to +153
if (retryResponse.status !== 202) {
return await parseResponse(retryResponse, schema);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium discord/client.ts:150

When a retry response in the 202 polling loop has status !== 202, parseResponse is called directly without checking for 429 or error statuses first. If Discord returns a 429 or 4xx/5xx during polling, the error response body is validated against the success schema, producing a misleading ValidationError instead of the proper DiscordApiError or RateLimitExhaustedError. Consider routing non-202 statuses through the same error handling as the initial request (429 handling, error response handling, then success parsing).

-    if (retryResponse.status !== 202) {
-      return await parseResponse(retryResponse, schema);
-    }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/discord/client.ts around lines 150-153:

When a retry response in the 202 polling loop has `status !== 202`, `parseResponse` is called directly without checking for 429 or error statuses first. If Discord returns a 429 or 4xx/5xx during polling, the error response body is validated against the success schema, producing a misleading `ValidationError` instead of the proper `DiscordApiError` or `RateLimitExhaustedError`. Consider routing non-202 statuses through the same error handling as the initial request (429 handling, error response handling, then success parsing).

Evidence trail:
src/discord/client.ts lines 136-156 (handle202 function with polling loop, line 150 showing direct parseResponse call for non-202), src/discord/client.ts lines 217-248 (discordFetch function showing proper error handling flow: 429 → handle429, !response.ok → handleErrorResponse, then parseResponse), src/discord/client.ts lines 80-114 (handle429 function), src/discord/client.ts lines 161-177 (handleErrorResponse function), src/discord/client.ts lines 180-210 (parseResponse function)

Comment thread src/cli/keys.ts
Comment on lines +10 to +30
const wasRaw = process.stdin.isRaw;
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding("utf8");

const handler = (data: string) => {
// Ctrl+C — exit
if (data === "\x03") {
process.exit(0);
}
onKey(data);
};

process.stdin.on("data", handler);

return () => {
process.stdin.removeListener("data", handler);
if (!wasRaw) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low cli/keys.ts:10

The cleanup function unconditionally calls process.stdin.pause(), even if stdin was already resumed before createKeyListener was called. This leaves stdin paused after cleanup, breaking other code that depends on stdin remaining active. Consider saving the initial paused state with const wasPaused = process.stdin.isPaused() and only calling pause() during cleanup if wasPaused was true.

+  const wasPaused = process.stdin.isPaused();
   process.stdin.setRawMode(true);
   process.stdin.resume();
   process.stdin.setEncoding("utf8");
@@ -27,3 +28,3 @@
-    process.stdin.pause();
+    if (wasPaused) {
+      process.stdin.pause();
+    }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/cli/keys.ts around lines 10-30:

The cleanup function unconditionally calls `process.stdin.pause()`, even if stdin was already resumed before `createKeyListener` was called. This leaves stdin paused after cleanup, breaking other code that depends on stdin remaining active. Consider saving the initial paused state with `const wasPaused = process.stdin.isPaused()` and only calling `pause()` during cleanup if `wasPaused` was true.

Evidence trail:
src/cli/keys.ts (lines 1-31 at REVIEWED_COMMIT): Line 9 saves `wasRaw = process.stdin.isRaw`, line 11 calls `process.stdin.resume()`, cleanup function (lines 24-30) conditionally restores raw mode with `if (!wasRaw)` but unconditionally calls `process.stdin.pause()` without checking if stdin was already active before the function was called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant