Skip to content

feat: PR-5 add CLI interactive components#5

Merged
mynameistito merged 9 commits into
mainfrom
init/05-cli-interactive
Mar 27, 2026
Merged

feat: PR-5 add CLI interactive components#5
mynameistito merged 9 commits into
mainfrom
init/05-cli-interactive

Conversation

@mynameistito
Copy link
Copy Markdown
Owner

@mynameistito mynameistito commented Mar 25, 2026

Summary

  • Add @clack/prompts wrappers for interactive user input (token, search params, menus)
  • Add interactive message browser with j/k navigation and JSON toggle
  • Add TTY key listener for live view toggle during search

Stacked PR Chain

This is PR 5 of 7 — merge in order.
main ← PR1 ← PR2 ← PR3 ← PR4 ← PR5 ← PR6 ← PR7

Test plan

  • bun run typecheck passes
  • bun run check passes linting

Summary by cubic

Adds interactive CLI components for Discord message search using @clack/prompts, a keyboard-driven message browser, and safer terminal cleanup. The CLI now routes help/version correctly (including subcommands), unifies --limit to cap total results, and tightens parser validation for presets and settings.

  • New Features

    • Interactive UI: token/search prompts with strict snowflake (17–20 digits) validation and cancel; message browser with j/k/↑/↓/v/q/Esc, JSON toggle, non-TTY guard.
    • CLI: global --help and --version (works with subcommands, version read from package.json); adds --guild and --client-id; unimplemented subcommands print clear errors (exit 1).
  • Bug Fixes

    • Help/version: --help without a subcommand shows global help; search --help short-circuits; search --version prints version without requiring --guild.
    • Pagination/rate limits: use actual request page size when --limit < max; --limit caps total fetched; only capture total results from the first page; track remapped bucket keys across retries.
    • Parser/settings: reject preset run-all when mixing --all with names; validate snowflakes for settings set guild/client-id; share INTEGER_REGEX and export SNOWFLAKE_REGEX; add exhaustiveness check for unknown commands; TTY/browser rendering fixes with 24-hour timestamps and truncated embed titles/fields.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a runnable CLI entrypoint, interactive prompt flows, a TTY message browser with raw-mode key handling, extended global CLI flags, updated argument parsing and tests, refined Discord rate-limit bucket propagation, pagination accumulation fixes, and a schema limit change.

Changes

Cohort / File(s) Summary
CLI Help & Argument Parsing
src/cli/args-help.ts, src/cli/args-parse.ts, __tests__/index.test.ts
Extended GLOBAL OPTIONS with --guild/-g and --client-id/-c; adjusted parsing to prioritize --help/--version before enforcing required --guild; added early --version returns for subcommands; added tests for help/interactive routing.
CLI Entrypoint & Docs
src/index.ts, README.md
Replaced TS stub with an executable CLI (#!/usr/bin/env node), implemented top-level help/version routing, placeholder handling for unimplemented subcommands; documented current implementation status.
Interactive Prompt Utilities
src/cli/prompts.ts
Added handleCancel, promptForToken, and promptForSearchParams using @clack/prompts; collects and validates search inputs and handles cancellations.
Terminal Browser & Key Handling
src/cli/browser.ts, src/cli/keys.ts
Added browseMessages(messages) interactive viewer (preview/json modes, navigation) and createKeyListener(onKey) for raw-mode stdin key handling with proper cleanup and Ctrl+C handling.
Discord Client Rate-Limit Handling
src/discord/client.ts
updateRateLimitState now returns the active/remapped bucket key; callers reassign and use the remapped key for subsequent waits/retries (including 429/202 paths).
Search Pagination & Schemas
src/discord/search.ts, src/discord/schemas.ts
Set state.totalResults only on first fetch to avoid overwrites; changed accumulation to explicit per-item push; removed MAX_PAGE_SIZE upper bound from SearchParamsSchema.limit.
CLI Utilities
src/cli/utils.ts
parseCommaSeparated now trims input and returns undefined for empty input; signature changed to return `string[]

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as CLI Router
    participant Prompts as Prompt System
    participant Terminal as Terminal/TTY
    participant Discord as Discord API
    participant Browser as Message Browser

    User->>CLI: invoke CLI (interactive / search / --help / --version)
    CLI->>Prompts: promptForToken / promptForSearchParams
    Prompts->>Terminal: render prompts
    Terminal->>Prompts: user input / cancel
    Prompts->>CLI: return SearchParams
    CLI->>Discord: discordFetch (paginated)
    Discord->>Discord: response + X-RateLimit-Bucket -> remapped key
    Discord->>CLI: return messages (pages)
    CLI->>Browser: browseMessages(messages)
    Browser->>Terminal: render preview / json
    Terminal->>Browser: key events (j/k/v/q, arrows)
    Browser->>Browser: update view/state
    Browser->>Terminal: re-render or exit
    Browser->>User: cleanup & resolve
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I tap the keys and watch prompts bloom,
Messages hop into a bright, small room,
j and k — I skip and softly peep,
JSON glows or previews I keep,
A rabbit cheers: browse on, little CLI!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: PR-5 add CLI interactive components' clearly summarizes the main addition of interactive CLI features (prompts, browser, key listener) across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description clearly relates to the changeset, detailing interactive CLI components, message browser, and terminal handling features that are reflected in the files changed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 init/05-cli-interactive

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.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/cli/keys.ts">

<violation number="1" location="src/cli/keys.ts:18">
P2: Restore stdin state before exiting on Ctrl+C so the terminal isn’t left in raw mode.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/cli/keys.ts
@mynameistito mynameistito changed the title feat: add CLI interactive components feat: PR-5 add CLI interactive components Mar 25, 2026
@mynameistito mynameistito changed the base branch from init/04-cli-parsing to main March 26, 2026 04:10
@mynameistito
Copy link
Copy Markdown
Owner Author

@CodeRabbit, please do a deep anaylsis on this PR and tell me any issues that need to be resolved even nit picky ones.

@mynameistito
Copy link
Copy Markdown
Owner Author

@greptile @cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented Mar 27, 2026

@greptile @cubic-dev-ai

@mynameistito I have started the AI code review. It will take a few minutes to complete.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me gather the full PR details and file contents for a thorough analysis.


🏁 Script executed:

#!/bin/bash
# Get the PR diff and changed files
gh pr view 5 --repo mynameistito/discord-search --json files,body,title,additions,deletions,changedFiles

echo "=== PR DIFF ==="
gh pr diff 5 --repo mynameistito/discord-search

Length of output: 58004


@mynameistito Here is a thorough analysis of all 14 files. Issues are grouped by severity.


🔴 Bugs / Correctness Issues

1. src/cli/args-types.tsParsedArgsSchema discriminated union will fail at runtime

z.discriminatedUnion("command", [...]) requires unique values for the discriminator key. You have five schemas with command: "preset" and three with command: "settings". Zod v3 throws at schema instantiation time when it detects duplicate discriminator values.

// ❌ This will throw: duplicate discriminator values "preset" and "settings"
export const ParsedArgsSchema = z.discriminatedUnion("command", [
  InteractiveArgsSchema,
  SearchArgsSchema,
  PresetListArgsSchema,   // "preset"
  PresetRunArgsSchema,    // "preset"
  PresetRunAllArgsSchema, // "preset"
  PresetSaveArgsSchema,   // "preset"
  PresetDeleteArgsSchema, // "preset"
  SettingsShowArgsSchema, // "settings"
  SettingsSetArgsSchema,  // "settings"
  SettingsInviteArgsSchema, // "settings"
]);

Fix: Use z.union instead, or use a nested discriminated union (discriminate first on command, then on action):

export const ParsedArgsSchema = z.union([
  InteractiveArgsSchema,
  SearchArgsSchema,
  PresetListArgsSchema,
  PresetRunArgsSchema,
  PresetRunAllArgsSchema,
  PresetSaveArgsSchema,
  PresetDeleteArgsSchema,
  SettingsShowArgsSchema,
  SettingsSetArgsSchema,
  SettingsInviteArgsSchema,
]);

2. src/discord/client.tshandle202 retry loop doesn't handle 429 on retries

Inside handle202, the retry loop fetches again and calls parseResponse directly if status !== 202. If a retry returns a 429 or another error code, it falls straight into parseResponse, which will fail schema validation instead of returning a proper error.

// In handle202 retry loop — missing status checks
if (retryResponse.status !== 202) {
  return await parseResponse(retryResponse, schema); // ❌ called even for 429, 401, 500, etc.
}

Fix: Add proper status handling in the retry:

if (retryResponse.status === 429) {
  // handle or return RateLimitExhaustedError
}
if (!retryResponse.ok) {
  return await handleErrorResponse(retryResponse);
}
if (retryResponse.status !== 202) {
  return await parseResponse(retryResponse, schema);
}

3. src/cli/keys.ts — Ctrl+C exits without invoking cleanup(), leaving terminal in raw mode

const handler = (data: string) => {
  if (data === "\x03") {
    process.exit(0); // ❌ raw mode never restored, listener never removed
  }
  onKey(data);
};

The caller's cleanup function (which restores isRaw and pauses stdin) is never called before exit, so the terminal is left in raw mode on some shells.

Fix:

const handler = (data: string) => {
  if (data === "\x03") {
    cleanup(); // restore terminal state first
    process.exit(0);
  }
  onKey(data);
};

4. src/cli/args-parse.ts — Flags missing their values are silently ignored

In applySearchParamFlag, when next is undefined or starts with -, the COMMA_SEP_FLAGS and STRING_FLAGS branches are skipped silently:

const commaSepKey = COMMA_SEP_FLAGS[arg];
if (commaSepKey && next) { // ← silently no-ops if next is undefined
  params[commaSepKey] = parseCommaSeparated(next);
  return 2;
}

So discord-search search -g 123 --content (missing value) silently produces a search with no content filter, rather than an error.

Fix: Return an error when a value flag has no value:

const commaSepKey = COMMA_SEP_FLAGS[arg];
if (commaSepKey) {
  if (!next || next.startsWith("-")) {
    return exitWithError(`Flag ${arg} requires a value`, "search");
  }
  params[commaSepKey] = parseCommaSeparated(next);
  return 2;
}

5. src/cli/args-parse.tsparseInt is not validated for NaN

if (arg === "--slop" && next) {
  params.slop = Number.parseInt(next, 10); // ❌ NaN if next is "abc"
  return 2;
}

Same applies to --offset and --limit. Passing a non-numeric value will silently set NaN in the params.

Fix:

if (arg === "--slop" && next) {
  const val = Number.parseInt(next, 10);
  if (Number.isNaN(val)) {
    return exitWithError(`--slop requires a numeric value, got: "${next}"`, "search");
  }
  params.slop = val;
  return 2;
}

🟡 Design Issues

6. src/types.tsAppState is a duplicate of Config

AppState in src/types.ts is structurally identical to Config in src/config.ts:

// src/types.ts
export type AppState = {
  clientId: string | undefined;
  defaultGuildId: string | undefined;
  token: string;
};

// src/config.ts
export type Config = {
  clientId: string | undefined;
  defaultGuildId: string | undefined;
  token: string;
};

Fix: Delete AppState and use Config everywhere (or vice-versa). Having two identical types invites drift.


7. src/discord/client.ts — Module-level rateLimitState singleton is not safe for concurrent use

rateLimitState is shared across all calls to discordFetch. This is fine for sequential pagination, but if two requests are ever in-flight simultaneously (or if the rate limit scope differs per endpoint), they'll incorrectly share state. Worth adding a comment at minimum, or scoping the state per request chain.


8. src/paths.tsOUTPUT_DIR is defined but never created

ensureAppDir() creates APP_DIR and .gitignore, but never creates OUTPUT_DIR. Any code that tries to write exports to OUTPUT_DIR will fail if it hasn't been manually created.

export const OUTPUT_DIR = join(APP_DIR, "output"); // defined but never mkdir'd

Fix:

export const ensureAppDir = async (): Promise<void> => {
  await mkdir(APP_DIR, { recursive: true });
  await mkdir(OUTPUT_DIR, { recursive: true }); // add this
  // ...
};

9. src/cli/args-help.ts vs src/cli/args-parse.ts--guild and --client-id are undocumented in GLOBAL_OPTIONS

parseGlobalFlags handles --guild/-g and --client-id/-c as global flags, but GLOBAL_OPTIONS in the help text only documents --help, --version, and --token. A user running discord-search --help will not see --guild and --client-id listed there.


10. src/discord/search.tstotalResults not updated on subsequent snowflake partitions

In fetchPage:

if (offset === 0 && !maxId) {
  state.totalResults = ...;
}

When pagination moves to a new partition (new maxId), state.totalResults is never re-evaluated. Progress reporting may show incorrect totals on large result sets that exceed 9,975 results.


🔵 Nitpicks

11. src/cli/prompts.tshandleCancel return type predicate is misleading

export const handleCancel = (value: unknown): value is symbol => {
  if (isCancel(value)) {
    cancel("Search cancelled.");
    process.exit(0);
  }
  return false; // ← always false; the `is symbol` predicate never resolves to true for callers
};

TypeScript infers callers can narrow with this predicate, but it's never true. The return type is effectively false. Consider void or just removing the predicate since callers never use the return value.


12. src/cli/keys.tsstdin.setEncoding("utf8") is set but never restored

process.stdin.setEncoding("utf8");
// ...
return () => {
  process.stdin.removeListener("data", handler);
  if (!wasRaw) {
    process.stdin.setRawMode(false);
  }
  process.stdin.pause(); // ← encoding never restored
};

If the caller had a different encoding (e.g., "binary"), cleanup leaves stdin with "utf8". Consider saving and restoring the previous encoding.


13. src/cli/browser.tsbrowseMessages([]) resolves silently with no user feedback

if (messages.length === 0) {
  resolve();
  return;
}

The user gets no indication that there are no messages. Consider printing a short message before resolving.


14. src/cli/prompts.ts"none" sentinel leaks into the authorType select options

Using "none" as a value in the select options and filtering it out afterward is a minor code smell. Prefer null or an explicit undefined sentinel typed at the prompt layer, or use a confirm first.


15. src/cli/prompts.ts — No validation on token format in promptForToken

A blank token or obviously malformed value would pass through since password() has no validate callback. Consider adding a basic non-empty check:

const token = await password({
  message: "Enter your Discord Bot Token:",
  validate: (v) => (!v?.trim() ? "Token cannot be empty" : undefined),
});

Summary Table

# File Severity Issue
1 args-types.ts 🔴 Bug Duplicate discriminator values crash ParsedArgsSchema
2 discord/client.ts 🔴 Bug handle202 retries don't handle non-202/non-OK responses
3 args-parse.ts 🔴 Bug Flags without values silently no-op
4 args-parse.ts 🔴 Bug parseInt NaN not validated for --slop/--offset/--limit
5 keys.ts 🔴 Bug Ctrl+C exits without calling cleanup() → raw mode left on
6 types.ts 🟡 Design AppState is a duplicate of Config
7 discord/client.ts 🟡 Design Module-level rate limit singleton unsafe for concurrent use
8 paths.ts 🟡 Design OUTPUT_DIR defined but never created by ensureAppDir
9 args-help.ts 🟡 Design --guild / --client-id global flags not in help text
10 discord/search.ts 🟡 Design totalResults stale on snowflake partition boundaries
11 prompts.ts 🔵 Nit handleCancel type predicate always false
12 keys.ts 🔵 Nit stdin.setEncoding not restored on cleanup
13 browser.ts 🔵 Nit Empty messages array resolves silently
14 prompts.ts 🔵 Nit "none" sentinel in authorType select
15 prompts.ts 🔵 Nit No empty-check validation in promptForToken

🧠 Learnings used
Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/handlers/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:43.140Z
Learning: Applies to src/handlers/**/handlers/**/*.ts : Use clack/prompts with handleCancel wrapper for user interactions

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/handlers/AGENTS.md:0-0
Timestamp: 2026-03-26T14:56:15.999Z
Learning: Applies to src/handlers/**/*.ts : Use clack/prompts with handleCancel wrapper for user interactions in handlers

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

5 issues found across 14 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/discord/client.ts">

<violation number="1" location="src/discord/client.ts:150">
P1: The 202-retry loop doesn't handle error responses (429, 4xx, 5xx). If a retry gets a 429 or server error, it will be fed to `parseResponse` and fail with a misleading `ValidationError` instead of the correct error type. Mirror the status handling from `discordFetch` (check for 429, non-OK) before calling `parseResponse`.</violation>
</file>

<file name="src/config.ts">

<violation number="1" location="src/config.ts:67">
P1: Settings are written with the bot token but without enforcing restrictive file permissions, which can expose credentials on multi-user systems.</violation>
</file>

<file name="src/cli/browser.ts">

<violation number="1" location="src/cli/browser.ts:102">
P1: Handle non-TTY mode before starting the interactive browser; otherwise this promise can hang forever when key input is unavailable.</violation>
</file>

<file name="src/discord/search.ts">

<violation number="1" location="src/discord/search.ts:130">
P1: Non-zero `offset` causes pagination to stop after the first page because `totalResults` is never initialized.</violation>
</file>

<file name="src/cli/args-parse.ts">

<violation number="1" location="src/cli/args-parse.ts:258">
P2: `preset run-all --export json` will incorrectly treat `json` (or `--output-dir` values) as preset names because the loop doesn’t skip flag values. Skip the next arg when consuming output flags to avoid corrupting `names`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/discord/client.ts Outdated
Comment thread src/config.ts
Comment thread src/cli/browser.ts Outdated
Comment thread src/discord/search.ts Outdated
Comment thread src/cli/args-parse.ts Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 27, 2026

Greptile Summary

This PR wires up a working CLI entrypoint (help/version), adds @clack/prompts-based interactive search prompts, a keyboard-driven message browser, and a safe TTY key listener. It also fixes help routing for --help with/without a subcommand, corrects rate-limit bucket-key tracking across retries, and unifies --limit to represent a total-result cap with proper pagination arithmetic.\n\nKey changes:\n- src/cli/prompts.ts — new interactive prompts for token, guild, filters, sort, and limit using @clack/prompts; cancel is handled uniformly via handleCancel\n- src/cli/browser.ts — new j/k/↑/↓/v/q message browser backed by createKeyListener; has a hang bug in non-TTY environments (see inline comment)\n- src/cli/keys.ts — new raw-mode TTY listener that saves/restores raw mode and encoding on cleanup; Ctrl+C exits cleanly\n- src/discord/client.tsupdateRateLimitState now returns the remapped bucket key so both discordFetch and handle202 track the correct key across retries\n- src/discord/schemas.ts — removes the .max(MAX_PAGE_SIZE) upper-bound from limit since it now represents a total-fetch cap, not a page size\n- src/cli/args-parse.ts + src/cli/args-help.ts--help without a subcommand routes to global help; search --help short-circuits before the required-guild guard\n- src/index.ts — stub entrypoint replaced with real help/version dispatch; unimplemented commands exit with code 1\n- __tests__/index.test.ts — placeholder replaced with real unit tests covering the arg-parse routing changes

Confidence Score: 4/5

Safe to merge after addressing the non-TTY hang in browseMessages — the rest of the changes are well-structured and correct.

One P1 issue: browseMessages returns a Promise that never resolves when stdin is not a TTY and messages are non-empty, which would hang any caller. The params.limit / maxMessages semantic mismatch is a P2 design concern that should be resolved before the search command is wired up. All other changes (rate-limit tracking, help routing, key listener, prompts) are clean and correct.

src/cli/browser.ts — non-TTY hang must be fixed before the browser is integrated; src/cli/prompts.tsparams.limit semantics should be clarified against searchAllMessages's maxMessages parameter.

Important Files Changed

Filename Overview
src/cli/browser.ts New interactive message browser with j/k/v/q navigation; contains a logic bug where the returned Promise never resolves when stdin is not a TTY and messages are non-empty.
src/cli/keys.ts New TTY key-listener utility that correctly saves/restores raw mode and encoding on cleanup, and handles Ctrl+C gracefully.
src/cli/prompts.ts New @clack/prompts wrappers for interactive search input; params.limit semantics are ambiguous — the prompt implies total-result cap but the field is used as page size downstream.
src/discord/client.ts Rate-limit bucket key now tracked and propagated correctly across retries — updateRateLimitState returns the active key and both discordFetch and handle202 use it.
src/cli/args-parse.ts Fixes help routing: --help without a subcommand now returns global help; search --help short-circuits before the required-guild check. Logic is clean and tests cover the new paths.
src/index.ts Working CLI entrypoint wired up with help/version output; all unimplemented commands exit with code 1 and a clear message. Version string is hardcoded.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["process.argv"] --> B["parseArgs()"]
    B --> C{command?}
    C -- "--help / no subcommand + --help" --> D["printHelp()"]
    C -- "--version" --> E["printVersion()"]
    C -- "interactive (no subcommand)" --> F["[stub] exit 1"]
    C -- "search --guild ..." --> G["[stub] exit 1"]
    C -- "preset / settings" --> H["[stub] exit 1"]

    subgraph "Interactive flow (future)"
        I["promptForToken()"] --> J["promptForSearchParams()"]
        J --> K["searchAllMessages()"]
        K --> L["browseMessages()"]
        L --> M{stdin TTY?}
        M -- "yes" --> N["createKeyListener()\nj/k/v/q navigation"]
        M -- "no" --> O["⚠️ Promise never resolves"]
    end
Loading

Reviews (2): Last reviewed commit: "fix: correct NOTE admonition syntax, tra..." | Re-trigger Greptile

Comment thread src/types.ts
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 43 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/index.ts">

<violation number="1" location="src/index.ts:8">
P1: The configured CLI entrypoint is currently a no-op, so the built `discord-search` command does not run the application.</violation>
</file>

<file name="src/discord/search.ts">

<violation number="1" location="src/discord/search.ts:168">
P1: Updating `totalResults` on every page breaks snowflake partitioning. When a subsequent partition adds a `max_id` filter, Discord returns a smaller `total_results` for that filtered scope. Overwriting `state.totalResults` with this smaller value causes the `allMessages.length >= totalResults` check to fire immediately, terminating pagination early and losing messages beyond the first partition.

Only capture `totalResults` from the initial unfiltered response, or use a monotonically non-decreasing update to prevent the total from shrinking across partitions.</violation>
</file>

<file name="src/discord/client.ts">

<violation number="1" location="src/discord/client.ts:334">
P1: Stale `bucketKey` after Discord remaps the rate-limit bucket. `updateRateLimitState` deletes the old key from `rateLimitBuckets` and stores state under a new `discord:*` key, but this `const` never updates. On 429 retries (and inside `handle202`), `getBucketState` creates a fresh state instead of using the remapped one, silently losing rate-limit tracking and risking additional 429s.</violation>
</file>

<file name="src/cli/keys.ts">

<violation number="1" location="src/cli/keys.ts:34">
P2: Cleanup does not restore stdin encoding when the original encoding was null, so this listener can leave stdin permanently in UTF-8 mode.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/index.ts Outdated
Comment thread src/discord/search.ts Outdated
Comment thread src/discord/client.ts Outdated
Comment thread src/cli/keys.ts Outdated
Add @clack/prompts wrappers for user input, interactive message
browser with keyboard navigation, and TTY key listener for live
view toggle.
- Fix Ctrl+C exiting without restoring terminal state (keys.ts)
- Restore stdin encoding on cleanup (keys.ts)
- Document --guild and --client-id in global help options (args-help.ts)
- Update totalResults on every page to fix stale progress on partitions (search.ts)
- Fix handleCancel misleading type predicate (prompts.ts)
- Replace "none" sentinel with confirm-first flow for author type (prompts.ts)
- Add non-empty validation to token prompt (prompts.ts)
- Show feedback when browse has no messages (browser.ts)
- Add doc comment for shared rate limit state (client.ts)
… stdin cleanup

- Replace no-op index.ts with working CLI entrypoint that parses args and dispatches commands
- Only capture totalResults from first unfiltered response to prevent early pagination termination
- Return remapped bucket key from updateRateLimitState so callers track the active key across retries
- Use strict null check for stdin encoding restore to avoid leaving stdin in UTF-8 mode
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/index.ts">

<violation number="1" location="src/index.ts:30">
P2: Handle `--help` for interactive/no-command mode before returning the unimplemented interactive error, otherwise `discord-search --help` fails.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/index.ts
Comment on lines +30 to +35
if (parsed.command === "interactive") {
process.stderr.write(
"Interactive mode is not yet implemented. Use 'discord-search search --help' for CLI usage.\n"
);
process.exit(1);
}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 27, 2026

Choose a reason for hiding this comment

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

P2: Handle --help for interactive/no-command mode before returning the unimplemented interactive error, otherwise discord-search --help fails.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/index.ts, line 30:

<comment>Handle `--help` for interactive/no-command mode before returning the unimplemented interactive error, otherwise `discord-search --help` fails.</comment>

<file context>
@@ -1,8 +1,55 @@
+    process.exit(0);
+  }
+
+  if (parsed.command === "interactive") {
+    process.stderr.write(
+      "Interactive mode is not yet implemented. Use 'discord-search search --help' for CLI usage.\n"
</file context>
Suggested change
if (parsed.command === "interactive") {
process.stderr.write(
"Interactive mode is not yet implemented. Use 'discord-search search --help' for CLI usage.\n"
);
process.exit(1);
}
if (parsed.command === "interactive") {
if (parsed.help) {
process.stdout.write(`${HELP_TEXT}\n`);
process.exit(0);
}
process.stderr.write(
"Interactive mode is not yet implemented. Use 'discord-search search --help' for CLI usage.\n"
);
process.exit(1);
}
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/prompts.ts`:
- Around line 138-158: The prompt validators for limitInput and offsetInput
currently only check for NaN; update the validate callbacks (the inline validate
passed to text(...) for limitInput and offsetInput) to parse the trimmed value
to an integer and enforce the same bounds used by SearchParamsSchema: check that
limit ≤ MAX_PAGE_SIZE and offset ≤ MAX_OFFSET (and that both are ≥ 0), returning
clear error messages like "Must be between 0 and MAX_PAGE_SIZE" or "Must be
between 0 and MAX_OFFSET" when out of range; keep allowing empty input (to mean
default/all) and still call handleCancel(limitInput)/handleCancel(offsetInput)
as before.

In `@src/index.ts`:
- Around line 17-53: The run() function currently handles known parsed.command
values but will silently return if ParsedArgs gains new command types; add an
exhaustive check after the existing if blocks that handles any unknown
parsed.command (use a default branch or an assertUnreachable-style helper) to
log an error and exit non-zero or throw; reference parsed.command and run() and,
if you add a helper, name it assertUnreachable(value: never) to make TypeScript
catch unhandled command variants at compile time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: febe38f9-78cf-41ee-850f-f08e0f7c4e90

📥 Commits

Reviewing files that changed from the base of the PR and between 1acf86b and f593ff4.

📒 Files selected for processing (7)
  • src/cli/args-help.ts
  • src/cli/browser.ts
  • src/cli/keys.ts
  • src/cli/prompts.ts
  • src/discord/client.ts
  • src/discord/search.ts
  • src/index.ts

Comment thread src/cli/prompts.ts
Comment thread src/index.ts
- Fix global help behavior: --help without subcommand now shows help instead of falling into interactive mode
- Update parseSearchCommand to short-circuit to help when global.help is true regardless of --guild presence
- Add parser tests for --help and search --help scenarios
- Unify --limit contract: changed from page-size limit to total fetch cap
- Update README with correct usage commands and implementation status
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests labels Mar 27, 2026
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="README.md">

<violation number="1" location="README.md:70">
P3: Fix the NOTE admonition syntax by closing the bracket so it renders correctly.</violation>
</file>

<file name="src/discord/search.ts">

<violation number="1" location="src/discord/search.ts:164">
P1: `fetchPage` now compares results against `MAX_PAGE_SIZE` even when the API request used a smaller `params.limit`, causing premature `break` in pagination.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/discord/search.ts Outdated
Comment thread README.md Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 70: Replace the malformed alert token "[!NOTE>" with the correct
"[!NOTE]" in the README so the note block renders properly; locate occurrences
of the alert start marker (the string "[!NOTE>") and change the trailing ">" to
a closing bracket "]" (i.e., update "[!NOTE>" -> "[!NOTE]") wherever it appears.
- Line 72: Remove the trailing whitespace on the blank line in README.md (the
line the reviewer flagged) by deleting the extra space character so the line
contains no trailing space; simply save the file with that trailing space
removed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a67d5fc-b29a-4830-8361-dbea7d79535b

📥 Commits

Reviewing files that changed from the base of the PR and between f593ff4 and ae8e5b4.

📒 Files selected for processing (5)
  • README.md
  • __tests__/index.test.ts
  • src/cli/args-parse.ts
  • src/discord/schemas.ts
  • src/discord/search.ts

Comment thread README.md Outdated
Comment thread README.md Outdated
…ion pageSize mismatch

- README.md: Fix [!NOTE> -> [!NOTE] so admonition renders correctly
- README.md: Remove trailing whitespace on blank line
- search.ts: Use actual request limit instead of MAX_PAGE_SIZE in fetchPage
  to prevent premature pagination break when params.limit < MAX_PAGE_SIZE

Co-authored-by: Amp <amp@ampcode.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/discord/search.ts (1)

168-175: ⚠️ Potential issue | 🟠 Major

Avoid hard-terminating pagination on total_results; it is not reliable for large searches.

The code correctly avoids overwriting state.totalResults per partition, but it is still used as a stop condition at lines 207 and 322. Discord's API documentation states that total_results "may not be accurate" and "may return slightly fewer results than the limit specified." For result sets exceeding 10,000, total_results can be capped or scoped, particularly in dynamic guilds or with old messages. This directly breaks snowflake pagination logic, which requires resilience beyond a single unreliable count.

Terminate on maxMessages (if specified), empty page, or maxId non-advancement instead. Use totalResults for progress display only, not pagination control.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/discord/search.ts` around lines 168 - 175, The pagination must not
hard-terminate based on state.totalResults (which is unreliable); update the
loop/stop logic so that state.totalResults and data.total_results are only used
for progress display, and pagination termination relies on: hitting maxMessages
(if provided), receiving an empty page (no messages returned), or detecting no
advancement of the pagination cursor (max_id / maxId not changing between
iterations). Remove or disable any checks that use state.totalResults as a stop
condition (references near the existing checks that currently check
state.totalResults at the later stop points) and ensure the loop instead checks
message array length, compares previous and current max_id, and enforces
maxMessages for final termination.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/discord/search.ts`:
- Around line 168-175: The pagination must not hard-terminate based on
state.totalResults (which is unreliable); update the loop/stop logic so that
state.totalResults and data.total_results are only used for progress display,
and pagination termination relies on: hitting maxMessages (if provided),
receiving an empty page (no messages returned), or detecting no advancement of
the pagination cursor (max_id / maxId not changing between iterations). Remove
or disable any checks that use state.totalResults as a stop condition
(references near the existing checks that currently check state.totalResults at
the later stop points) and ensure the loop instead checks message array length,
compares previous and current max_id, and enforces maxMessages for final
termination.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e3f7668b-7a01-4164-a3f6-8bc1b5990149

📥 Commits

Reviewing files that changed from the base of the PR and between ae8e5b4 and 2677136.

📒 Files selected for processing (2)
  • README.md
  • src/discord/search.ts

@mynameistito
Copy link
Copy Markdown
Owner Author

@grep

@mynameistito
Copy link
Copy Markdown
Owner Author

@greptile

@mynameistito
Copy link
Copy Markdown
Owner Author

@coderabbitai any more issues that need to be resolved, even really nit picky ones.

Comment thread src/cli/browser.ts
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me do a thorough read of all the changed files to find any additional issues.


🏁 Script executed:

#!/bin/bash
echo "=== src/cli/browser.ts ==="
cat src/cli/browser.ts

echo "=== src/cli/keys.ts ==="
cat src/cli/keys.ts

echo "=== src/cli/prompts.ts ==="
cat src/cli/prompts.ts

echo "=== src/cli/args-parse.ts ==="
cat src/cli/args-parse.ts

echo "=== src/cli/args-types.ts ==="
cat src/cli/args-types.ts

echo "=== src/cli/args-help.ts ==="
cat src/cli/args-help.ts

echo "=== src/discord/client.ts ==="
cat src/discord/client.ts

echo "=== src/discord/search.ts ==="
cat src/discord/search.ts

echo "=== src/discord/schemas.ts ==="
cat src/discord/schemas.ts

echo "=== src/index.ts ==="
cat src/index.ts

echo "=== src/types.ts ==="
cat src/types.ts

echo "=== src/paths.ts ==="
cat src/paths.ts

echo "=== __tests__/index.test.ts ==="
cat __tests__/index.test.ts

echo "=== README.md ==="
cat README.md

echo "=== package.json ==="
cat package.json

echo "=== src/discord/types.ts ==="
cat src/discord/types.ts 2>/dev/null || echo "NOT FOUND"

echo "=== src/config.ts ==="
cat src/config.ts 2>/dev/null || echo "NOT FOUND"

echo "=== All TS files in src ==="
fd -e ts . src --exec echo {}

Length of output: 77488


Let me also verify the utils.ts file to confirm the duplication issue.


🏁 Script executed:

#!/bin/bash
cat src/cli/utils.ts
cat src/cli/ansi.ts
echo "=== errors.ts ==="
cat src/errors.ts

Length of output: 1366


@mynameistito Here are the additional issues I found — including some that are genuinely impactful — after a thorough read of all files.


🔴 New Bugs

16. src/cli/browser.tsANSI_CLEAR_SCREEN without ANSI_CURSOR_HOME causes garbled rendering

\x1b[2J (ANSI_CLEAR_SCREEN) erases the visible screen contents but does not move the cursor. Text written after it appears at wherever the cursor last was, not at the top-left. ansi.ts already exports ANSI_CURSOR_HOME (\x1b[H) for exactly this purpose, but browser.ts never imports or uses it.

// src/cli/ansi.ts already exports this — just use it
import { ANSI_CLEAR_SCREEN, ANSI_CURSOR_HOME, ... } from "@/cli/ansi.ts";

// In renderMessageView and in the quit cleanup:
process.stdout.write(ANSI_CLEAR_SCREEN + ANSI_CURSOR_HOME); // ✅ reset cursor to top-left

17. src/index.ts--version alongside a subcommand triggers a parse error instead of printing version

parseGlobalFlags correctly extracts --version from any position (including after a subcommand). But for discord-search search --version, parseSearchCommand calls exitWithError("Missing required --guild/-g") before returning, so run() never reaches the parsed.version check. The --version check needs to happen during parsing, or the subcommand parsers must short-circuit on global.version the same way they do on global.help.

// In parseSearchCommand — mirror the help short-circuit pattern:
if (global.version) {
  return parseWithError<SearchArgs>( // or return a dedicated VersionArgs
    { command: "search", version: true, help: false, ... },
    "search"
  );
}

🟡 New Design Issues

18. src/cli/utils.ts vs src/cli/prompts.tsparseCommaSeparated duplicated with divergent signatures

src/cli/utils.ts exports:

export const parseCommaSeparated = (input: string): string[] => ...
// → always returns string[], never undefined

src/cli/prompts.ts exports its own version:

export const parseCommaSeparated = (value: string): string[] | undefined => ...
// → returns undefined for empty/whitespace input

args-parse.ts imports from utils.ts; prompts.ts uses its own copy. The two implementations have different contracts, which will cause confusion and drift. Consolidate into utils.ts with the | undefined return type (the safer interface) and import from there in prompts.ts.


19. src/index.tsprintVersion() hardcodes "0.0.0" rather than reading from package.json

const printVersion = () => {
  process.stdout.write("discord-search 0.0.0\n"); // ❌ will never update
};

With Bun and tsdown, you can inject the version at build time or import it directly:

import { version } from "../../package.json";
// or use: import.meta.env.npm_package_version
process.stdout.write(`discord-search ${version}\n`);

20. src/index.tsrun() has no exhaustiveness check for unhandled commands

After all the if (parsed.command === ...) branches, the function silently returns undefined if a new command value is added to the schema but forgotten in run(). Add a never-guard at the end:

// After all the if-blocks:
const _exhaustive: never = parsed.command;
process.stderr.write(`Unhandled command: ${_exhaustive}\n`);
process.exit(1);

21. src/cli/prompts.tsparseCommaSeparated(",,,") returns [] (empty array), which is truthy

const authorIdList = parseCommaSeparated(authorIds as string);
if (authorIdList) { // ← [] is truthy!
  params.authorId = authorIdList; // sets params.authorId = [] → empty array filter
}

A user who leaves the field blank or accidentally enters only commas will get params.authorId = [] set, which is different from "no filter" (undefined). Zod will accept an empty array. Fix by checking .length:

if (authorIdList && authorIdList.length > 0) {
  params.authorId = authorIdList;
}

22. src/cli/prompts.tsauthorType uses a single select but the schema accepts an array

SearchParamsSchema.authorType is z.array(z.enum(["user", "bot", "webhook"])), but the prompt uses select (pick one). If a user needs to search for both users and bots, they can't express that through the prompt. Consider multiselect for consistency with the schema:

const selected = await multiselect({
  message: "Author type:",
  options: [
    { value: "user", label: "Users" },
    { value: "bot", label: "Bots" },
    { value: "webhook", label: "Webhooks" },
  ],
  required: false,
});
handleCancel(selected);
if ((selected as string[]).length > 0) {
  params.authorType = selected as ("user" | "bot" | "webhook")[];
}

🔵 New Nits

23. src/discord/schemas.ts.max(20) on snowflakeSchema is redundant

The regex /^\d{17,20}$/ already rejects strings longer than 20 digits. The .max(20) check can never fire because the regex guard runs first and is stricter. Remove it to avoid confusing duplicate error messages:

const snowflakeSchema = z
  .string()
  .regex(SNOWFLAKE_REGEX, { message: "Invalid Discord ID: must be a 17-20 digit numeric snowflake" });
  // .max(20) ← redundant, regex already enforces this

24. src/cli/browser.ts — Embed title and field name/value are not truncated

Content is truncated at 100 chars and descriptions at 200 chars, but embed titles (up to 256 chars in Discord) and field names/values (up to 256/1024 chars) are rendered raw. Long field values will break the terminal layout:

if (embed.title) {
  const title = embed.title.length > 80 ? `${embed.title.slice(0, 80)}...` : embed.title;
  process.stdout.write(`  Embed: ${title}\n`);
}
// Similarly for field.name and field.value

25. src/cli/browser.tsbrowseMessages Promise never rejects; hangs if stdin is disrupted

If process.stdout.write throws or stdin emits an error event, the new Promise((resolve) => {...}) returned by browseMessages hangs forever because only resolve is called, never reject. Consider wrapping writes in try/catch and calling reject(err).


26. src/cli/browser.ts — Escape key is undocumented in the navigation hint

The key hint shows [q] back, but the code also exits on \x1b (Escape). Update the hint:

`${ANSI_DIM}[j/↓] next  [k/↑] prev  [v] toggle JSON  [q/Esc] back${ANSI_RESET}\n`

27. src/discord/search.tsArray.prototype.push.apply is non-idiomatic

Array.prototype.push.apply(state.allMessages, pageMessages); // non-idiomatic

Prefer the modern spread syntax which is clearer and equally performant for typical message set sizes:

state.allMessages.push(...pageMessages);

28. README.md — References .env.example that doesn't appear to exist in the repo

cp .env.example .env  # ← no .env.example file found

Either add an .env.example file with the expected variables (DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, DISCORD_CLIENT_ID), or remove/update this instruction.


29. src/cli/browser.tsmsg.timestamp is displayed as a raw ISO-8601 string

Timestamps like 2023-01-15T12:34:56.789000+00:00 are hard to read at a glance. Consider formatting with new Date(msg.timestamp).toLocaleString() or a shorter form for a better UX.


Summary of New Issues

# File Severity Issue
16 browser.ts 🔴 Bug ANSI_CLEAR_SCREEN without ANSI_CURSOR_HOME → garbled rendering
17 index.ts 🔴 Bug --version with subcommand triggers guild error instead of printing version
18 utils.ts/prompts.ts 🟡 Design parseCommaSeparated duplicated with divergent return types
19 index.ts 🟡 Design printVersion() hardcodes "0.0.0" instead of reading from package.json
20 index.ts 🟡 Design run() missing exhaustiveness check for unhandled commands
21 prompts.ts 🟡 Design parseCommaSeparated(",,,")[] is truthy; sets empty-array filter
22 prompts.ts 🟡 Design authorType uses select (one) but schema supports an array
23 schemas.ts 🔵 Nit .max(20) on snowflakeSchema is redundant after regex
24 browser.ts 🔵 Nit Embed title, field name/value not truncated — can break terminal layout
25 browser.ts 🔵 Nit browseMessages Promise never rejects — hangs if stdout fails
26 browser.ts 🔵 Nit Escape key undocumented in nav hint
27 search.ts 🔵 Nit Array.prototype.push.apply — prefer push(...pageMessages)
28 README.md 🔵 Nit References .env.example that doesn't appear to exist
29 browser.ts 🔵 Nit msg.timestamp shown as raw ISO string; hard to read

🧠 Learnings used
Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/handlers/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:43.140Z
Learning: Applies to src/handlers/**/handlers/**/*.ts : Use clack/prompts with handleCancel wrapper for user interactions

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.708Z
Learning: Applies to src/discord/**/*.ts : Do not skip Zod validation of API responses

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/handlers/AGENTS.md:0-0
Timestamp: 2026-03-26T03:48:58.569Z
Learning: Applies to src/handlers/**/*.ts : Coordinate discord API → collate → export → file pipeline in handlers

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/handlers/AGENTS.md:0-0
Timestamp: 2026-03-26T14:56:15.999Z
Learning: Applies to src/handlers/**/*.ts : Use clack/prompts with handleCancel wrapper for user interactions in handlers

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.708Z
Learning: Applies to src/discord/**/*.ts : Validate every API response with Zod before processing

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/handlers/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:43.140Z
Learning: Applies to src/handlers/**/handlers/**/*.ts : Don't call discord directly from CLI handlers

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/handlers/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:43.140Z
Learning: Applies to src/handlers/**/handlers/**/export.ts : Coordinate discord API → collate → export → file pipeline

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.708Z
Learning: Applies to src/discord/**/*.ts : Extract magic numbers as named constants (MAX_OFFSET=9975, MAX_RETRIES=5, BASE_DELAY_MS)

Learnt from: CR
Repo: mynameistito/discord-search PR: 0
File: src/discord/AGENTS.md:0-0
Timestamp: 2026-03-26T03:41:36.708Z
Learning: Applies to src/discord/**/search.ts : Use offset pagination with 0-9975 range (25 results per page, max 400 pages)

…emas, and search

- Add non-TTY guard to prevent Promise hangs in browseMessages
- Fix ANSI cursor positioning by adding ANSI_CURSOR_HOME
- Fix --version flag to work with subcommands (search, preset, settings)
- Consolidate parseCommaSeparated in utils.ts, remove duplicate from prompts
- Add VERSION constant for version display
- Fix empty array truthy bug by adding length checks
- Change authorType to multiselect to match schema array support
- Remove redundant .max(20) validation from snowflakeSchema
- Truncate embed titles (80 chars) and field names/values (40/100 chars)
- Add comprehensive error handling with reject to browseMessages Promise
- Document Esc key in navigation hints
- Use spread syntax for array operations
- Format timestamps with 24-hour locale format
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/cli/browser.ts">

<violation number="1" location="src/cli/browser.ts:168">
P2: `render()` references `cleanup` before it is initialized, which can throw a `ReferenceError` on initial render failures.</violation>
</file>

<file name="src/cli/args-parse.ts">

<violation number="1" location="src/cli/args-parse.ts:330">
P2: The `search --version` path constructs an invalid `SearchArgs` object, causing parse-time failure instead of printing the version.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/cli/browser.ts
Comment thread src/cli/args-parse.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
src/cli/prompts.ts (1)

132-151: ⚠️ Potential issue | 🟡 Minor

Numeric prompt validation remains too permissive (Line 132-151 / 190-198).

Using Number.parseInt + NaN-only checks accepts partial numerics (e.g., 10abc) and defers failures to schema validation instead of giving immediate prompt feedback.

♻️ Proposed fix
+import { MAX_OFFSET } from "@/discord/schemas.ts";
@@
   const limitInput = await text({
@@
     validate: (v) => {
-      if (v?.trim() && Number.isNaN(Number.parseInt(v.trim(), 10))) {
-        return "Must be a number";
+      const s = v?.trim();
+      if (!s) return;
+      if (!/^\d+$/.test(s)) {
+        return "Must be a whole number";
+      }
+      const n = Number(s);
+      if (n < 1) {
+        return "Must be at least 1";
       }
     },
   });
@@
   const offsetInput = await text({
@@
     validate: (v) => {
-      if (v?.trim() && Number.isNaN(Number.parseInt(v.trim(), 10))) {
-        return "Must be a number";
+      const s = v?.trim();
+      if (!s) return;
+      if (!/^\d+$/.test(s)) {
+        return "Must be a whole number";
+      }
+      const n = Number(s);
+      if (n > MAX_OFFSET) {
+        return `Must be <= ${MAX_OFFSET}`;
       }
     },
   });
#!/bin/bash
# Verify permissive parseInt-based validation in prompts
rg -nP 'Number\.parseInt\(v\.trim\(\), 10\)' src/cli/prompts.ts -C3

# Verify offset schema bound for mismatch with prompt-time validation strictness
rg -nP 'offset:\s*z\.number\(\)\.int\(\)\.min\(0\)\.max\(MAX_OFFSET\)' src/discord/schemas.ts -C1

Also applies to: 190-198

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/prompts.ts` around lines 132 - 151, The prompt validators for
limitInput and offsetInput use Number.parseInt(...) with only a NaN check, which
permits inputs like "10abc"; update the validate functions used in text({...})
for limitInput and offsetInput to enforce strictly numeric input (e.g., check
trimmed value matches /^\d+$/ or use Number(v.trim()) and Number.isInteger with
no extraneous chars) and return "Must be a number" when invalid so the prompt
rejects malformed values immediately; ensure this stricter validation aligns
with the offset schema (offset: z.number().int().min(0).max(MAX_OFFSET)) and
apply the same change to the duplicate validator later in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/args-parse.ts`:
- Around line 493-504: The code is inventing fake actions like "preset/list" and
"settings/show" to carry --version; instead create a dedicated version variant
(or a top-level short-circuit) in parseArgs so --version never mutates other
action values. Concretely, replace the branches that return parseWithError with
fake actions when global.version is true by returning a ParsedArgs for a
dedicated command such as command: "version" (e.g., via
parseWithError<ParsedArgs>({ command: "version", action: "show", help:
global.help, version: true, token: global.token }, "version")), or alternatively
add a single early check in parseArgs (if global.version) that returns this
dedicated version ParsedArgs before any command-specific mapping; update any
other similar blocks (including the other occurrence around settings/show) to
use the same dedicated variant.
- Around line 329-338: The returned SearchArgs for the global.version branch is
missing required fields and fails schema validation; update the object passed to
parseWithError in the global.version check to include the required fields (e.g.,
add params: [] and json: false) so the shape satisfies
ParsedArgsSchema/SearchArgs validation and allows the version handler in
index.ts to run; reference the parseWithError call, the SearchArgs type, and the
global.version condition when making this change.

In `@src/cli/browser.ts`:
- Around line 104-111: handleRenderError currently calls reject(err) then
cleanup(), which lets exceptions from cleanup escape; change it so cleanup is
invoked inside a try/catch (e.g., try { cleanup(); } catch (cleanupErr) { /*
swallow or log via console.error/processLogger */ }) and then call reject(err)
(or call reject in a finally block) so that any exception thrown by cleanup does
not propagate; update the handleRenderError function to swallow or log cleanup
errors and ensure reject(err) is always invoked without rethrowing cleanup
exceptions.
- Around line 160-169: render() is called before the cleanup variable is
initialized, so if renderMessageView throws, handleRenderError(err, reject,
cleanup) will reference cleanup in TDZ and throw a ReferenceError that hides the
original error; fix by declaring and initializing cleanup to a no-op before the
initial render call (e.g. let cleanup: () => void = () => {};), then later
assign the real cleanup function where currently defined, so render,
renderMessageView, handleRenderError and state/messages logic stay the same but
cleanup is safe to reference on errors.

In `@src/discord/search.ts`:
- Line 200: The spread operator call state.allMessages.push(...pageMessages)
causes large-batch performance issues; replace it with an explicit loop that
iterates pageMessages and pushes each item into state.allMessages (e.g., for
(const msg of pageMessages) state.allMessages.push(msg)) so accumulation happens
one element at a time; update the code in the function/method containing
state.allMessages and pageMessages to use this for-loop pattern instead of the
spread push.

In `@src/index.ts`:
- Around line 22-25: The code currently calls process.exit() after printing
output (e.g., when parsed.version is truthy and printVersion() is called);
change these exits to set process.exitCode to the desired status (0 or other)
and then return from the function so the process can flush I/O naturally. Locate
the branches using parsed.version, printVersion(), and other places that call
process.exit() (the blocks around those conditions) and replace each direct
process.exit(...) call with assignment to process.exitCode and a plain return;
ensure the same exit code value is preserved.

---

Duplicate comments:
In `@src/cli/prompts.ts`:
- Around line 132-151: The prompt validators for limitInput and offsetInput use
Number.parseInt(...) with only a NaN check, which permits inputs like "10abc";
update the validate functions used in text({...}) for limitInput and offsetInput
to enforce strictly numeric input (e.g., check trimmed value matches /^\d+$/ or
use Number(v.trim()) and Number.isInteger with no extraneous chars) and return
"Must be a number" when invalid so the prompt rejects malformed values
immediately; ensure this stricter validation aligns with the offset schema
(offset: z.number().int().min(0).max(MAX_OFFSET)) and apply the same change to
the duplicate validator later in the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: da21e05e-9060-4644-a46f-a481fafda602

📥 Commits

Reviewing files that changed from the base of the PR and between 2677136 and e394318.

📒 Files selected for processing (7)
  • src/cli/args-parse.ts
  • src/cli/browser.ts
  • src/cli/prompts.ts
  • src/cli/utils.ts
  • src/discord/schemas.ts
  • src/discord/search.ts
  • src/index.ts

Comment thread src/cli/args-parse.ts
Comment thread src/cli/args-parse.ts
Comment thread src/cli/browser.ts
Comment thread src/cli/browser.ts Outdated
Comment thread src/discord/search.ts Outdated
Comment thread src/index.ts
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/cli/args-parse.ts">

<violation number="1" location="src/cli/args-parse.ts:336">
P2: `params: {}` does not satisfy SearchParamsSchema (missing required `guildId`), so `search --version` will fail validation and exit instead of showing the version.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/cli/args-parse.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/browser.ts`:
- Around line 168-200: The initial render failure can still lead to
createKeyListener being installed; change the logic so the code only sets up the
key listener if the initial render succeeded. In practice, add a local boolean
(e.g., renderSucceeded) or make render() return a success flag and in the outer
flow check that flag before assigning cleanup = createKeyListener(...). Update
the try/catch around render() (and the inner catch that calls handleRenderError)
to set renderSucceeded = false on error and skip calling createKeyListener,
referencing the existing render, handleRenderError, createKeyListener, cleanup,
resolve, and reject symbols.

In `@src/cli/prompts.ts`:
- Around line 34-42: Update the validate callback on the text(...) call that
assigns guildId to not only check for non-empty but also enforce Discord
snowflake format: trim the input and ensure it matches /^\d{17,19}$/, returning
"Guild ID must be a 17–19 digit Discord snowflake" when it fails; otherwise
return undefined/true to indicate success. Target the validate function passed
to text({ message: "Guild (Server) ID:", initialValue: defaultGuildId, validate:
... }) and replace the current only-empty check with the combined trim + regex
check.
- Around line 15-20: Change handleCancel from returning void to an assertion
function so TypeScript can narrow types without casts: make handleCancel generic
with an assertion signature (using asserts value is T) and keep the existing
logic that calls isCancel and cancel/process.exit; update callsites (e.g., where
token/password is awaited) to rely on the assertion instead of casting.
Reference symbols: handleCancel, isCancel, cancel. Ensure the function remains
exported and behavior unchanged except for its type signature.

In `@src/index.ts`:
- Line 7: Replace the hardcoded const VERSION = "0.0.0" with a dynamic value so
it cannot drift from package.json: either import or require the package.json
version (e.g., pull version from package.json into the VERSION constant in
src/index.ts) or read process.env.npm_package_version at runtime, or support a
build-time injected token like __VERSION__ and fall back to package.json; update
the symbol VERSION accordingly so the app always uses the package.json-provided
version.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ab486f62-6cf8-4746-b096-cb0ec08186ff

📥 Commits

Reviewing files that changed from the base of the PR and between e394318 and d6c926c.

📒 Files selected for processing (5)
  • src/cli/args-parse.ts
  • src/cli/browser.ts
  • src/cli/prompts.ts
  • src/discord/search.ts
  • src/index.ts

Comment thread src/cli/browser.ts Outdated
Comment thread src/cli/prompts.ts Outdated
Comment thread src/cli/prompts.ts
Comment thread src/index.ts Outdated
- args-parse: search --version returns HelpArgs to avoid Zod validation failure on missing guildId
- browser: only install key listener if initial render succeeded
- prompts: enforce snowflake format (17-20 digits) on guildId validation
- prompts: convert handleCancel to assertion function for proper type narrowing
- index: import VERSION from package.json instead of hardcoded value

Co-authored-by: Amp <amp@ampcode.com>
@mynameistito
Copy link
Copy Markdown
Owner Author

@coderabbitai any more issues that need to be resolved, please do a deep analysis

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 27, 2026

- args-parse: reject --all + named presets in preset run-all
- args-parse: validate snowflake format for settings set guild/client-id
- index: add exhaustiveness check for unhandled commands
- utils: extract shared INTEGER_REGEX, import in args-parse and prompts
- schemas: export SNOWFLAKE_REGEX, import in prompts and args-parse
- browser: rename misleading safeWrite to write
- keys: document stdin encoding restoration limitation

Co-authored-by: Amp <amp@ampcode.com>
@mynameistito mynameistito merged commit 051c60b into main Mar 27, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation source tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant