Skip to content

Add static docs site and agent-friendly CLI output#10

Open
batuhan wants to merge 25 commits into
mainfrom
batuhan/fix
Open

Add static docs site and agent-friendly CLI output#10
batuhan wants to merge 25 commits into
mainfrom
batuhan/fix

Conversation

@batuhan
Copy link
Copy Markdown
Member

@batuhan batuhan commented May 21, 2026

Summary

  • Add a fully static Astro/Starlight docs site under docs/, refresh the top-level README, and regenerate the CLI README/docs with links to the published docs.
  • Add agent-friendly CLI surfaces: beeper schema, command aliases like beeper send, beeper search, and beeper ls, command metadata, broader --dry-run coverage for mutating commands, and machine-readable output controls for formats, field selection, results-only output, text, and ids.
  • Normalize CLI output envelopes and error payloads around ok/error, improve resolver and schema validation errors, and make spinners respect machine-readable output modes.
  • Improve setup/install flows by detecting an already installed local Beeper Server, offering to use it instead of reinstalling, and normalizing server environments across local, dev, staging, and prod.
  • Make the npm launcher easier to debug by logging download, redirect, verification, extraction, cache, and optional launch steps.

Testing

  • Existing CLI smoke coverage updated for the new output envelope, schema command, resolve commands, installed-server setup action, server environment URL normalization, and launcher-related behavior.
  • messages search validation test updated for the new structured error payload.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added --dry-run mode across many commands to preview changes without executing
    • Added resolve commands for chat, account, contact, target, and bridge selectors
    • Added schema and exit-codes commands for automation and integration
    • Added safety profiles (agent-safe, readonly, full) for access control
    • Improved setup command with better server detection and installation handling
    • Added new server environment support (local, dev, staging, prod)
  • Changes

    • Simplified CLI documentation structure
    • Reorganized package with streamlined command registry
    • Updated output formats and flag naming conventions
    • Removed deprecated commands and plugin system

Walkthrough

Replaces Oclif with a custom CLI core (parse/output/policy/schema/MCP), overhauls setup/install with server-env normalization, adds dry-run across many commands, updates docs to generated registry pages, adjusts tests/workflows, and removes legacy plugins/scripts.

Changes

Beeper CLI overhaul

Layer / File(s) Summary
Core CLI engine and types
packages/cli/src/cli/*
Adds custom command registry, parser, output/error handling, policy/safety profiles, schema builder, MCP server, and main entrypoint.
Setup/install and server environments
packages/cli/src/cli/setup.ts, .../lib/{targets,installations,server-env,desktop-auth,app-state,oauth}.ts
Refactors setup flows, installs, readiness, token/auth, and introduces normalized server envs and update handling.
Command updates with dry-run and behavior tweaks
packages/cli/src/commands/**/*
Adds --dry-run to many commands, adjusts flags (e.g., server-env), output/spinner logic, and removes deprecated commands.
Docs, safety profiles, and generation
packages/cli/docs/**, packages/cli/safety-profiles/*, packages/cli/scripts/generate-command-docs.ts
Replaces hand-written docs with generated command pages, adds safety profiles, and a docs generator.
Tests, packaging, and CI
packages/cli/test/cli-smoke.ts, workflows, bin/scripts, package.jsons
Updates tests to new envelopes/commands, simplifies CI/publish, changes entrypoints, removes legacy scripts/plugins.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(0, 150, 255, 0.5)
  participant User
  participant CLI
  participant Parser
  participant Policy
  participant Command
  participant Output
  end
  User->>CLI: runCli(argv)
  CLI->>Parser: parseCommand(argv, commands)
  Parser-->>CLI: {command, flags, args}
  CLI->>Policy: enforcePolicy(command, flags)
  Policy-->>CLI: ok
  CLI->>Command: command.run(ctx)
  Command-->>CLI: result
  CLI->>Output: writeResult(result, flags, command)
  Output-->>User: human/json/jsonl
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

  • beeper/cli#8: Adjusts command alias generation and tests in a similar area (command map/registry updates).
✨ 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 batuhan/fix

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

Caution

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

⚠️ Outside diff range comments (1)
packages/cli/src/commands/setup.ts (1)

283-304: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Non-interactive “current target unreachable” output still ignores installed server state.

You added serverInstalled for interactive recovery here, but the JSON/non-TTY path still calls currentTargetBrokenOutput(...), which always emits install-server. This makes automation output inconsistent and can incorrectly suggest reinstalling an already installed server.

Suggested fix
-      if (flags.json || !process.stdin.isTTY) {
-        await printData(currentTargetBrokenOutput(target, readiness), flags.json ? 'json' : 'human')
+      if (flags.json || !process.stdin.isTTY) {
+        const serverInstalled = await isServerInstalled()
+        await printData(currentTargetBrokenOutput(target, readiness, serverInstalled), flags.json ? 'json' : 'human')
         return
       }
-function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record<string, unknown> {
+function currentTargetBrokenOutput(target: Target, readiness: Readiness, serverInstalled: boolean): Record<string, unknown> {
+  const serverAction = installedServerAction(serverInstalled)
   return {
     state: 'current-target-unreachable',
     message: `Beeper CLI is set up for ${target.name ?? target.id}, but it is not reachable.`,
     target: publicTarget(target),
     readiness,
     recommendedAction: action('retry-current', `beeper setup -t ${target.id}`),
     availableActions: [
       action('retry-current', `beeper setup -t ${target.id}`),
       action('use-desktop', 'beeper setup --desktop'),
-      action('install-server', 'beeper setup --server --install --yes'),
+      serverAction,
       action('connect-remote', 'beeper setup --remote <url>'),
     ],
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/setup.ts` around lines 283 - 304, The
non-interactive path always outputs "install-server" because
currentTargetBrokenOutput is called without the actual installed state; update
the flow so the result of isServerInstalled() (serverInstalled) is used to
determine the JSON/non-TTY output—either by passing serverInstalled into
currentTargetBrokenOutput or by adding logic in the non-interactive branch to
emit "use-installed-server" when serverInstalled is true (identify the
isServerInstalled() call and the currentTargetBrokenOutput(...) invocation to
modify). Ensure the JSON/TTY output mirrors the interactive prompt choice text
for option 3 instead of always suggesting installation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/npm/scripts/build.ts`:
- Around line 132-137: The milestone logic in the download progress handler
(variables percent, nextLoggedPercent, downloaded, total, function logStep) can
skip intermediate thresholds when percent jumps past multiple milestones; change
the check to a loop that while percent >= nextLoggedPercent (or percent === 100)
emits logStep for each milestone and increments nextLoggedPercent by 25 each
iteration (ensuring the final 100% is emitted with milestone 100), so every 25%
milestone is logged even if a single data event advances progress across several
thresholds.
- Around line 113-118: The download function follows redirects recursively
without a cap; add a maxRedirects parameter (e.g., maxRedirects = 10) and a
currentRedirects counter to the download function signature, increment it on
each redirect and if it exceeds the limit reject with a clear error instead of
recursing forever; update the redirect branch that calls download(nextURL,
destination) to call download(nextURL, destination, currentRedirects + 1,
maxRedirects) (or pass a default when initiating), and ensure all caller sites
provide the new parameter or rely on the default.

---

Outside diff comments:
In `@packages/cli/src/commands/setup.ts`:
- Around line 283-304: The non-interactive path always outputs "install-server"
because currentTargetBrokenOutput is called without the actual installed state;
update the flow so the result of isServerInstalled() (serverInstalled) is used
to determine the JSON/non-TTY output—either by passing serverInstalled into
currentTargetBrokenOutput or by adding logic in the non-interactive branch to
emit "use-installed-server" when serverInstalled is true (identify the
isServerInstalled() call and the currentTargetBrokenOutput(...) invocation to
modify). Ensure the JSON/TTY output mirrors the interactive prompt choice text
for option 3 instead of always suggesting installation.
🪄 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: CHILL

Plan: Pro

Run ID: 33f5d751-b2d8-4103-bce8-43f1439e85eb

📥 Commits

Reviewing files that changed from the base of the PR and between 9502715 and 879f92a.

📒 Files selected for processing (5)
  • packages/cli/README.md
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/commands/watch.ts
  • packages/cli/test/cli-smoke.ts
  • packages/npm/scripts/build.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (ubuntu-latest / bun 1.3.10)
  • GitHub Check: test (macos-latest / bun 1.3.10)
🧰 Additional context used
🪛 ESLint
packages/cli/src/commands/setup.ts

[error] 416-416: Do not use useless undefined.

(unicorn/no-useless-undefined)


[error] 641-641: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 656-656: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 672-672: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

🔇 Additional comments (4)
packages/cli/src/commands/watch.ts (1)

17-18: LGTM!

packages/cli/README.md (1)

2418-2419: LGTM!

packages/cli/src/commands/setup.ts (1)

106-107: LGTM!

Also applies to: 254-275, 347-351, 406-435, 513-530, 626-690

packages/cli/test/cli-smoke.ts (1)

3-3: LGTM!

Also applies to: 238-262

Comment thread packages/npm/scripts/build.ts Outdated
Comment thread packages/npm/scripts/build.ts Outdated
@batuhan
Copy link
Copy Markdown
Member Author

batuhan commented May 30, 2026

@indent review

@indent
Copy link
Copy Markdown

indent Bot commented May 30, 2026

PR Summary

Adds gogcli-style ergonomics on top of the slim hand-rolled CLI from 044405c. Builds out flag/command aliases (--yes/-y, --noop/--preview/-n, -j, targets ls, config show, etc.), tab completion (__complete + shell-specific generators), per-command markdown docs (~74 files under packages/cli/docs/commands/), and product-direction guardrails in a top-level INTENT.md.

  • Restores --read-only enforcement at policy.ts:9-13 (rejects any non-read command, honoring BEEPER_READONLY env).
  • Adds --enable-commands/--enable-commands-exact/--disable-commands (comma-separated) for declarative command allow/deny lists, layered with safety profiles.
  • Adds top-level --timeout parsing in main.ts:39-58 so command runs abort with code:"timeout" after the configured duration.
  • Adds global --access-token / --home / --account / --color / --full / --select / --results-only / --version flags with env-var fallbacks (BEEPER_ACCESS_TOKEN, BEEPER_CLI_CONFIG_DIR, BEEPER_READONLY).
  • Adds output: 'targets'|'accounts'|'chats'|'contacts'|'messages'|'status'|'diagnostic' taxonomy and a new table renderer in output.ts:122-225 that picks per-kind preferred columns; --full disables truncation; --plain emits TSV.
  • Adds JSON --select <a.b,c> projection and --results-only extraction (unwraps data/items/messages/etc. keys) in output.ts:235-246 / output.ts:227-233.
  • New top-level commands: doctor, exit-codes, completion, config {get,set,unset,list,keys,path}; new exit code EmptyResults (empty_results).
  • MCP gets --allow-tool <glob> patterns, --list-tools, --timeout-seconds, --max-output-bytes byte-truncation, and structuredContent per MCP spec at mcp.ts:88-103.
  • Adds scripts/generate-command-docs.ts so the per-command docs stay in sync with the schema; new smoke tests cover the new aliases/flags.

Issues

7 potential issues found:

  • The JSON envelope has now changed shape THREE times in this PR with zero migration notes. Original CLI: {success, data, error: string}. Mid-PR (6d302f3+): {ok, data, error: {code, message, exitCode, kind, hint}, meta}. After 044405c's rewrite: success is the bare value with no envelope (process.stdout.write(JSON.stringify(value, null, 2)) at packages/cli/src/cli/output.ts:14); errors are {error: {code, exitCode, hint, kind, message}} at output.ts:42 — no ok/success field at all, no data wrapper. External automation that reads envelope.success, envelope.ok, or treats envelope.data as the top level is silently broken. The README/CHANGELOG/docs that explained the envelope are also deleted in this commit. Either restore an {ok, data, error} (or whatever the canonical shape is) wrapping and document it, or add a top-level note in the PR description + any remaining docs calling out the breaking change. → Autofix
  • ExitCodes.AuthRequired and ExitCodes.NotReady collide on value 4 in packages/cli/src/lib/errors.ts:15-16EmptyResults was inserted at 3 and AuthRequired bumped to 4, but NotReady was left at 4. Every NotReady error is now silently reported as auth_required (output.ts:errorCode() matches code === ExitCodes.AuthRequired first and returns before the NotReady branch). beeper exit-codes (the new diagnostic command added in this commit, commands.ts:963-977) publishes them as two separate codes, so the documented contract lies. Header comment at errors.ts:1-9 also no longer lists not_ready at all. → Autofix
  • packages/cli/src/lib/prompts.ts promptText/promptConfirm/promptChoice never check globalFlags.noInput or process.stdin.isTTY before opening readline on stdin. The new setup (cli/setup.ts:139,150,153,159,175,249,251,258,263,277,286,292,392), accounts add (commands.ts:1868,1926), and account-login.ts:87,372,375 all call these helpers unconditionally — every one of them will hang on rl.question(...) when the CLI is run with --no-input or under an agent/CI pipe (stdin.isTTY === false). The globalFlags.noInput plumbing exists (parse.ts:89, commands.ts:1755's chatResolutionOptions) but the prompts layer ignores it. → Autofix
  • packages/cli/src/cli/setup.ts:502 interactive(flags) returns !flags.json && process.stdin.isTTY — it ignores globalFlags.noInput, so beeper setup --no-input from a TTY (the common case for an agent or CI shell session) still takes the interactive branch and calls promptConfirm(...)/promptChoice(...)/promptText(...) (setup.ts:139,150,153,159,175,249,263,277,292,392), all of which will block on stdin. The noInput global flag exists but isn't wired here. → Autofix
  • .DS_Store (committed in cf9a5bd) is still tracked at the repo root in 044405c and .gitignore still doesn't ignore it. Same class of dev-environment leak as the .claude/scheduled_tasks.lock removed in fefbf4e. Delete the file (git rm .DS_Store) and add **/.DS_Store to .gitignore. → Autofix
  • packages/cli/src/cli/output.ts:14-17 runs --select before --results-only, so --json --results-only --select id,text collapses any enveloped response (anything with a data/items/messages wrapper) to {}. selectFields walks the top-level object looking for id/text, finds neither on the envelope, and primaryResult then returns the empty result unchanged. Today most commands return bare arrays so the bug is latent; the moment any command returns {data: [...]} (or any future change wraps results), the combination silently empties the output. Swap the two lines so primaryResult runs first. → Autofix
  • Destructive commands run with no confirmation and no --force check. remove target, remove account, and messages delete are all risk: 'destructive' in packages/cli/src/cli/commands.ts (L260, L267, L1373) and the schema declares requirements: ['destructive', 'force'] (schema.ts:100), but the command bodies (removeTargetCommand L1032, removeAccount L1039, messagesDelete L1373) never read ctx.globalFlags.force and never call promptConfirm. beeper remove target prod deletes the saved target without any prompt or --force requirement; same for beeper remove account someone@example.com and beeper messages delete <id>. This is a regression from cf9a5bd's bridges/delete.ts, which at least called confirm(...) and threw on cancel. → Autofix
19 issues already resolved
  • Validation throw new Error(...) sites in commands render as kind:"bug" / internal_error with the bug panel — pre-existing, not new in this PR. 3bf94b7 correctly restored the && inferredCode === undefined guard and tightened inferExitCode (fixing the library-error reclassification from 49b0ca88), but the strict regex still doesn't match the user-facing validation messages: setup.ts:56-60 "Specify at most one of …", chats/{avatar,draft,disappear}.ts "Provide --…" / "--clear cannot be combined …" / "--seconds must be a positive integer …", presence.ts:20-21 "--duration must be a positive integer …", messages/export.ts:25,47-48 "Use only one of --before-cursor …" / "--after is not a valid ISO timestamp …", api/post.ts:26 "--body is not valid JSON …", setup.ts:236 "Install requires --install --yes …", every validation in accounts/add.ts. All 58 sites should be converted to throw usageError(...) (already exported from packages/cli/src/lib/errors.ts:65) so they render as one-line aborts with exit 2 / code:"usage_error" for JSON consumers. (fixed by commit 044405c)
  • beeper schema <command> never filters: Args.string({ multiple: true }) isn't a valid oclif v4 args option so args.command is a string (not an array), and the handler guards with Array.isArray(args.command) ? args.command.join(' ') : undefined — always false, so requested is always undefined and the full schema is returned even for the documented beeper schema send --results-only example. (fixed by commit c5cfb05)
  • bridges delete blocks forever under --no-input. The local confirm() helper at packages/cli/src/commands/bridges/delete.ts:43-51 opens readline on stdin without checking isNoInput()/process.stdin.isTTY, so beeper bridges delete foo --no-input (or any agent/CI invocation without a TTY) hangs on rl.question('… [y/N] ') instead of aborting. Every other interactive prompt in this PR (bridges/login.ts:29, bridges/login-password.ts:30, manager.ts:639-643) correctly gates on isNoInput(). (fixed by commit 044405c)
  • 7b17068 only relocated the dry-run guard in 8 commands (accounts/add, auth/logout, chats/disappear, messages/export, presence, send/voice, update, verify/approve). 54 commands still execute ensureWritable(flags)/createClient(flags)/resolveChatID(...) before the flags['dry-run'] early return — e.g. send/text, send/file, send/react, all 16 chats/* except disappear, messages/delete, messages/edit, every verify/* except approve, every targets/*, both install/*, config/{set,reset}, etc. beeper send text … --dry-run --read-only --json against a config dir with no targets/auth still fails because ensureWritable and createClient run first. (fixed by commit 044405c)
  • inferExitCode regexes match common substrings (required, parse, invalid, usage, not found, etc.) in any non-CLIError message and flip isBug to false, suppressing the bug panel for real programming errors — e.g. node's Cannot find required module 'foo' is reclassified as a usage error (exit 2) instead of a bug with stack trace and report link. (fixed by commit ffcff36)
  • In the npm launcher's download progress logger, the logged value is the next threshold (nextLoggedPercent), not the actual percent — so a single chunk that takes the download from 0% to 80% will print downloaded 25% while the file is actually 80% done; milestones are still emitted in order and 100% is correct, but intermediate labels lag actual progress. (fixed by commit 3212e03)
  • None of the new mutating bridge commands honor --dry-run. bridges/{delete,register,config,login,login-password,logout,run,proxy}.ts all inherit 'dry-run' from BeeperCommand.baseFlags (lib/command.ts:9) but none branch on it before performing server mutations (deleteBridge, registerBridge/state PUT, loginWithEmail/saveTargetAuth, logoutBridgeTarget, websocket-proxy state events, bridge subprocess spawn). beeper bridges delete foo --dry-run deletes the bridge. This compounds open issue 26b89d6f's dry-run-ordering gap with brand-new commands that lack the guard entirely. (fixed by commit 044405c)
  • 3212e03 broke the npm launcher build: packages/npm/scripts/build.ts builds the launcher source as a template literal returned from launcher() (outer backticks on L32/L169), but every nested template literal inside that string (L48, 58, 60-61, 69, 76, 84, 92, 100, 110, 112, 117-118, 124, 136, 156, 165) now uses bare backticks instead of the previous \`` escapes. The outer template terminates at the first inner backtick on L48, producing invalid JavaScript — node --checkagainst the file rejects it withSyntaxError: Unexpected identifier 'beeper'. This blocks bun run buildinpackages/npm` and prevents publishing the npm wrapper. (fixed by commit a819a5a)
  • db163a2 accidentally commits .claude/scheduled_tasks.lock — a Claude Code runtime lock file containing the author's local session ID, PID, and acquire timestamp. The repo has no .claude/ entry in .gitignore, so this kind of dev-environment state will keep leaking into commits. Should be removed from the tree and the .claude/ directory added to .gitignore. (fixed by commit fefbf4e)
  • schema reads positional command-path tokens from this.argv (raw input incl. flag values), not the parsed argv returned by this.parse(...). The arg.startsWith('-') filter strips flag names but not flag values, so beeper schema send --select id produces pathArgs=['send','id']requested='send id' → no match — same dead-filter symptom my original review reported, just now triggered by any value-taking flag (--select, --format, --timeout, --target, --base-url). (fixed by commit 31c9d55)
  • bridges register and bridges whoami bypass the envelope output pipeline, breaking --json/--select/--results-only/--quiet. register.ts:30-32 writes raw JSON.stringify(output, null, 2) via writeRegistrationJSON (manager.ts:309) instead of printData, so machine consumers don't get the {ok,data,error,meta} shape every other command emits. whoami.ts:18-21 has two divergent JSON paths (--raw writes raw, --json uses printData) that emit incompatible shapes for ostensibly the same data. New commands config, delete, run also write plain process.stdout.write/stderr.write lines for success/progress, so --json --quiet still prints Started deleting bridge\n etc. to stdout. (fixed by commit 044405c)
  • packages/cli/src/lib/command-metadata.ts mutatingVerbs/mutatingRoots is out of date for the new bridge subcommands. register, login, login-password, proxy, run, config aren't listed, so metadataForCommand('bridges register') (used by schema, man, and the JSON command manifest at lib/manifest.ts) returns mutates: false for commands that PUT appservice registrations, save Matrix tokens, or spawn bridge subprocesses. Agents reading the schema to decide what to dry-run/force will treat these as read-only. (fixed by commit 044405c)
  • --server-env flag descriptions in setup, install server, targets add desktop, targets add server, and packages/cli/README.md all say Server feed environment: prod or staging, but the new normalizeServerEnv accepts four envs (local, dev, staging, prod) plus production as an alias — local/dev are silently undocumented, and removing options:[...] also drops tab-completion suggestions. (fixed by commit 337c94c)
  • currentTargetBrokenOutput still hardcodes the install-server action in its JSON envelope, so users hitting setup --json (or non-TTY) with an unreachable current target are told to reinstall the Server even when it is already installed — the interactive handleBrokenCurrentTarget was updated to use installedServerAction but this JSON path was missed, contradicting the PR's stated goal of preferring the installed Server "including JSON setup actions". (fixed by commit 337c94c)
  • ensureNotDryRun throws new CLIError('dry-run: …', ExitCodes.Success), which the new catch() handler will turn into {ok:false, error:{exitCode:0, …}} on stderr while the process exits with code 0 — a contract mismatch. The helper is currently unused, but the first command that adopts it will report successful dry-runs as failures to JSON consumers. (fixed by commit ffcff36)
  • docs/src/content/docs/watch.mdx (the new Starlight docs page that replaces packages/cli/docs/watch.md) still lists only chat.upserted, chat.deleted, message.upserted, message.deleted as event types — message.stream was added to the flag options and the auto-generated CLI README but the maintained docs page wasn't updated. (fixed by commit 044405c)
  • lib/bridges/manager.ts adds a network surface (requestJSON, downloadFile, fetch to mau.dev/api.beeper.com/Matrix homeserver) with no timeout/AbortSignal handling. A stalled connection during beeper bridges {whoami,login,register,run} will hang indefinitely — the same launcher-download concern CodeRabbit raised on packages/npm/scripts/build.ts, but for the user-facing CLI. Worse, BeeperCommand's inherited --timeout flag is ignored entirely by these new code paths. downloadFile (manager.ts:499) also leaves the tmp-${basename} partial file behind on pipeline failure and skips integrity verification on the binary it then chmod 0o755s and executes. (fixed by commit 044405c)
  • 12ed611 removes the normalizeInstallRequest workaround that explicitly forced kind: 'server' to serverEnv: 'staging'/channel: 'nightly' because the production endpoint "returns a beeper-server artifact instead of the Desktop app bundle" (per the removed TODO). The default interactive setupFromChoice/handleBrokenCurrentTarget now passes server-env: 'prod'/channel: 'stable', so every menu-driven "Install local Beeper Server" will hit https://api.beeper.com/desktop/download/.../stable/com.automattic.beeper.server — if the prod endpoint hasn't actually been fixed externally, users get a broken artifact and the smoke test (which only asserts URL strings) won't catch it. (fixed by commit 044405c)
  • packages/cli/README.md has https://example.com placeholders in the docs-site badge (L9), the "Read the docs" link (L25), and every cell of the documentation topic table (L205-217) — these will render as broken links on npm and GitHub when the package publishes. The other example.com occurrences in the same file (e.g. https://desktop.example.com, you@example.com, webhook example) are legitimate code-sample placeholders and should be left alone. (fixed by commit f22ce03)

CI Checks

All CI checks pass on fefbf4e. The launcher-build syntax error that blocked the previous run is resolved by a819a5a (JSON-encoded launcher source).


⚡ Autofix All Issues

@batuhan batuhan changed the title more fixes Improve setup fallback handling and launcher install output May 30, 2026
Comment thread packages/npm/scripts/build.ts Outdated
Comment thread packages/cli/src/commands/schema.ts Outdated
Comment thread packages/cli/src/lib/command.ts Outdated
Comment thread packages/cli/src/lib/command.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: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (4)
packages/cli/src/commands/setup.ts (1)

170-173: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

JSON fallback still hardcodes install-server for broken current target.

The non-interactive/JSON branch still emits install-server in availableActions instead of preferring use-installed-server when a local server is already present, which conflicts with this PR’s fallback objective.

Suggested fix
-        await printData(currentTargetBrokenOutput(target, readiness), flags.json ? 'json' : 'human')
+        const serverInstalled = await isServerInstalled()
+        await printData(currentTargetBrokenOutput(target, readiness, serverInstalled), flags.json ? 'json' : 'human')
         return
       }

-function currentTargetBrokenOutput(target: Target, readiness: Readiness): Record<string, unknown> {
+function currentTargetBrokenOutput(target: Target, readiness: Readiness, serverInstalled: boolean): Record<string, unknown> {
   return {
@@
     availableActions: [
       action('retry-current', `beeper setup -t ${target.id}`),
       action('use-desktop', 'beeper setup --desktop'),
-      action('install-server', 'beeper setup --server --install --yes'),
+      installedServerAction(serverInstalled),
       action('connect-remote', 'beeper setup --remote <url>'),
     ],
   }
 }

Also applies to: 708-720

packages/cli/src/commands/auth/logout.ts (1)

12-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip the env-token hard error in dry-run mode.

Line 12’s guard runs before the dry-run return, so auth logout --dry-run can error even though it performs no mutation. Move the dry-run branch earlier (or gate the throw with !flags['dry-run']).

Proposed fix
     const { flags } = await this.parse(AuthLogout)
     ensureWritable(flags)
     const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] })
+    const token = target.auth?.accessToken
+    if (flags['dry-run']) {
+      await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human')
+      return
+    }
     if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) {
       throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.')
     }
-    const token = target.auth?.accessToken
-    if (flags['dry-run']) {
-      await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, flags.json ? 'json' : 'human')
-      return
-    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/auth/logout.ts` around lines 12 - 19, The guard
that throws when process.env.BEEPER_ACCESS_TOKEN is set runs before the dry-run
branch, causing `auth logout --dry-run` to error; to fix, ensure the dry-run
short-circuit happens before that throw (or wrap the throw in a `if
(!flags['dry-run'])` check). Concretely, in the logout command move the `if
(flags['dry-run']) { await printDryRun('auth.logout', ...) ; return }` block to
run before checking `process.env.BEEPER_ACCESS_TOKEN &&
!target.auth?.accessToken`, or add `&& !flags['dry-run']` to that throw
condition so `auth.logout --dry-run` does not throw while still preserving the
original guard for real runs.
packages/cli/src/commands/send/voice.ts (1)

23-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

send voice --dry-run should not require writable mode.

The command checks writability before the dry-run early return, so read-only mode can prevent preview-only execution.

Proposed fix
   async run(): Promise<void> {
     const { flags } = await this.parse(SendVoice)
-    ensureWritable(flags)
+    if (!flags['dry-run']) ensureWritable(flags)
     const client = await createClient(flags)

Also applies to: 39-42

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/send/voice.ts` around lines 23 - 25, The command
enforces writable mode before checking dry-run; update SendVoice command so that
the dry-run path bypasses writability: parse flags via this.parse(SendVoice),
check flags.dryRun (or flags["dry-run"]) and return the preview path before
calling ensureWritable(flags) or createClient(flags), and apply the same change
to the other occurrence that currently calls ensureWritable before the dry-run
check; keep references to ensureWritable, createClient and SendVoice when
locating the code to reorder the checks.
packages/cli/src/commands/accounts/add.ts (1)

30-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

--dry-run is blocked by the write guard.

ensureWritable(flags) runs before the dry-run early return, so read-only mode prevents previewing this command. Gate the write check when --dry-run is set.

Proposed fix
   async run(): Promise<void> {
     const { args, flags } = await this.parse(AccountsAdd)
-    ensureWritable(flags)
     const client = await createClient(flags)
@@
     if (flags['dry-run']) {
       await printDryRun('accounts.add', {
@@
       return
     }
+
+    ensureWritable(flags)
 
     const step = await client.bridges.loginSessions.create(accountType.id, {

Also applies to: 70-84

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/accounts/add.ts` around lines 30 - 33, The current
run() in AccountsAdd calls ensureWritable(flags) before handling the --dry-run
flag, which prevents previewing in read-only mode; modify run() (and any other
locations in the same file around the 70-84 block) to check for flags['dry-run']
(or flags.dryRun) first and early-return/preview when set, and only call
ensureWritable(flags) for non-dry-run flows; keep the same behavior and
messaging but gate the write-guard behind the dry-run check so ensureWritable is
skipped during dry runs.
🟡 Minor comments (25)
packages/cli/src/commands/chats/notify-anyway.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Drop unused printSuccess import.

printSuccess isn’t used and currently causes a lint error.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/notify-anyway.ts` at line 5, Remove the
unused import symbol printSuccess from the import list in the notify-anyway
module to resolve the lint error: update the import statement that currently
reads import { printData, printDryRun, printSuccess } from '../../lib/output.js'
to only import the symbols actually used (printData, printDryRun) so that
printSuccess is no longer referenced in the file.
packages/cli/src/commands/chats/remind.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printData import.

printData is no longer used in this file and currently triggers lint failure.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printDryRun, printSuccess } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/remind.ts` at line 5, The import list
includes an unused symbol printData in the import statement that causes a lint
error; remove printData from the import (keep printDryRun and printSuccess) in
the top-level import line where printData is referenced so the file no longer
imports an unused identifier (or alternatively use printData where intended),
ensuring the import matches actual usage in the remind.ts module.
packages/cli/src/commands/chats/draft.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is unused and currently fails lint.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/draft.ts` at line 5, The import list in
draft.ts includes an unused symbol printSuccess which triggers the linter;
remove printSuccess from the import statement (leave printData and printDryRun
intact) in the import from '../../lib/output.js' so the file only imports the
symbols actually used.
packages/cli/src/lib/output.ts-102-102 (1)

102-102: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unnecessary fallback object in spread.

The ?? {} fallback is redundant in object spread and currently hits lint.

Suggested fix
-    writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...(opts.data ?? {}) }), format)
+    writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity: opts.entity, ...opts.data }), format)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/output.ts` at line 102, The object spread uses a
redundant fallback; update the call to writeJSON/jsonPayload to spread opts.data
directly instead of using the unnecessary "...(opts.data ?? {})" fallback—i.e.,
change the spread to "...(opts.data)" within the jsonPayload invocation so
writeJSON(jsonPayload({ message: opts.message, detail: opts.detail, entity:
opts.entity, ...(opts.data) }), format) and remove the "?? {}" portion.
packages/cli/src/lib/output.ts-281-281 (1)

281-281: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use strict null checks in printText.

The nullish check uses ==, which violates the configured lint rules.

Suggested fix
-    if (item == null) continue
+    if (item === null || item === undefined) continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/output.ts` at line 281, In function printText replace
the loose nullish check on the variable item (currently using ==) with a strict
check that explicitly tests for null and undefined so lint rules pass; locate
the line "if (item == null) continue" in printText and change it to an explicit
null and undefined check for item (i.e., check both null and undefined) so the
branch behavior remains the same but uses strict comparison.
packages/cli/src/commands/chats/mark-unread.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is no longer used and currently triggers lint failure.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/mark-unread.ts` at line 5, Remove the unused
import to fix the lint error: in the import statement that currently imports
printData, printDryRun, printSuccess (from '../../lib/output.js') drop
printSuccess so only printData and printDryRun are imported; update the import
line where printSuccess is referenced to ensure no other code uses it (e.g.,
check for any occurrences of printSuccess in mark-unread.ts) and run linter to
confirm the error is resolved.
packages/cli/src/commands/chats/archive.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Drop unused printSuccess import.

printSuccess is unused in this command and should be removed.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/archive.ts` at line 5, The import list
includes an unused symbol printSuccess from the module imported in the top-level
import statement (the line that currently reads import { printData, printDryRun,
printSuccess } from '../../lib/output.js'); remove printSuccess from that import
so only printData and printDryRun are imported, and run a quick lint/type check
to ensure no other references to printSuccess remain in the chats/archive
command.
packages/cli/src/commands/chats/mark-read.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess from imports.

printSuccess is not used and currently triggers no-unused-vars.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/mark-read.ts` at line 5, The import list in
mark-read.ts includes an unused symbol printSuccess which triggers
no-unused-vars; remove printSuccess from the import statement that currently
reads import { printData, printDryRun, printSuccess } from '../../lib/output.js'
so the import only includes the actually used symbols (printData, printDryRun)
and save the file.
packages/cli/src/commands/send/file.ts-33-37 (1)

33-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add required blank line before the final statement.

This block currently violates @stylistic/padding-line-between-statements.

Suggested fix
     if (flags['dry-run']) {
       await printDryRun('send.file', request, flags.json ? 'json' : 'human')
       return
     }
+
     await printData(await sendMessage(client, request), flags.json ? 'json' : 'human')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/send/file.ts` around lines 33 - 37, Add a blank
line between the dry-run early-return block and the final await so the file
complies with padding-line-between-statements: after the if (flags['dry-run']) {
... return } block (which calls printDryRun) insert one empty line before the
final await printData(await sendMessage(client, request), flags.json ? 'json' :
'human'); ensure the surrounding code using sendMessage, printData and
printDryRun remains unchanged.
packages/cli/src/commands/chats/description.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the unused printSuccess import.

printSuccess isn’t referenced and should be dropped to satisfy lint rules.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/description.ts` at line 5, Remove the unused
import symbol printSuccess from the import statement that currently reads import
{ printData, printDryRun, printSuccess } from '../../lib/output.js'; update the
import to only include referenced symbols (printData, printDryRun) so the unused
printSuccess is eliminated and lint errors are resolved.
packages/cli/src/commands/resolve/account.ts-25-26 (1)

25-26: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

--pick 0 is incorrectly treated as “not provided”.

Using a truthy check lets 0 bypass validation and fall into default selection logic.

Suggested fix
-    const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined
-    if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick: flags.pick, count: candidates.length })
+    const pick = flags.pick
+    const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined
+    if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching accounts`, { selector: args.selector, pick, count: candidates.length })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/resolve/account.ts` around lines 25 - 26, The code
treats flags.pick==0 as "not provided" due to a truthy check; update the
selection logic to explicitly detect when a pick was provided (e.g., use typeof
flags.pick !== 'undefined' or flags.pick != null) and validate it as an integer
in range 1..candidates.length before indexing candidates (use flags.pick - 1).
Replace the current ternary that references flags.pick with this explicit check
and ensure the notFound error (and its message) is thrown when the provided pick
is outside the valid range.
packages/cli/src/commands/resolve/contact.ts-33-34 (1)

33-34: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

--pick 0 bypasses candidate range validation.

Because selection is guarded by truthiness, 0 behaves like “unset” instead of invalid input.

Suggested fix
-    const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined
-    if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick: flags.pick, count: candidates.length })
+    const pick = flags.pick
+    const selected = pick !== undefined ? candidates[pick - 1] : candidates.length === 1 ? candidates[0] : undefined
+    if (pick !== undefined && !selected) throw notFound(`--pick ${pick} is outside the ${candidates.length} matching contacts`, { selector: args.selector, pick, count: candidates.length })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/resolve/contact.ts` around lines 33 - 34, The
validation incorrectly treats flags.pick as truthy so a value of 0 bypasses
range checking; update the logic around selected and the subsequent throw to
explicitly detect when flags.pick is provided (e.g., flags.pick !== undefined &&
flags.pick !== null), compute the index as flags.pick - 1 to set selected from
candidates, and validate that flags.pick is between 1 and candidates.length
(throw notFound using the existing notFound, args.selector, and flags.pick if
out of range); ensure selected remains undefined when no pick was provided and
keep references to selected, flags.pick, candidates, notFound, and
args.selector.
packages/cli/src/commands/targets/start.ts-20-27 (1)

20-27: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Address padding-line lint failures in the new dry-run branches.

Line 20 and Line 24/27 need the blank-line separation required by @stylistic/padding-line-between-statements; this will likely fail lint as-is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/targets/start.ts` around lines 20 - 27, The new
branches around launchDesktopApp/printSuccess and the subsequent conditionals
violate padding-line rules; add the required blank lines to separate statement
groups: insert a blank line after the "return" following printSuccess (the
launchDesktopApp/printSuccess block) and ensure there's a blank line before the
if that checks "if (!target.managed || target.type !== 'server')" and before "if
(flags['dry-run'])" so each logical statement group
(launchDesktopApp/printSuccess/return, the non-managed/type check, and the
dry-run branch) is separated; update the block containing launchDesktopApp,
printSuccess, the return, the non-managed/type if, and the flags['dry-run'] if
to include those blank lines.
packages/cli/src/lib/command.ts-148-158 (1)

148-158: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix switch-case lint violations in errorCode.

Line 151–157 currently violates unicorn/switch-case-braces (and ordering if your lint config enforces it), which can block CI.

Proposed fix
 function errorCode(code: number, isBug: boolean): string {
   if (isBug) return 'internal_error'
   switch (code) {
-    case ExitCodes.Usage: return 'usage_error'
-    case ExitCodes.AuthRequired: return 'auth_required'
-    case ExitCodes.NotReady: return 'not_ready'
-    case ExitCodes.NotFound: return 'not_found'
-    case ExitCodes.Ambiguous: return 'ambiguous_selector'
-    case ExitCodes.CommandNotFound: return 'command_not_found'
-    default: return 'runtime_error'
+    case ExitCodes.Ambiguous: { return 'ambiguous_selector' }
+    case ExitCodes.AuthRequired: { return 'auth_required' }
+    case ExitCodes.CommandNotFound: { return 'command_not_found' }
+    case ExitCodes.NotFound: { return 'not_found' }
+    case ExitCodes.NotReady: { return 'not_ready' }
+    case ExitCodes.Usage: { return 'usage_error' }
+    default: { return 'runtime_error' }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/command.ts` around lines 148 - 158, The switch in
function errorCode violates unicorn/switch-case-braces—wrap each case body in
braces and use explicit breaks (or returns inside braces) to satisfy the rule;
update the cases for ExitCodes.Usage, ExitCodes.AuthRequired,
ExitCodes.NotReady, ExitCodes.NotFound, ExitCodes.Ambiguous, and
ExitCodes.CommandNotFound inside errorCode to use { return '...'; } for each
case (and reorder cases if your linter enforces a specific order) so the switch
follows the lint rule and CI will pass.
packages/cli/src/commands/chats/unpin.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is unused and should be dropped to satisfy lint.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/unpin.ts` at line 5, The import list includes
an unused symbol printSuccess which triggers a lint error; remove printSuccess
from the import statement that currently imports printData, printDryRun,
printSuccess so the import only includes the used symbols (printData and
printDryRun).
packages/cli/src/commands/chats/unmute.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is not used and triggers the reported ESLint error.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/unmute.ts` at line 5, The import statement
currently brings in printData, printDryRun and printSuccess but printSuccess is
unused; edit the import in the module to remove the unused symbol (remove
printSuccess from the import list in the import { printData, printDryRun,
printSuccess } line) and run linting to confirm no remaining references to
printSuccess in the file (check functions or usages around unmute command
handlers).
packages/cli/src/commands/chats/pin.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is unused in this command and should be removed.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/pin.ts` at line 5, The import list in the
chats pin command currently includes an unused symbol `printSuccess`; remove
`printSuccess` from the import statement that reads `import { printData,
printDryRun, printSuccess } from '../../lib/output.js'` so only used functions
(e.g., `printData` and `printDryRun`) are imported to eliminate the unused
import and lint warnings.
packages/cli/src/commands/chats/focus.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is not used in this command and is currently reported by ESLint.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/focus.ts` at line 5, Remove the unused
printSuccess import from the import list in the focus command; edit the import
line that currently reads "import { printData, printDryRun, printSuccess } from
'../../lib/output.js'" and drop printSuccess so only used symbols (printData,
printDryRun) are imported to satisfy ESLint.
packages/cli/src/commands/chats/avatar.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is not referenced and is flagged by ESLint.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/avatar.ts` at line 5, The import statement in
avatar.ts includes an unused symbol printSuccess which ESLint flags; remove
printSuccess from the import list (leave printData and printDryRun intact) so
the import becomes only the used symbols and run lint to confirm no other unused
imports remain.
packages/cli/src/commands/chats/unarchive.ts-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused printSuccess import.

printSuccess is unused in this file and currently lint-failing.

Suggested fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printData, printDryRun } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/chats/unarchive.ts` at line 5, The import list in
unarchive.ts unnecessarily includes printSuccess which is not used and causes a
linter error; remove printSuccess from the import statement that currently reads
"import { printData, printDryRun, printSuccess } from '../../lib/output.js'" so
it only imports the used symbols (printData, printDryRun).
packages/cli/src/commands/send/text.ts-36-36 (1)

36-36: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add blank line before this statement.

The project's ESLint configuration requires a blank line between the preceding statement block and this line.

📏 Proposed fix
     if (flags['dry-run']) {
       await printDryRun('send.text', request, flags.json ? 'json' : 'human')
       return
     }
+
     await printData(await sendMessage(client, request), flags.json ? 'json' : 'human')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/send/text.ts` at line 36, Add a blank line before
the statement that awaits printData(await sendMessage(client, request),
flags.json ? 'json' : 'human'); specifically insert a newline separating the
preceding block from the call that uses sendMessage and printData (which
reference client, request, and flags.json) so the ESLint rule requiring a blank
line before that statement is satisfied.
packages/cli/src/commands/targets/remove.ts-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused import.

printData is imported but never used in this file.

🧹 Proposed fix
-import { printData, printDryRun, printSuccess } from '../../lib/output.js'
+import { printDryRun, printSuccess } from '../../lib/output.js'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/targets/remove.ts` at line 7, Remove the unused
imported symbol printData from the import statement in remove.ts: locate the
import line that brings in printData, printDryRun, and printSuccess and delete
printData so only printDryRun and printSuccess are imported; ensure no other
references to printData remain in the file (e.g., in any helper functions or
exports).
packages/cli/src/commands/resolve/bridge.ts-32-33 (1)

32-33: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate --pick as 1-indexed explicitly.

--pick 0 is currently treated as “not provided” due the truthy check, which breaks the documented 1-indexed behavior.

Suggested fix
-    const selected = flags.pick ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined
-    if (flags.pick && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick: flags.pick, count: candidates.length })
+    if (flags.pick !== undefined && flags.pick < 1) {
+      throw new Error('--pick must be >= 1')
+    }
+    const selected = flags.pick !== undefined ? candidates[flags.pick - 1] : candidates.length === 1 ? candidates[0] : undefined
+    if (flags.pick !== undefined && !selected) throw notFound(`--pick ${flags.pick} is outside the ${candidates.length} matching bridges`, { selector: args.selector, pick: flags.pick, count: candidates.length })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/resolve/bridge.ts` around lines 32 - 33, The code
treats flags.pick as truthy, so --pick 0 is ignored; change the logic to
explicitly detect when a pick was provided and validate it as a 1-indexed
integer within range: use a check like "typeof flags.pick !== 'undefined'" (or
Number.isInteger(flags.pick)) to decide selection (set selected =
candidates[flags.pick - 1] only when pick is a valid integer between 1 and
candidates.length), keep the fallback of selecting the sole candidate when
candidates.length === 1, and if a pick was provided but is out of range throw
the existing notFound(...) with the same context (selector, pick, count).
docs/astro.config.mjs-30-36 (1)

30-36: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Repository links appear to target the wrong GitHub repo.

The docs config points to beeper/desktop-api-cli, while this project context is beeper/cli. Social, edit-link, and command-reference links should be aligned to avoid broken/misdirected docs navigation.

Proposed fix
-          href: 'https://github.com/beeper/desktop-api-cli',
+          href: 'https://github.com/beeper/cli',
@@
-          'https://github.com/beeper/desktop-api-cli/edit/main/docs/',
+          'https://github.com/beeper/cli/edit/main/docs/',
@@
-              link: 'https://github.com/beeper/desktop-api-cli/blob/main/packages/cli/README.md',
+              link: 'https://github.com/beeper/cli/blob/main/packages/cli/README.md',

Also applies to: 87-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/astro.config.mjs` around lines 30 - 36, The repo links in the docs
config point to the wrong GitHub repository; update all references that use
'beeper/desktop-api-cli' to 'beeper/cli' (including the social href entries,
editLink.baseUrl, and any command-reference or similar link fields) so
navigation and edit links resolve to the correct repo; search for occurrences of
the string 'beeper/desktop-api-cli' in this file and replace them with
'beeper/cli' (ensure editLink.baseUrl remains correctly suffixed with 'docs/'
and other link templates keep their existing path structure).
packages/cli/src/commands/send/voice.ts-43-45 (1)

43-45: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Lint error: missing blank line before await printData.

This matches the reported ESLint failure and should be fixed to avoid pipeline breakage.

Proposed fix
     if (flags['dry-run']) {
       await printDryRun('send.voice', request, flags.json ? 'json' : 'human')
       return
     }
+
     await printData(
       await sendMessage(client, request),
       flags.json ? 'json' : 'human',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/send/voice.ts` around lines 43 - 45, The linter
flagged a missing blank line before the call to await printData; fix it by
inserting a single blank line immediately above the statement that calls await
printData(...) (the invocation that awaits sendMessage(client, request) and uses
flags.json ? 'json' : 'human'), keeping the surrounding code and indentation
intact so the statements referencing sendMessage, client, request, printData,
and flags.json remain unchanged.
🧹 Nitpick comments (3)
packages/cli/src/commands/messages/export.ts (1)

26-41: ⚡ Quick win

Dry-run check happens after API calls.

The --dry-run check at line 29 occurs after resolveChatID (line 28), which performs an API call to look up the chat. For a true dry-run that avoids side effects, consider checking flags['dry-run'] before createClient and resolveChatID, and printing a dry-run output with the raw selector string instead of the resolved chatID.

If validating the chat selector in dry-run mode is intentional, this is acceptable, but it's inconsistent with other commands in this PR (e.g., verify/start.ts checks dry-run before API calls).

♻️ Suggested refactor to check dry-run earlier
 async run(): Promise<void> {
   const { flags } = await this.parse(MessagesExport)
   if (flags['before-cursor'] && flags['after-cursor']) throw new Error('Use only one of --before-cursor or --after-cursor')
-  if (flags.output !== '-') ensureWritable(flags)
-  const client = await createClient(flags)
-  const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick })
   if (flags['dry-run']) {
     await printDryRun('messages.export', {
-      chatID,
+      chat: flags.chat,
+      pick: flags.pick,
       output: flags.output,
       beforeCursor: flags['before-cursor'],
       afterCursor: flags['after-cursor'],
       after: flags.after,
       before: flags.before,
       limit: flags.limit,
       asc: flags.asc,
     }, flags.json ? 'json' : 'human')
     return
   }
+  if (flags.output !== '-') ensureWritable(flags)
+  const client = await createClient(flags)
+  const chatID = await resolveChatID(client, flags.chat, { pick: flags.pick })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/messages/export.ts` around lines 26 - 41, The
dry-run flag is checked after performing API calls (createClient and
resolveChatID), causing side effects; move the flags['dry-run'] check to occur
before calling createClient and resolveChatID in messages.export, and when in
dry-run mode call printDryRun('messages.export', {...}) using the raw selector
(flags.chat) and the other CLI flags (output, before-cursor, after-cursor,
after, before, limit, asc) rather than the resolved chatID so no API lookup is
performed; keep printDryRun usage and the json/human format selection the same.
packages/cli/src/commands/contacts/search.ts (1)

35-35: 💤 Low value

Consider simplifying the machine-readable output check.

The expression !isMachineReadableOutput(flags.json ? 'json' : 'human') is functionally equivalent to !flags.json since those are the only two output formats. The current implementation adds indirection without clear benefit.

If additional output formats are planned, the current approach provides future flexibility. Otherwise, consider using !flags.json directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/contacts/search.ts` at line 35, Replace the
indirection in constructing useSpinner by directly using the JSON flag: instead
of computing useSpinner with isMachineReadableOutput(flags.json ? 'json' :
'human'), set useSpinner based on flags.json (i.e., useSpinner should be the
negation of flags.json); if you intend to support more output formats later,
keep the isMachineReadableOutput call but document that intent—refer to the
useSpinner variable and isMachineReadableOutput(flags.json ? 'json' : 'human')
expression and the flags.json flag when making the change.
packages/cli/src/commands/presence.ts (1)

20-28: ⚡ Quick win

Dry-run check happens after API calls.

The --dry-run check at line 25 occurs after resolveChatID (line 24), which performs an API call. For a true dry-run that avoids side effects, consider checking flags['dry-run'] before createClient and resolveChatID.

This is the same pattern as in messages/export.ts. If you want dry-run to validate selectors, this is acceptable, but it means dry-run mode still makes API calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/presence.ts` around lines 20 - 28, The dry-run
branch currently runs after createClient and resolveChatID which cause API
calls; move the flags['dry-run'] check up so it runs before createClient and
resolveChatID to avoid side effects. Specifically, in presence command (around
ensureWritable, createClient, resolveChatID, printDryRun) check flags['dry-run']
immediately after ensureWritable (or before any API-creating calls), and if true
call printDryRun and return; only call createClient and resolveChatID in the
non-dry-run path. If you still need selector validation without API calls,
replace resolveChatID with a local validation step for flags.chat/pick before
exiting in dry-run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1909ded-8979-41e7-934b-9c9261157a6e

📥 Commits

Reviewing files that changed from the base of the PR and between 879f92a and 6d302f3.

⛔ Files ignored due to path filters (3)
  • docs/bun.lock is excluded by !**/*.lock
  • docs/public/favicon.svg is excluded by !**/*.svg
  • docs/src/assets/logo.svg is excluded by !**/*.svg
📒 Files selected for processing (122)
  • README.md
  • docs/.gitignore
  • docs/astro.config.mjs
  • docs/package.json
  • docs/src/content.config.ts
  • docs/src/content/docs/accounts.mdx
  • docs/src/content/docs/api.mdx
  • docs/src/content/docs/auth.mdx
  • docs/src/content/docs/chats.mdx
  • docs/src/content/docs/config.mdx
  • docs/src/content/docs/connect.mdx
  • docs/src/content/docs/contacts.mdx
  • docs/src/content/docs/exit-codes.mdx
  • docs/src/content/docs/export.mdx
  • docs/src/content/docs/index.mdx
  • docs/src/content/docs/install.mdx
  • docs/src/content/docs/media.mdx
  • docs/src/content/docs/messages.mdx
  • docs/src/content/docs/plugins.mdx
  • docs/src/content/docs/presence.mdx
  • docs/src/content/docs/quickstart.mdx
  • docs/src/content/docs/rpc.mdx
  • docs/src/content/docs/scripting.mdx
  • docs/src/content/docs/send.mdx
  • docs/src/content/docs/targets.mdx
  • docs/src/content/docs/update.mdx
  • docs/src/content/docs/watch.mdx
  • docs/src/styles/theme.css
  • docs/tsconfig.json
  • packages/cli/README.md
  • packages/cli/docs/api.md
  • packages/cli/docs/config.md
  • packages/cli/docs/export.md
  • packages/cli/docs/setup.md
  • packages/cli/docs/update.md
  • packages/cli/docs/watch.md
  • packages/cli/package.json
  • packages/cli/scripts/generate-command-map.ts
  • packages/cli/scripts/generate-readme.ts
  • packages/cli/src/commands.generated.ts
  • packages/cli/src/commands/accounts/add.ts
  • packages/cli/src/commands/accounts/remove.ts
  • packages/cli/src/commands/accounts/use.ts
  • packages/cli/src/commands/api/post.ts
  • packages/cli/src/commands/api/request.ts
  • packages/cli/src/commands/auth/email/response.ts
  • packages/cli/src/commands/auth/logout.ts
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/chats/description.ts
  • packages/cli/src/commands/chats/disappear.ts
  • packages/cli/src/commands/chats/draft.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/mute.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/chats/priority.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/chats/rename.ts
  • packages/cli/src/commands/chats/start.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/config/reset.ts
  • packages/cli/src/commands/config/set.ts
  • packages/cli/src/commands/contacts/search.ts
  • packages/cli/src/commands/export.ts
  • packages/cli/src/commands/install/desktop.ts
  • packages/cli/src/commands/install/server.ts
  • packages/cli/src/commands/man.ts
  • packages/cli/src/commands/media/download.ts
  • packages/cli/src/commands/messages/delete.ts
  • packages/cli/src/commands/messages/edit.ts
  • packages/cli/src/commands/messages/export.ts
  • packages/cli/src/commands/messages/search.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/commands/resolve/account.ts
  • packages/cli/src/commands/resolve/bridge.ts
  • packages/cli/src/commands/resolve/chat.ts
  • packages/cli/src/commands/resolve/contact.ts
  • packages/cli/src/commands/resolve/target.ts
  • packages/cli/src/commands/schema.ts
  • packages/cli/src/commands/send/file.ts
  • packages/cli/src/commands/send/react.ts
  • packages/cli/src/commands/send/sticker.ts
  • packages/cli/src/commands/send/text.ts
  • packages/cli/src/commands/send/unreact.ts
  • packages/cli/src/commands/send/voice.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/commands/targets/add/desktop.ts
  • packages/cli/src/commands/targets/add/remote.ts
  • packages/cli/src/commands/targets/add/server.ts
  • packages/cli/src/commands/targets/disable.ts
  • packages/cli/src/commands/targets/enable.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/commands/targets/restart.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/targets/stop.ts
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/update.ts
  • packages/cli/src/commands/verify.ts
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/commands/verify/cancel.ts
  • packages/cli/src/commands/verify/qr-confirm.ts
  • packages/cli/src/commands/verify/qr-scan.ts
  • packages/cli/src/commands/verify/recovery-key.ts
  • packages/cli/src/commands/verify/reset-recovery-key.ts
  • packages/cli/src/commands/verify/sas-confirm.ts
  • packages/cli/src/commands/verify/sas.ts
  • packages/cli/src/commands/verify/start.ts
  • packages/cli/src/commands/watch.ts
  • packages/cli/src/lib/command-metadata.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/lib/errors.ts
  • packages/cli/src/lib/manifest.ts
  • packages/cli/src/lib/output.ts
  • packages/cli/src/lib/resolve.ts
  • packages/cli/test/cli-smoke.ts
  • packages/cli/test/messages-search-validation.test.ts
💤 Files with no reviewable changes (6)
  • packages/cli/docs/api.md
  • packages/cli/docs/update.md
  • packages/cli/docs/watch.md
  • packages/cli/docs/export.md
  • packages/cli/docs/setup.md
  • packages/cli/docs/config.md
✅ Files skipped from review due to trivial changes (18)
  • docs/tsconfig.json
  • docs/.gitignore
  • docs/src/content.config.ts
  • docs/src/content/docs/contacts.mdx
  • docs/src/content/docs/media.mdx
  • docs/src/content/docs/exit-codes.mdx
  • docs/src/content/docs/presence.mdx
  • docs/src/content/docs/export.mdx
  • docs/src/content/docs/watch.mdx
  • docs/src/content/docs/rpc.mdx
  • docs/src/content/docs/auth.mdx
  • docs/src/content/docs/update.mdx
  • docs/src/content/docs/connect.mdx
  • docs/src/content/docs/api.mdx
  • docs/src/content/docs/plugins.mdx
  • README.md
  • packages/cli/src/lib/manifest.ts
  • packages/cli/README.md

Comment thread packages/cli/src/commands/chats/disappear.ts Outdated
Comment thread packages/cli/src/commands/chats/unremind.ts Outdated
Comment thread packages/cli/src/commands/resolve/contact.ts Outdated
Comment thread packages/cli/src/commands/schema.ts Outdated
Comment thread packages/cli/src/commands/schema.ts Outdated
Comment thread packages/cli/src/commands/verify/approve.ts Outdated
Comment thread packages/cli/src/lib/command-metadata.ts Outdated
Comment thread packages/cli/src/lib/command-metadata.ts Outdated
Comment thread packages/cli/src/lib/output.ts Outdated
Comment thread packages/cli/src/lib/resolve.ts Outdated
Comment thread packages/cli/src/lib/server-env.ts
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: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/cli/README.md (1)

9-9: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace example.com placeholder with actual documentation URL.

The README contains multiple references to example.com as a placeholder for the documentation site. Before merging, these should be updated to the actual documentation URL.

Occurrences:

  • Line 9: docs badge
  • Line 25: "Read the docs" link
  • Lines 205-220: Documentation topic table

Also applies to: 25-25, 205-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/README.md` at line 9, Update the placeholder documentation URL
in packages/cli/README.md by replacing all occurrences of "example.com" with the
actual docs URL: update the docs badge link (the line containing the badge
image), the "Read the docs" anchor, and every link in the documentation topic
table (previously lines showing topics) so they point to the real documentation
site; double-check for any other "example.com" occurrences in the file and
replace them consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/lib/installations.ts`:
- Line 151: installDesktop() currently throws when request.serverEnv !== 'prod',
which breaks non-prod installs; change the install behavior to stop hard-failing
and instead map the desktop update feed to 'prod' while still passing the
original request.serverEnv to the launched app: in installDesktop(), remove the
throw for request.serverEnv !== 'prod', introduce a local variable (e.g.,
feedEnv) that is set to 'prod' when request.serverEnv !== 'prod' and use feedEnv
for any feed/update URLs, but continue to pass request.serverEnv (or
target.serverEnv) into the app launch/config so the app sees the requested
environment; also add a short warning log when mapping non-prod -> prod so users
know the feed was remapped.

---

Outside diff comments:
In `@packages/cli/README.md`:
- Line 9: Update the placeholder documentation URL in packages/cli/README.md by
replacing all occurrences of "example.com" with the actual docs URL: update the
docs badge link (the line containing the badge image), the "Read the docs"
anchor, and every link in the documentation topic table (previously lines
showing topics) so they point to the real documentation site; double-check for
any other "example.com" occurrences in the file and replace them consistently.
🪄 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: CHILL

Plan: Pro

Run ID: 189263df-a84a-4970-9808-6c723d61c15d

📥 Commits

Reviewing files that changed from the base of the PR and between 6d302f3 and 12ed611.

📒 Files selected for processing (11)
  • docs/src/content/docs/targets.mdx
  • packages/cli/README.md
  • packages/cli/src/commands/install/server.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/commands/targets/add/desktop.ts
  • packages/cli/src/commands/targets/add/server.ts
  • packages/cli/src/lib/installations.ts
  • packages/cli/src/lib/manifest.ts
  • packages/cli/src/lib/server-env.ts
  • packages/cli/src/lib/targets.ts
  • packages/cli/test/cli-smoke.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/src/content/docs/targets.mdx
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/cli/src/commands/install/server.ts
  • packages/cli/src/commands/targets/add/server.ts
  • packages/cli/src/lib/manifest.ts
  • packages/cli/src/commands/setup.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (ubuntu-latest / bun 1.3.10)
  • GitHub Check: test (ubuntu-latest / bun 1.3.10)
  • GitHub Check: test (macos-latest / bun 1.3.10)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-27T22:40:16.795Z
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: README.md:178-178
Timestamp: 2026-05-27T22:40:16.795Z
Learning: In beeper CLI documentation, the correct recovery-key verification command syntax is:
`beeper verify recovery-key -t <target> --key "<key>"`.
Use the top-level `verify` subcommand and pass the recovery key via the `--key` flag. Do not use the previously incorrect form like `beeper auth verify recovery-key --code KEY`—update any doc snippets to match the supported syntax.

Applied to files:

  • packages/cli/README.md
🔇 Additional comments (7)
packages/cli/src/lib/targets.ts (1)

6-6: LGTM!

Also applies to: 205-205

packages/cli/src/lib/server-env.ts (1)

1-16: LGTM!

packages/cli/test/cli-smoke.ts (1)

252-252: LGTM!

Also applies to: 284-311, 346-346

packages/cli/src/commands/targets/add/desktop.ts (1)

12-12: LGTM!

Also applies to: 19-22

packages/cli/src/lib/installations.ts (1)

92-93: LGTM!

Also applies to: 106-106, 120-120

packages/cli/README.md (2)

452-452: LGTM!

Also applies to: 501-501, 594-594, 623-623


2578-2578: LGTM!

Comment thread packages/cli/src/lib/installations.ts Outdated
Comment thread packages/cli/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: 5

♻️ Duplicate comments (1)
packages/cli/src/lib/command.ts (1)

50-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Non-CLIError validation failures are being mislabeled as internal bugs.

Line 50 now marks every non-CLIError as isBug. Several commands still throw plain Error for user validation, so expected usage failures can surface as kind: "bug" / internal_error and show the bug panel.

Proposed fix
-    const isBug = error instanceof BugError || !(error instanceof CLIError)
+    const isBug = error instanceof BugError || (!(error instanceof CLIError) && inferredCode === undefined)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/command.ts` at line 50, The current logic sets isBug for
any error that isn't a CLIError, causing plain Error-based validation failures
to be treated as internal bugs; update the check in command.ts so only explicit
BugError instances are treated as bugs (e.g., replace const isBug = error
instanceof BugError || !(error instanceof CLIError) with const isBug = error
instanceof BugError) and ensure any user-facing validation errors are thrown as
CLIError (or converted earlier) so they aren't misclassified.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/presence.ts`:
- Line 26: Add a blank line before the ensureWritable(flags) statement to
satisfy the `@stylistic/padding-line-between-statements` lint rule; locate the
ensureWritable(flags) call in the presence command (ensureWritable) and insert
one empty line above it so there is padding between the previous statement/block
and the ensureWritable(flags) invocation.

In `@packages/cli/src/commands/schema.ts`:
- Around line 27-29: The code builds requested by filtering this.argv which
contains raw tokens (flags and their values); change it to use the parsed argv
returned from await this.parse(Schema) (e.g., const parsed = await
this.parse(Schema)) and filter parsed.argv instead of this.argv so flag values
like "json" aren’t mistaken for positional paths; update the pathArgs
computation (variable name pathArgs) and the requested assignment to use
parsed.argv.

In `@packages/cli/src/commands/verify/approve.ts`:
- Line 16: Add a blank line before the ensureWritable(flags) call in the
approve.ts file to satisfy the `@stylistic/padding-line-between-statements` rule;
locate the ensureWritable(flags) invocation (and the surrounding write/client
setup code that uses flags) and insert a single empty line immediately above it
so the statement is separated from the preceding block.

In `@packages/cli/src/lib/command.ts`:
- Around line 147-167: The switch in errorCode is triggering stylistic lint
errors because there are no blank lines between case blocks; update the switch
in the errorCode function (the case entries for ExitCodes.Ambiguous,
AuthRequired, CommandNotFound, NotFound, NotReady, Usage, and the default) to
include a blank line between each case block (i.e., a blank line after each
closing brace/return) so `@stylistic/padding-line-between-statements` is
satisfied.

In `@packages/npm/scripts/build.ts`:
- Line 48: The generated launcher() function returns a template literal that
itself contains unescaped/backticked template literals (e.g., the line
containing console.error(`beeper-cli does not ship a binary for
${process.platform}/${process.arch}.`)), which prematurely close the outer
string; fix by replacing inner backticks with regular quotes or string
concatenation (or escape the inner backticks) inside launcher() so the outer
`return \`...\`` remains valid; search for other occurrences inside launcher()
of nested backticks and change them to use single/double quotes or +
concatenation, ensuring any ${...} interpolations intended in the generated code
remain correct.

---

Duplicate comments:
In `@packages/cli/src/lib/command.ts`:
- Line 50: The current logic sets isBug for any error that isn't a CLIError,
causing plain Error-based validation failures to be treated as internal bugs;
update the check in command.ts so only explicit BugError instances are treated
as bugs (e.g., replace const isBug = error instanceof BugError || !(error
instanceof CLIError) with const isBug = error instanceof BugError) and ensure
any user-facing validation errors are thrown as CLIError (or converted earlier)
so they aren't misclassified.
🪄 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: CHILL

Plan: Pro

Run ID: 80a53ca3-850f-4182-af2c-c5f19ff59e28

📥 Commits

Reviewing files that changed from the base of the PR and between 12ed611 and f22ce03.

📒 Files selected for processing (45)
  • README.md
  • docs/astro.config.mjs
  • packages/cli/README.md
  • packages/cli/scripts/generate-readme.ts
  • packages/cli/src/commands/accounts/add.ts
  • packages/cli/src/commands/auth/logout.ts
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/chats/description.ts
  • packages/cli/src/commands/chats/disappear.ts
  • packages/cli/src/commands/chats/draft.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/install/server.ts
  • packages/cli/src/commands/messages/export.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/commands/resolve/account.ts
  • packages/cli/src/commands/resolve/bridge.ts
  • packages/cli/src/commands/resolve/contact.ts
  • packages/cli/src/commands/schema.ts
  • packages/cli/src/commands/send/file.ts
  • packages/cli/src/commands/send/text.ts
  • packages/cli/src/commands/send/voice.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/commands/targets/add/desktop.ts
  • packages/cli/src/commands/targets/add/server.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/update.ts
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/lib/command-metadata.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/lib/installations.ts
  • packages/cli/src/lib/output.ts
  • packages/cli/src/lib/resolve.ts
  • packages/npm/scripts/build.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/chats/draft.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/send/text.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/send/file.ts
  • packages/cli/src/commands/targets/add/server.ts
  • packages/cli/src/commands/chats/pin.ts
  • docs/astro.config.mjs
  • packages/cli/src/commands/targets/add/desktop.ts
  • packages/cli/src/lib/command-metadata.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/scripts/generate-readme.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/accounts/add.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-05-27T22:40:16.795Z
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: README.md:178-178
Timestamp: 2026-05-27T22:40:16.795Z
Learning: In beeper CLI documentation, the correct recovery-key verification command syntax is:
`beeper verify recovery-key -t <target> --key "<key>"`.
Use the top-level `verify` subcommand and pass the recovery key via the `--key` flag. Do not use the previously incorrect form like `beeper auth verify recovery-key --code KEY`—update any doc snippets to match the supported syntax.

Applied to files:

  • README.md
  • packages/cli/README.md
🪛 Biome (2.4.15)
packages/npm/scripts/build.ts

[error] 48-48: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 48-48: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 48-48: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 48-48: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 48-48: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 48-48: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 48-48: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 48-48: expected ( but instead found $

(parse)


[error] 48-48: expected ; but instead found {

(parse)


[error] 48-48: expected , but instead found .

(parse)


[error] 48-48: expected ; but instead found {

(parse)


[error] 48-48: expected , but instead found .

(parse)


[error] 48-48: Expected an identifier but instead found '`'.

(parse)


[error] 58-58: expected ) but instead found beeper

(parse)


[error] 58-58: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 58-58: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 60-60: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 62-62: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 63-63: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 64-64: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 66-66: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 68-68: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 73-73: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 75-75: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 77-77: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 78-78: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 79-79: await is only allowed within async functions and at the top levels of modules.

(parse)


[error] 80-80: await is only allowed within async functions and at the top levels of modules.

(parse)

🪛 ESLint
packages/cli/src/commands/presence.ts

[error] 26-26: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/chats/disappear.ts

[error] 26-26: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/messages/export.ts

[error] 40-40: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/resolve/account.ts

[error] 25-25: Use object destructuring.

(prefer-destructuring)


[error] 26-26: Unexpected negated condition.

(unicorn/no-negated-condition)

packages/cli/src/commands/resolve/contact.ts

[error] 33-33: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 33-33: Use .length === 0 when checking length is zero.

(unicorn/explicit-length-check)


[error] 34-34: Use object destructuring.

(prefer-destructuring)


[error] 35-35: Unexpected negated condition.

(unicorn/no-negated-condition)

packages/cli/src/lib/installations.ts

[error] 157-157: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/lib/command.ts

[error] 150-150: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 153-153: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 156-156: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 159-159: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 162-162: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 165-165: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/auth/logout.ts

[error] 17-17: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/resolve/bridge.ts

[error] 32-32: Use object destructuring.

(prefer-destructuring)


[error] 33-33: Unexpected negated condition.

(unicorn/no-negated-condition)

packages/cli/src/commands/schema.ts

[error] 124-124: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 127-127: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 130-130: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 133-133: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/verify/approve.ts

[error] 16-16: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/lib/output.ts

[error] 127-127: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 131-131: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

🪛 GitHub Actions: CI / 0_test (ubuntu-latest _ bun 1.3.10).txt
packages/npm/scripts/build.ts

[error] 48-48: Typecheck failed at console.error template string. Error(s): Expected ';' but found 'beeper'; Expected ';' but found 'does'; Expected ';' but found 'ship'; Expected ';' but found 'binary'; Expected '(' but found '$'; Expected '}' but found '.'; Expected ';' but found 'platform'; Unexpected '}'.

🪛 GitHub Actions: CI / 1_test (macos-latest _ bun 1.3.10).txt
packages/npm/scripts/build.ts

[error] 48-48: typecheck failed: Syntax error in console.error template string. Error: Expected ";" but found "beeper". (at packages/npm/scripts/build.ts:48:18)


[error] 48-48: typecheck failed: Syntax error. Error: Expected ";" but found "does". (at packages/npm/scripts/build.ts:48:29)


[error] 48-48: typecheck failed: Syntax error. Error: Expected ";" but found "ship". (at packages/npm/scripts/build.ts:48:38)


[error] 48-48: typecheck failed: Syntax error. Error: Expected ";" but found "binary". (at packages/npm/scripts/build.ts:48:45)


[error] 48-48: typecheck failed: Syntax error. Error: Expected "(" but found "$". (at packages/npm/scripts/build.ts:48:56)


[error] 48-48: typecheck failed: Syntax error. Error: Expected "}" but found ".". (at packages/npm/scripts/build.ts:48:65)


[error] 48-48: typecheck failed: Syntax error. Error: Expected ";" but found "platform". (at packages/npm/scripts/build.ts:48:66)


[error] 48-48: typecheck failed: Syntax error. Error: Unexpected "}". (at packages/npm/scripts/build.ts:48:74)

🪛 GitHub Actions: CI / test (macos-latest _ bun 1.3.10)
packages/npm/scripts/build.ts

[error] 48-48: Typecheck failed: Expected ';' but found 'beeper'.


[error] 48-48: Typecheck failed: Expected ';' but found 'does'.


[error] 48-48: Typecheck failed: Expected ';' but found 'ship'.


[error] 48-48: Typecheck failed: Expected ';' but found 'binary'.


[error] 48-48: Typecheck failed: Expected '(' but found '$'.


[error] 48-48: Typecheck failed: Expected '}' but found '.'.


[error] 48-48: Typecheck failed: Expected ';' but found 'platform'.


[error] 48-48: Typecheck failed: Expected ';' but found 'does'. (Also reported: Unexpected '}' at line 48.)

🪛 GitHub Actions: CI / test (ubuntu-latest _ bun 1.3.10)
packages/npm/scripts/build.ts

[error] 48-48: TypeScript typecheck failed: Expected ";" but found "beeper". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:18)


[error] 48-48: TypeScript typecheck failed: Expected ";" but found "does". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:29)


[error] 48-48: TypeScript typecheck failed: Expected ";" but found "ship". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:38)


[error] 48-48: TypeScript typecheck failed: Expected ";" but found "a". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:45)


[error] 48-48: TypeScript typecheck failed: Expected "(" but found "$". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:56)


[error] 48-48: TypeScript typecheck failed: Expected "}" but found ".". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:65)


[error] 48-48: TypeScript typecheck failed: Expected ";" but found "platform". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:66)


[error] 48-48: TypeScript typecheck failed: Unexpected "}". (at /home/runner/work/cli/cli/packages/npm/scripts/build.ts:48:74)

🔇 Additional comments (19)
packages/cli/src/commands/send/voice.ts (1)

24-26: LGTM!

Also applies to: 38-38, 42-44, 46-46

packages/cli/src/commands/chats/description.ts (1)

5-5: LGTM!

packages/cli/src/commands/chats/archive.ts (1)

5-5: LGTM!

packages/cli/src/commands/update.ts (1)

26-26: LGTM!

README.md (1)

8-10: LGTM!

Also applies to: 25-25, 205-205, 210-217

packages/cli/src/commands/chats/disappear.ts (1)

23-29: LGTM!

packages/cli/src/commands/install/server.ts (1)

6-6: LGTM!

Also applies to: 12-12

packages/cli/src/commands/setup.ts (1)

39-39: LGTM!

Also applies to: 173-174, 711-722

packages/cli/README.md (1)

9-10: LGTM!

Also applies to: 25-25, 205-217, 452-452, 501-501, 594-594, 623-623, 2578-2579

packages/cli/src/commands/auth/logout.ts (1)

10-10: LGTM!

Also applies to: 17-19

packages/cli/src/commands/resolve/bridge.ts (1)

32-34: LGTM!

packages/cli/src/commands/schema.ts (1)

121-135: LGTM!

packages/cli/src/lib/output.ts (1)

102-103: LGTM!

Also applies to: 127-134, 289-289

packages/cli/src/commands/messages/export.ts (1)

28-30: LGTM!

Also applies to: 40-42

packages/cli/src/commands/resolve/account.ts (1)

25-27: LGTM!

packages/cli/src/lib/resolve.ts (1)

39-40: LGTM!

packages/cli/src/commands/resolve/contact.ts (1)

28-30: LGTM!

Also applies to: 34-36, 58-63

packages/cli/src/commands/chats/mark-read.ts (1)

5-5: LGTM!

Also applies to: 17-19

packages/cli/src/lib/installations.ts (1)

151-157: LGTM!

Comment thread packages/cli/src/commands/presence.ts Outdated
Comment thread packages/cli/src/commands/schema.ts Outdated
Comment thread packages/cli/src/commands/verify/approve.ts Outdated
Comment thread packages/cli/src/lib/command.ts Outdated
Comment thread packages/npm/scripts/build.ts Outdated
Comment thread packages/cli/src/lib/command.ts Outdated
Comment thread packages/npm/scripts/build.ts Outdated
Comment thread packages/cli/src/commands/schema.ts Outdated
Comment thread .claude/scheduled_tasks.lock 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: 3

🧹 Nitpick comments (4)
packages/cli/src/commands/api/request.ts (1)

28-32: ⚡ Quick win

Resolve the @stylistic/padding-line-between-statements lint errors.

Static analysis flags missing blank lines before the statements at Lines 28 and 32. Add the expected blank lines to pass the lint check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/api/request.ts` around lines 28 - 32, Add the
missing blank lines to satisfy the padding-line-between-statements lint rule by
inserting a blank line before the conditional branch and before the final await
call: ensure there is an empty line before the "if (flags['no-auth']) { … }"
block's first statement that calls printData/appRequest and also an empty line
before the final "await printData(await appRequest(...), format)" call; locate
the usage of flags['no-auth'], appRequest, printData, method and args.path in
request.ts and add the blank lines accordingly.
packages/cli/src/commands/api/post.ts (1)

28-37: ⚡ Quick win

Resolve the @stylistic/padding-line-between-statements lint errors.

Static analysis flags missing blank lines before the statements at Lines 28, 32, and 36, which will fail the lint check. Add the expected blank lines before these block statements.

♻️ Proposed fix
     } catch {
       throw new Error(`--body is not valid JSON: ${flags.body}`)
     }
+
     if (flags['dry-run']) {
       await printDryRun('api.post', { method: 'POST', path: args.path, body, noAuth: flags['no-auth'], target: flags.target, baseURL: flags['base-url'] }, format)
       return
     }
+
     if (flags['no-auth']) {
       await printData(await appRequest('POST', args.path, { baseURL: flags['base-url'], body, target: flags.target, token: false }), format)
       return
     }
+
     const client = await createClient(flags)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/api/post.ts` around lines 28 - 37, The lint rule
requires blank lines separating top-level statement blocks; add a single empty
line before the "if (flags['dry-run'])" block, before the "if
(flags['no-auth'])" block, and before the "const client = await
createClient(flags)" statement in this function so there is a blank line between
each branch; keep the existing code and callsites (printDryRun, appRequest,
printData, createClient, client.post) unchanged and only insert those padding
blank lines.
packages/cli/src/commands/auth/logout.ts (1)

32-32: ⚡ Quick win

Resolve the @stylistic/padding-line-between-statements lint error.

Static analysis flags a missing blank line before the printSuccess statement at Line 32.

♻️ Proposed fix
       await clearTargetAuth(target)
     }
+
     await printSuccess({ message: 'Logged out', detail: token ? 'local token cleared' : 'no token was stored', data: { revoked, hadToken: Boolean(token) } }, format)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/auth/logout.ts` at line 32, Add a blank line before
the printSuccess invocation to satisfy the
`@stylistic/padding-line-between-statements` rule: inside the logout command in
logout.ts, insert a single empty line immediately above the await
printSuccess(...) call (the call that uses token, revoked and hadToken) so there
is a separating blank line between the preceding statement and the printSuccess
await.
packages/cli/src/commands/config/set.ts (1)

18-22: ⚡ Quick win

Resolve the @stylistic/padding-line-between-statements lint error.

Static analysis flags a missing blank line before the updateConfig statement at Line 22.

♻️ Proposed fix
     if (flags['dry-run']) {
       await printDryRun('config.set', { [args.key]: nextValue }, format)
       return
     }
+
     await updateConfig(config => ({ ...config, [args.key]: nextValue }))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/config/set.ts` around lines 18 - 22, The linter
wants a blank line separating the conditional dry-run branch from the subsequent
side-effect call; inside the config.set logic, add one empty line between the
end of the if (flags['dry-run']) { ... return } block and the await
updateConfig(...) call so that printDryRun/return is visually separated from
updateConfig; locate the flags['dry-run'] check and the await updateConfig(...)
invocation and insert a single blank line before updateConfig.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/api/get.ts`:
- Around line 19-26: Add a blank line before the declaration of const client to
satisfy the padding-line-between-statements rule: after the early-return branch
that calls printData(await appRequest(...)) leave one empty line, then declare
const client = await createClient(flags) and continue with await printData(await
client.get(args.path), format); this touches the block using format, appRequest,
createClient, printData, flags and args.path.

In `@packages/cli/src/commands/config/reset.ts`:
- Around line 12-17: The padding-line lint rule is failing because there is no
blank line between the early return block and the subsequent reset call; add a
single blank line between the if-block that handles flags['dry-run']/printDryRun
and the await resetConfig() call so the sequence (printDryRun -> return) is
visually separated from resetConfig(), then keep the following await
printSuccess({ message: 'Config reset' }, format) as-is; this involves editing
the code around printDryRun, resetConfig, and printSuccess in
packages/cli/src/commands/config/reset.ts.

In `@packages/cli/src/commands/verify/cancel.ts`:
- Around line 16-18: Add a blank line before the declaration of const client in
the verify cancel command to satisfy the
`@stylistic/padding-line-between-statements` ESLint rule: locate the function in
cancel.ts where createClient(flags) is called, and insert an empty line between
the preceding statement/block and the line calling createClient so the sequence
reads with a blank line before const client; keep the rest of the logic using
createClient, client.app.verifications.cancel(flags.id ?? 'active', {}), and
printData unchanged.

---

Nitpick comments:
In `@packages/cli/src/commands/api/post.ts`:
- Around line 28-37: The lint rule requires blank lines separating top-level
statement blocks; add a single empty line before the "if (flags['dry-run'])"
block, before the "if (flags['no-auth'])" block, and before the "const client =
await createClient(flags)" statement in this function so there is a blank line
between each branch; keep the existing code and callsites (printDryRun,
appRequest, printData, createClient, client.post) unchanged and only insert
those padding blank lines.

In `@packages/cli/src/commands/api/request.ts`:
- Around line 28-32: Add the missing blank lines to satisfy the
padding-line-between-statements lint rule by inserting a blank line before the
conditional branch and before the final await call: ensure there is an empty
line before the "if (flags['no-auth']) { … }" block's first statement that calls
printData/appRequest and also an empty line before the final "await
printData(await appRequest(...), format)" call; locate the usage of
flags['no-auth'], appRequest, printData, method and args.path in request.ts and
add the blank lines accordingly.

In `@packages/cli/src/commands/auth/logout.ts`:
- Line 32: Add a blank line before the printSuccess invocation to satisfy the
`@stylistic/padding-line-between-statements` rule: inside the logout command in
logout.ts, insert a single empty line immediately above the await
printSuccess(...) call (the call that uses token, revoked and hadToken) so there
is a separating blank line between the preceding statement and the printSuccess
await.

In `@packages/cli/src/commands/config/set.ts`:
- Around line 18-22: The linter wants a blank line separating the conditional
dry-run branch from the subsequent side-effect call; inside the config.set
logic, add one empty line between the end of the if (flags['dry-run']) { ...
return } block and the await updateConfig(...) call so that printDryRun/return
is visually separated from updateConfig; locate the flags['dry-run'] check and
the await updateConfig(...) invocation and insert a single blank line before
updateConfig.
🪄 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: CHILL

Plan: Pro

Run ID: 888ea263-2436-4542-a190-b1662ae9d9a0

📥 Commits

Reviewing files that changed from the base of the PR and between f22ce03 and fefbf4e.

📒 Files selected for processing (53)
  • .gitignore
  • packages/cli/src/commands/api/get.ts
  • packages/cli/src/commands/api/post.ts
  • packages/cli/src/commands/api/request.ts
  • packages/cli/src/commands/auth/email/response.ts
  • packages/cli/src/commands/auth/logout.ts
  • packages/cli/src/commands/bridges/show.ts
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/chats/description.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/config/reset.ts
  • packages/cli/src/commands/config/set.ts
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/media/download.ts
  • packages/cli/src/commands/messages/search.ts
  • packages/cli/src/commands/plugins/available.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/commands/resolve/chat.ts
  • packages/cli/src/commands/schema.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/commands/targets/disable.ts
  • packages/cli/src/commands/targets/enable.ts
  • packages/cli/src/commands/targets/list.ts
  • packages/cli/src/commands/targets/logs.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/commands/targets/restart.ts
  • packages/cli/src/commands/targets/show.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/targets/status.ts
  • packages/cli/src/commands/targets/stop.ts
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/commands/verify/cancel.ts
  • packages/cli/src/lib/command-metadata.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/lib/export/index.ts
  • packages/cli/src/lib/ink/render.tsx
  • packages/cli/src/lib/ink/theme.ts
  • packages/cli/src/lib/installations.ts
  • packages/cli/src/lib/profiles.ts
  • packages/cli/src/lib/resolve.ts
  • packages/cli/src/lib/setup-login.ts
  • packages/npm/scripts/build.ts
💤 Files with no reviewable changes (14)
  • packages/cli/src/lib/export/index.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/chats/description.ts
✅ Files skipped from review due to trivial changes (7)
  • .gitignore
  • packages/cli/src/commands/plugins/available.ts
  • packages/cli/src/commands/targets/list.ts
  • packages/cli/src/lib/ink/theme.ts
  • packages/cli/src/commands/targets/logs.ts
  • packages/cli/src/commands/targets/show.ts
  • packages/cli/src/commands/targets/status.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • packages/cli/src/commands/media/download.ts
  • packages/cli/src/commands/targets/enable.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/targets/disable.ts
  • packages/cli/src/commands/targets/restart.ts
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/commands/schema.ts
  • packages/cli/src/lib/command-metadata.ts
  • packages/npm/scripts/build.ts
  • packages/cli/src/commands/auth/email/response.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/lib/installations.ts
  • packages/cli/src/lib/command.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (macos-latest / bun 1.3.10)
  • GitHub Check: test (ubuntu-latest / bun 1.3.10)
  • GitHub Check: test (ubuntu-latest / bun 1.3.10)
🧰 Additional context used
🪛 ESLint
packages/cli/src/commands/config/set.ts

[error] 22-22: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/auth/logout.ts

[error] 32-32: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/api/post.ts

[error] 28-28: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 32-32: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 36-36: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/setup.ts

[error] 140-140: Use switch instead of multiple else-if.

(unicorn/prefer-switch)

packages/cli/src/commands/api/get.ts

[error] 24-24: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/api/request.ts

[error] 28-28: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 32-32: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/config/reset.ts

[error] 16-16: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/verify/cancel.ts

[error] 17-17: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

🔇 Additional comments (13)
packages/cli/src/commands/resolve/chat.ts (1)

19-59: LGTM!

packages/cli/src/commands/setup.ts (2)

110-197: LGTM!


39-39: ⚡ Quick win

Update concern: default/alias regression scope and oclif ordering. This CLI defines --server-env with default: 'prod' (not 'production'), and options: [...SERVER_ENVIRONMENTS] plus parse: normalizeServerEnv. normalizeServerEnv maps 'production''prod', but whether 'production' is accepted depends on whether oclif validates the options list against the raw input before running the custom parse function; the available evidence doesn’t show that ordering precisely.

packages/cli/src/commands/targets/stop.ts (1)

13-22: LGTM!

packages/cli/src/commands/targets/use.ts (1)

9-21: LGTM!

packages/cli/src/commands/api/request.ts (1)

24-27: 💤 Low value

Confirm intentional GET pass-through under --dry-run.

--dry-run only short-circuits for non-GET methods, so api request GET … --dry-run still performs the live request. This is consistent with the ensureWritable gating at Line 21 (reads are side-effect-free), so it looks intentional—just confirming it's expected.

packages/cli/src/commands/bridges/show.ts (1)

33-39: LGTM!

packages/cli/src/commands/doctor.ts (1)

13-19: LGTM!

packages/cli/src/commands/messages/search.ts (1)

61-68: LGTM!

packages/cli/src/lib/ink/render.tsx (1)

1-1: LGTM!

Also applies to: 31-31, 130-143

packages/cli/src/lib/profiles.ts (1)

148-162: LGTM!

packages/cli/src/lib/resolve.ts (1)

60-103: LGTM!

packages/cli/src/lib/setup-login.ts (1)

31-43: LGTM!

Comment thread packages/cli/src/commands/api/get.ts Outdated
Comment thread packages/cli/src/commands/config/reset.ts Outdated
Comment thread packages/cli/src/commands/verify/cancel.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

Caution

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

⚠️ Outside diff range comments (3)
packages/cli/src/commands/setup.ts (1)

53-76: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

setup --dry-run still requires a writable environment.

Line 54 runs ensureWritable(flags) before the new dry-run early return, so the preview fails anywhere the CLI config is read-only. Since this branch only prints derived actions, it should behave like the auth logout dry-run path and skip the write precondition.

🔧 Minimal fix
   async run(): Promise<void> {
     const { flags } = await this.parse(Setup)
-    ensureWritable(flags)
     const targetModeCount = [Boolean(flags.remote), flags.server, flags.desktop].filter(Boolean).length
     if (targetModeCount > 1) throw new Error('Specify at most one of --remote, --server, or --desktop')
     const authModeCount = [flags.local, flags.oauth, Boolean(flags.email)].filter(Boolean).length
     if (authModeCount > 1) throw new Error('Specify at most one of --local, --oauth, or --email')
     if ((flags.local || flags.oauth) && (flags.remote || flags.server || flags.desktop)) {
       throw new Error('Use --local or --oauth with an existing target, not with --remote, --server, or --desktop.')
     }
+    if (!flags['dry-run']) ensureWritable(flags)
     if (flags['dry-run']) {
       await printDryRun('setup', {
         target: flags.target,
         baseURL: flags['base-url'],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/setup.ts` around lines 53 - 76, The dry-run path
currently calls ensureWritable(flags) before deciding to return, causing
read-only environments to fail; update setup command logic so the dry-run
shortcut is checked and returned before calling ensureWritable(flags) (or make
ensureWritable a no-op when flags['dry-run'] is true): move the flags['dry-run']
check (which calls printDryRun) to run prior to invoking ensureWritable(flags)
(or add an early return when flags['dry-run'] is set) so printDryRun can run in
read-only configs; touch the Setup command entry where ensureWritable and
printDryRun are used to implement this change.
packages/cli/src/commands/auth/logout.ts (2)

21-32: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the missing separator before printSuccess().

Line 32 still violates @stylistic/padding-line-between-statements, which will keep CI red.

🎨 Minimal fix
     if (token) {
       const response = await fetch(new URL('/oauth/revoke', target.baseURL), {
         method: 'POST',
         headers: { 'content-type': 'application/x-www-form-urlencoded' },
         body: new URLSearchParams({ token, token_type_hint: 'access_token' }),
         signal: AbortSignal.timeout(5000),
       }).catch(() => undefined)
       revoked = Boolean(response?.ok)
       await clearTargetAuth(target)
     }
+
     await printSuccess({ message: 'Logged out', detail: token ? 'local token cleared' : 'no token was stored', data: { revoked, hadToken: Boolean(token) } }, format)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/auth/logout.ts` around lines 21 - 32, The last
statement calling printSuccess(...) needs a blank separator line before it to
satisfy stylistic rules; insert a single empty line between the token-handling
block (where you call clearTargetAuth(target) and compute revoked) and the final
await printSuccess(...) call so there is padding between the if/await block and
the printSuccess invocation; target symbols to locate are the revoked/token
variables, the fetch + clearTargetAuth(target) sequence, and the await
printSuccess(...) call.

14-20: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dry-run hides the BEEPER_ACCESS_TOKEN failure path.

When auth only comes from BEEPER_ACCESS_TOKEN, the real command throws at Lines 18-20, but --dry-run now returns a normal payload with hadToken: false. That makes the preview lie about whether logout can actually proceed.

🔧 Minimal fix
     const format = flags.json ? 'json' : 'human'
     const target = await resolveTarget({ target: flags.target, baseURL: flags['base-url'] })
     const token = target.auth?.accessToken
+
+    if (process.env.BEEPER_ACCESS_TOKEN && !token) {
+      throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.')
+    }
+
     if (flags['dry-run']) {
       await printDryRun('auth.logout', { target: target.id, baseURL: target.baseURL, hadToken: Boolean(token), revokeToken: Boolean(token) }, format)
       return
     }
-    if (process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken) {
-      throw new Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.')
-    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/auth/logout.ts` around lines 14 - 20, The dry-run
path currently returns a normal payload even when logout would fail due to
BEEPER_ACCESS_TOKEN being set and no target.auth?.accessToken; move or duplicate
the env check so the same error is thrown during dry-run: before calling
printDryRun('auth.logout', ...) check if process.env.BEEPER_ACCESS_TOKEN &&
!target.auth?.accessToken and throw the same Error('auth logout cannot clear
BEEPER_ACCESS_TOKEN from the environment; unset it in the calling process.'), so
printDryRun only runs when the real command would proceed; reference
flags['dry-run'], printDryRun, process.env.BEEPER_ACCESS_TOKEN, and
target.auth?.accessToken.
♻️ Duplicate comments (1)
packages/cli/src/commands/api/get.ts (1)

24-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore the blank line before const client.

CI still reports @stylistic/padding-line-between-statements here after the early return block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/api/get.ts` at line 24, Add a blank line before the
statement "const client = await createClient(flags)" so there is a padding line
separating it from the preceding early-return block; locate the code in the get
command where createClient(flags) is awaited and ensure a single empty line
exists above that const declaration to satisfy
`@stylistic/padding-line-between-statements`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/api/post.ts`:
- Around line 28-37: Add missing blank lines between the conditional branches in
the api.post command: insert a blank line before the if (flags['dry-run'])
branch, another blank line before the if (flags['no-auth']) branch, and a blank
line before the const client = await createClient(flags) statement so the
sequence of checks involving flags['dry-run'], printDryRun, flags['no-auth'],
appRequest, createClient and client.post complies with
`@stylistic/padding-line-between-statements`.

In `@packages/cli/src/commands/api/request.ts`:
- Around line 24-32: Add the required blank line padding between the conditional
blocks in the api.request flow: insert a blank line before the "if
(flags['no-auth'])" branch and another blank line before the final "await
printData(await appRequest(...))" call so that the sequence (dry-run -> no-auth
-> default) has separating empty lines; adjust the block around printDryRun,
appRequest, and printData to satisfy `@stylistic/padding-line-between-statements`
without changing logic.

In `@packages/cli/src/commands/config/reset.ts`:
- Around line 9-14: The ensureWritable check is applied even for dry-run; change
the control flow so that when flags['dry-run'] is true the code calls
printDryRun('config.reset', {}, format) and returns before calling
ensureWritable. Concretely, either move the ensureWritable(flags) call below the
dry-run branch or wrap it in if (!flags['dry-run']) so ensureWritable is only
enforced for real mutations; keep references to ensureWritable, flags,
printDryRun and ConfigReset to locate the change.

In `@packages/cli/src/commands/config/set.ts`:
- Around line 18-22: The code lacks the required blank separator before the
subsequent statement: after the conditional that checks flags['dry-run'] and
calls printDryRun (and returns), insert a blank line before the call to
updateConfig to satisfy `@stylistic/padding-line-between-statements`;
specifically, ensure there is an empty line between the return from the
printDryRun branch and the updateConfig(() => ...) invocation that updates
[args.key] to nextValue.
- Around line 17-20: The dry-run hides a "clear" intent because nextValue is
undefined when args.value === '' so printDryRun('config.set', { [args.key]:
nextValue }, ...) omits the key; change the value used for the dry-run payload
so an explicit marker is shown (e.g., use null) — compute a separate
payloadValue (payloadValue = args.value === '' ? null : args.value) and pass
that to printDryRun while leaving nextValue/actual set logic unchanged; update
references in the config.set flow (args.value, nextValue, printDryRun) so JSON
dry-run shows the clear intent.

In `@packages/npm/scripts/build.ts`:
- Line 33: The download() helper lacks request/socket timeouts and doesn't clean
up partial files on stall/errors; update download() to use an AbortController
(or set a socket timeout) to abort the request after a configurable timeout, and
on abort/error ensure the created write stream (createWriteStream(destination))
is closed and the partial destination file (and tempDir if applicable) is
removed (use rm with force) before rejecting; also attach cleanup logic to
response 'error', file 'error', and the outer request 'error' handlers so
artifact.file/destination is always removed on failure and redirects continue to
propagate the same cleanup behavior.

---

Outside diff comments:
In `@packages/cli/src/commands/auth/logout.ts`:
- Around line 21-32: The last statement calling printSuccess(...) needs a blank
separator line before it to satisfy stylistic rules; insert a single empty line
between the token-handling block (where you call clearTargetAuth(target) and
compute revoked) and the final await printSuccess(...) call so there is padding
between the if/await block and the printSuccess invocation; target symbols to
locate are the revoked/token variables, the fetch + clearTargetAuth(target)
sequence, and the await printSuccess(...) call.
- Around line 14-20: The dry-run path currently returns a normal payload even
when logout would fail due to BEEPER_ACCESS_TOKEN being set and no
target.auth?.accessToken; move or duplicate the env check so the same error is
thrown during dry-run: before calling printDryRun('auth.logout', ...) check if
process.env.BEEPER_ACCESS_TOKEN && !target.auth?.accessToken and throw the same
Error('auth logout cannot clear BEEPER_ACCESS_TOKEN from the environment; unset
it in the calling process.'), so printDryRun only runs when the real command
would proceed; reference flags['dry-run'], printDryRun,
process.env.BEEPER_ACCESS_TOKEN, and target.auth?.accessToken.

In `@packages/cli/src/commands/setup.ts`:
- Around line 53-76: The dry-run path currently calls ensureWritable(flags)
before deciding to return, causing read-only environments to fail; update setup
command logic so the dry-run shortcut is checked and returned before calling
ensureWritable(flags) (or make ensureWritable a no-op when flags['dry-run'] is
true): move the flags['dry-run'] check (which calls printDryRun) to run prior to
invoking ensureWritable(flags) (or add an early return when flags['dry-run'] is
set) so printDryRun can run in read-only configs; touch the Setup command entry
where ensureWritable and printDryRun are used to implement this change.

---

Duplicate comments:
In `@packages/cli/src/commands/api/get.ts`:
- Line 24: Add a blank line before the statement "const client = await
createClient(flags)" so there is a padding line separating it from the preceding
early-return block; locate the code in the get command where createClient(flags)
is awaited and ensure a single empty line exists above that const declaration to
satisfy `@stylistic/padding-line-between-statements`.
🪄 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: CHILL

Plan: Pro

Run ID: 1f36d472-5015-49fd-bfa0-e4c40634eba9

📥 Commits

Reviewing files that changed from the base of the PR and between f22ce03 and fefbf4e.

📒 Files selected for processing (53)
  • .gitignore
  • packages/cli/src/commands/api/get.ts
  • packages/cli/src/commands/api/post.ts
  • packages/cli/src/commands/api/request.ts
  • packages/cli/src/commands/auth/email/response.ts
  • packages/cli/src/commands/auth/logout.ts
  • packages/cli/src/commands/bridges/show.ts
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/chats/description.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/config/reset.ts
  • packages/cli/src/commands/config/set.ts
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/media/download.ts
  • packages/cli/src/commands/messages/search.ts
  • packages/cli/src/commands/plugins/available.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/commands/resolve/chat.ts
  • packages/cli/src/commands/schema.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/commands/targets/disable.ts
  • packages/cli/src/commands/targets/enable.ts
  • packages/cli/src/commands/targets/list.ts
  • packages/cli/src/commands/targets/logs.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/commands/targets/restart.ts
  • packages/cli/src/commands/targets/show.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/targets/status.ts
  • packages/cli/src/commands/targets/stop.ts
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/commands/verify/cancel.ts
  • packages/cli/src/lib/command-metadata.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/lib/export/index.ts
  • packages/cli/src/lib/ink/render.tsx
  • packages/cli/src/lib/ink/theme.ts
  • packages/cli/src/lib/installations.ts
  • packages/cli/src/lib/profiles.ts
  • packages/cli/src/lib/resolve.ts
  • packages/cli/src/lib/setup-login.ts
  • packages/npm/scripts/build.ts
💤 Files with no reviewable changes (14)
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/lib/export/index.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/chats/description.ts
✅ Files skipped from review due to trivial changes (7)
  • .gitignore
  • packages/cli/src/commands/plugins/available.ts
  • packages/cli/src/commands/targets/list.ts
  • packages/cli/src/commands/bridges/show.ts
  • packages/cli/src/commands/targets/show.ts
  • packages/cli/src/lib/ink/theme.ts
  • packages/cli/src/commands/targets/status.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/targets/logs.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/targets/stop.ts
  • packages/cli/src/commands/targets/restart.ts
  • packages/cli/src/commands/auth/email/response.ts
  • packages/cli/src/commands/media/download.ts
  • packages/cli/src/lib/setup-login.ts
  • packages/cli/src/commands/targets/disable.ts
  • packages/cli/src/commands/targets/enable.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/lib/ink/render.tsx
  • packages/cli/src/commands/messages/search.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/lib/command-metadata.ts
  • packages/cli/src/lib/resolve.ts
  • packages/cli/src/commands/resolve/chat.ts
  • packages/cli/src/lib/installations.ts
  • packages/cli/src/lib/profiles.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/commands/schema.ts
📜 Review details
🧰 Additional context used
🪛 ESLint
packages/cli/src/commands/verify/cancel.ts

[error] 17-17: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/api/get.ts

[error] 24-24: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/api/post.ts

[error] 28-28: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 32-32: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 36-36: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/api/request.ts

[error] 28-28: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 32-32: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/auth/logout.ts

[error] 32-32: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/config/reset.ts

[error] 16-16: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/config/set.ts

[error] 22-22: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/setup.ts

[error] 140-140: Use switch instead of multiple else-if.

(unicorn/prefer-switch)

🔇 Additional comments (2)
packages/cli/src/commands/verify/cancel.ts (1)

13-17: Missing blank line before const client still fails lint.

This is the same @stylistic/padding-line-between-statements violation already called out earlier, and it still appears at Line 17.

packages/cli/src/commands/doctor.ts (1)

13-17: LGTM!

Comment thread packages/cli/src/commands/api/post.ts Outdated
Comment thread packages/cli/src/commands/api/request.ts Outdated
Comment thread packages/cli/src/commands/config/reset.ts Outdated
Comment thread packages/cli/src/commands/config/set.ts Outdated
Comment thread packages/cli/src/commands/config/set.ts Outdated
Comment thread packages/npm/scripts/build.ts Outdated
@batuhan batuhan changed the title Improve setup fallback handling and launcher install output Add static docs site and agent-friendly CLI output Jun 2, 2026
Comment thread packages/cli/src/commands/bridges/delete.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: 5

🧹 Nitpick comments (4)
packages/cli/src/lib/bridges/websocket-proxy.ts (1)

64-64: ⚡ Quick win

Abort listener accumulates on the shared signal across reconnects.

runSingleProxy adds a new 'abort' listener on the same signal on every reconnect without ever removing it. On a long-lived proxy with many reconnects this retains references to closed ws instances and can trigger Node's MaxListenersExceededWarning. Remove the listener once the session ends.

♻️ Proposed fix
-  signal.addEventListener('abort', () => ws.close())
+  const onAbort = () => ws.close()
+  signal.addEventListener('abort', onAbort)
   await new Promise<void>((resolve, reject) => {
     ws.once('open', () => {
       send(ws, { command: 'bridge_status', data: { stateEvent: 'UNCONFIGURED' } })
       keepalive(signal, ws).catch(error => process.stderr.write(`Websocket ping returned error: ${(error as Error).message}\n`))
       resolve()
     })
     ws.once('error', reject)
   })
-  await new Promise<void>((resolve, reject) => {
+  try {
+    await new Promise<void>((resolve, reject) => {
       ws.on('message', data => {
         handleMessage(ws, registration, JSON.parse(String(data))).catch(error => {
           process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`)
         })
       })
       ws.once('close', (code, reason) => {
         if (code === 4001 || String(reason).includes('conn_replaced')) reject(new Error('conn_replaced'))
         else resolve()
       })
       ws.once('error', reject)
-  })
+    })
+  } finally {
+    signal.removeEventListener('abort', onAbort)
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/bridges/websocket-proxy.ts` at line 64, runSingleProxy
currently adds an anonymous 'abort' listener to the shared signal on each
reconnect which leaks closed ws instances; change it to add a named handler
(e.g., onAbort) when calling signal.addEventListener('abort', onAbort) and
ensure you call signal.removeEventListener('abort', onAbort) when the ws session
ends (for example inside ws.on('close')/ws.on('error') or immediately after
ws.close() completes) so the listener is removed and references to the closed ws
are released.
packages/cli/src/lib/bridges/manager.ts (1)

499-508: 💤 Low value

Static rename import instead of dynamic import.

await import('node:fs/promises').then(fs => fs.rename(...)) can be simplified by importing rename at the top alongside the other node:fs/promises members, avoiding a per-call module resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/bridges/manager.ts` around lines 499 - 508, The
downloadFile function currently uses a dynamic import for rename; change this to
a static import by adding rename to the existing node:fs/promises imports and
then replace the dynamic import call in downloadFile with a direct await
rename(tmp, path). Update the import list where mkdir, chmod, rm,
createWriteStream, etc. are imported to include rename so module resolution
happens once at load time rather than per call.
packages/cli/src/lib/bridges/catalog.ts (1)

65-67: ⚡ Quick win

Wrap plugin JSON parsing with file context.

JSON.parse on a user/env-provided catalog file will throw a native, contextless error (e.g. Unexpected token ...) and abort the CLI without indicating which file failed. Adding the path makes misconfigured custom catalogs debuggable.

♻️ Proposed change
-  const plugin = JSON.parse(await readFile(path, 'utf8')) as CatalogPlugin
+  let plugin: CatalogPlugin
+  try {
+    plugin = JSON.parse(await readFile(path, 'utf8')) as CatalogPlugin
+  } catch (err) {
+    throw new Error(`Failed to parse bridge catalog "${path}": ${(err as Error).message}`)
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/bridges/catalog.ts` around lines 65 - 67, In
loadCatalogPlugin, JSON.parse on the file contents can throw an opaque error;
wrap the parse in a try/catch around the readFile/JSON.parse (the code that
assigns plugin) and when catching, throw or rethrow an Error that includes the
catalog file path (path) and the original error message/stack so the CLI reports
which file failed (e.g., `Error parsing catalog JSON at ${path}:
${err.message}`) while preserving the original error for debugging.
packages/cli/src/commands/bridges/whoami.ts (1)

22-67: ⚡ Quick win

Resolve the error-level ESLint findings to avoid a lint/CI failure.

Static analysis reports error-level violations that will likely break the lint step: missing blank lines before statements (Lines 22, 26, 58), perfectionist/sort-array-includes (Line 65, expects androidsms before hungryserv), and unicorn/explicit-length-check (Line 67).

♻️ Suggested fixes
-  if (['hungryserv', 'androidsms', 'imessage'].includes(name)) return ''
+  if (['androidsms', 'hungryserv', 'imessage'].includes(name)) return ''
   const states = Object.values(bridge.remoteState ?? {})
-  if (!states.length) return bridge.bridgeState?.isSelfHosted ? '' : 'not logged in'
+  if (states.length === 0) return bridge.bridgeState?.isSelfHosted ? '' : 'not logged in'

These are auto-fixable via eslint --fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/bridges/whoami.ts` around lines 22 - 67, Add the
missing blank lines and fix the lint rule violations: in printWhoami and
formatBridge add blank lines before logical statement blocks to satisfy
formatting; in formatBridgeRemotes change the includes array to the correct
sorted order (put "androidsms" before "hungryserv") and replace the explicit
falsy length check (!states.length) with an explicit length comparison
(states.length === 0) so it satisfies unicorn/explicit-length-check. After
making these small edits to printWhoami, formatBridge, and formatBridgeRemotes,
run eslint --fix to auto-apply remaining formatting fixes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/bridges/delete.ts`:
- Around line 27-37: The current delete flow uses flags.force for skipping the
whoami/self-hosted checks but uses isForce(flags) to skip the confirmation,
causing inconsistent gating; update the logic so both checks use the same
predicate (preferably isForce(flags)) — i.e., replace occurrences of checks
against flags.force with isForce(flags) when deciding whether to call whoami()
and validate bridgeInfo (refer to whoami, bridgeInfo, args.bridge, flags.force
and isForce(flags)), and keep the confirmation gated by isForce(flags) and
confirm() as currently written to ensure consistent behavior.

In `@packages/cli/src/lib/bridges/manager.ts`:
- Line 5: The import list at the top of manager.ts includes readFile but it's
unused and causing a lint error; remove readFile from the named import (i.e.,
change "import { access, chmod, mkdir, readFile, rm, writeFile } ..." to exclude
readFile) and run the linter/CI to confirm the no-unused-vars violation is
resolved.

In `@packages/cli/src/lib/bridges/websocket-proxy.ts`:
- Around line 74-78: The 'message' listener currently calls JSON.parse(...) as
an argument to handleMessage, so a parse error will throw synchronously and
bypass the handleMessage(...).catch; fix by parsing inside the listener with a
try/catch: call JSON.parse(String(data)) inside a try block, on parse error
write to process.stderr and return early, otherwise call handleMessage(ws,
registration, parsed).catch(...) to handle async errors from handleMessage;
reference the ws.on('message', ...) listener and handleMessage function in your
change.
- Around line 53-55: runSingleProxy builds wsURL using an absolute path which
drops any base path like /_hungryserv/<username>, and also has two bugs around
message parsing and accumulating abort listeners; fix by constructing wsURL
using the homeserverURL’s pathname as the base (e.g., join
homeserverURL.pathname with the relative path
'_matrix/client/unstable/fi.mau.as_sync' so the per-user prefix is preserved)
instead of new URL('/_matrix/...', homeserverURL), change the ws.on('message',
...) handler so JSON.parse is wrapped in try/catch or moved inside the async
handleMessage call so parse errors are handled by the existing .catch, and avoid
adding a new abort listener on every reconnect by registering a single named
abort handler (or add the listener once before reconnect attempts and remove it
when appropriate) rather than calling signal.addEventListener('abort', () =>
ws.close()) repeatedly.

In `@packages/cli/src/types/ws.d.ts`:
- Line 2: Remove the unused import IncomingHttpHeaders from the top of the
ws.d.ts file; it’s imported but never referenced (headers are typed as
Record<string, string>), so delete the line "import type { IncomingHttpHeaders }
from 'node:http'" or replace any intended usage with the existing Record<string,
string> type and save to satisfy ESLint/CI.

---

Nitpick comments:
In `@packages/cli/src/commands/bridges/whoami.ts`:
- Around line 22-67: Add the missing blank lines and fix the lint rule
violations: in printWhoami and formatBridge add blank lines before logical
statement blocks to satisfy formatting; in formatBridgeRemotes change the
includes array to the correct sorted order (put "androidsms" before
"hungryserv") and replace the explicit falsy length check (!states.length) with
an explicit length comparison (states.length === 0) so it satisfies
unicorn/explicit-length-check. After making these small edits to printWhoami,
formatBridge, and formatBridgeRemotes, run eslint --fix to auto-apply remaining
formatting fixes.

In `@packages/cli/src/lib/bridges/catalog.ts`:
- Around line 65-67: In loadCatalogPlugin, JSON.parse on the file contents can
throw an opaque error; wrap the parse in a try/catch around the
readFile/JSON.parse (the code that assigns plugin) and when catching, throw or
rethrow an Error that includes the catalog file path (path) and the original
error message/stack so the CLI reports which file failed (e.g., `Error parsing
catalog JSON at ${path}: ${err.message}`) while preserving the original error
for debugging.

In `@packages/cli/src/lib/bridges/manager.ts`:
- Around line 499-508: The downloadFile function currently uses a dynamic import
for rename; change this to a static import by adding rename to the existing
node:fs/promises imports and then replace the dynamic import call in
downloadFile with a direct await rename(tmp, path). Update the import list where
mkdir, chmod, rm, createWriteStream, etc. are imported to include rename so
module resolution happens once at load time rather than per call.

In `@packages/cli/src/lib/bridges/websocket-proxy.ts`:
- Line 64: runSingleProxy currently adds an anonymous 'abort' listener to the
shared signal on each reconnect which leaks closed ws instances; change it to
add a named handler (e.g., onAbort) when calling
signal.addEventListener('abort', onAbort) and ensure you call
signal.removeEventListener('abort', onAbort) when the ws session ends (for
example inside ws.on('close')/ws.on('error') or immediately after ws.close()
completes) so the listener is removed and references to the closed ws are
released.
🪄 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: CHILL

Plan: Pro

Run ID: 038dcfc3-417b-424a-b907-9e8dd7cd0a75

📥 Commits

Reviewing files that changed from the base of the PR and between fefbf4e and cf9a5bd.

⛔ Files ignored due to path filters (1)
  • .DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (22)
  • packages/cli/package.json
  • packages/cli/scripts/generate-command-map.ts
  • packages/cli/scripts/sync-bridge-manager-config.ts
  • packages/cli/src/commands.generated.ts
  • packages/cli/src/commands/bridges/config.ts
  • packages/cli/src/commands/bridges/delete.ts
  • packages/cli/src/commands/bridges/list.ts
  • packages/cli/src/commands/bridges/login-password.ts
  • packages/cli/src/commands/bridges/login.ts
  • packages/cli/src/commands/bridges/logout.ts
  • packages/cli/src/commands/bridges/proxy.ts
  • packages/cli/src/commands/bridges/register.ts
  • packages/cli/src/commands/bridges/run.ts
  • packages/cli/src/commands/bridges/show.ts
  • packages/cli/src/commands/bridges/whoami.ts
  • packages/cli/src/lib/bridges/catalog.ts
  • packages/cli/src/lib/bridges/command.ts
  • packages/cli/src/lib/bridges/generated.ts
  • packages/cli/src/lib/bridges/go-template.ts
  • packages/cli/src/lib/bridges/manager.ts
  • packages/cli/src/lib/bridges/websocket-proxy.ts
  • packages/cli/src/types/ws.d.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/cli/package.json
  • packages/cli/src/commands.generated.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: README.md:178-178
Timestamp: 2026-05-27T22:40:21.941Z
Learning: In the beeper CLI, the correct command for recovery-key verification is `beeper verify recovery-key -t <target> --key "<key>"` (top-level `verify` subcommand, `--key` flag). The `auth.md` docs previously had incorrect syntax (`beeper auth verify recovery-key --code KEY`), which was fixed in commit be4ec8b of PR `#13`.
🪛 ast-grep (0.43.0)
packages/cli/scripts/sync-bridge-manager-config.ts

[warning] 60-60: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(var ${name} = map\\[string\\]bool\\{([\\s\\S]*?)\\n\\})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 67-67: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(var ${name} = map\\[string\\]string\\{([\\s\\S]*?)\\n\\})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 ESLint
packages/cli/src/types/ws.d.ts

[error] 2-2: 'IncomingHttpHeaders' is defined but never used.

(@typescript-eslint/no-unused-vars)

packages/cli/src/commands/bridges/whoami.ts

[error] 22-22: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 26-26: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 58-58: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 65-65: Expected "androidsms" to come before "hungryserv".

(perfectionist/sort-array-includes)


[error] 67-67: Use .length === 0 when checking length is zero.

(unicorn/explicit-length-check)

packages/cli/src/commands/bridges/register.ts

[error] 34-34: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/commands/bridges/delete.ts

[error] 38-38: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 60-60: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 63-63: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/lib/bridges/websocket-proxy.ts

[error] 15-15: Invalid group length in numeric value.

(unicorn/numeric-separators-style)


[error] 24-24: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 27-27: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 45-45: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 49-49: Do not use useless undefined.

(unicorn/no-useless-undefined)


[error] 104-104: 'HeadersInit' is not defined.

(no-undef)


[error] 111-111: Use for…of instead of .forEach(…).

(unicorn/no-array-for-each)


[error] 116-116: Unnecessary return statement.

(no-useless-return)


[error] 124-124: Do not use useless undefined.

(unicorn/no-useless-undefined)


[error] 139-139: 'BodyInit' is not defined.

(no-undef)

packages/cli/src/commands/bridges/run.ts

[error] 62-62: Unexpected negated condition.

(unicorn/no-negated-condition)


[error] 74-74: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 118-118: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 120-120: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 124-124: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 132-132: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 157-157: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 163-163: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 167-167: Do not use useless undefined.

(unicorn/no-useless-undefined)

packages/cli/src/lib/bridges/go-template.ts

[error] 45-45: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 45-45: Use .length > 0 when checking length is not zero.

(unicorn/explicit-length-check)


[error] 58-58: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 74-74: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 89-89: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 94-94: Use .length === 0 when checking length is zero.

(unicorn/explicit-length-check)


[error] 97-97: Missing braces in case clause.

(unicorn/switch-case-braces)


[error] 102-102: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 107-107: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 113-114: Expected "eq" to come before "setfield".

(perfectionist/sort-switch-case)


[error] 113-113: Missing braces in case clause.

(unicorn/switch-case-braces)


[error] 113-113: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 121-121: Missing braces in case clause.

(unicorn/switch-case-braces)


[error] 121-121: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 144-144: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 151-151: Use a for-of loop instead of this for loop.

(unicorn/no-for-loop)


[error] 168-168: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 172-172: function 'truthy' is equivalent to Boolean. Use Boolean directly.

(unicorn/prefer-native-coercion-functions)

packages/cli/src/commands/bridges/show.ts

[error] 26-26: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

packages/cli/src/lib/bridges/manager.ts

[error] 5-5: 'readFile' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 111-111: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 172-172: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 189-189: Use object destructuring.

(prefer-destructuring)


[error] 210-210: Expected "imessage" to come before "imessagecloud".

(perfectionist/sort-array-includes)


[error] 247-247: Use .length > 0 when checking length is not zero.

(unicorn/explicit-length-check)


[error] 305-305: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 361-361: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 388-388: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 393-393: Unexpected negated condition.

(unicorn/no-negated-condition)


[error] 409-409: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 412-412: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 426-426: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 453-453: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 457-457: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 463-463: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 494-494: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 511-511: Expected "discord" to come before "whatsapp".

(perfectionist/sort-array-includes)


[error] 511-511: Expected "gmessages" to come before "slack".

(perfectionist/sort-array-includes)


[error] 511-511: Expected "imessagego" to come before "signal".

(perfectionist/sort-array-includes)


[error] 511-511: Expected "bluesky" to come before "twitter".

(perfectionist/sort-array-includes)


[error] 555-555: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 557-557: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 574-582: Unexpected labeled statement.

(no-labels)


[error] 579-579: Unexpected label in break statement.

(no-labels)


[error] 584-584: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 591-591: Use switch instead of multiple else-if.

(unicorn/prefer-switch)


[error] 604-604: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 604-604: Expected "facebook" to come before "instagram".

(perfectionist/sort-array-includes)


[error] 607-607: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 620-620: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 623-623: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 636-636: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 644-644: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 657-657: Use for…of instead of .forEach(…).

(unicorn/no-array-for-each)


[error] 690-690: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 694-695: This if statement can be replaced by a ternary expression.

(unicorn/prefer-ternary)


[error] 697-697: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)


[error] 729-729: Arrow function should not return assignment.

(no-return-assign)


[error] 730-730: Arrow function should not return assignment.

(no-return-assign)


[error] 753-753: Unexpected use of '&'.

(no-bitwise)


[error] 753-753: Unexpected use of '^'.

(no-bitwise)


[error] 753-753: Invalid group length in numeric value.

(unicorn/numeric-separators-style)


[error] 753-753: Unexpected use of '>>>'.

(no-bitwise)


[error] 753-753: Unexpected use of '>>>'.

(no-bitwise)


[error] 754-754: Unexpected use of '>>>'.

(no-bitwise)


[error] 758-758: Invalid group length in numeric value.

(unicorn/numeric-separators-style)


[error] 759-759: Unexpected use of '^'.

(no-bitwise)


[error] 759-759: Unexpected use of '&'.

(no-bitwise)


[error] 759-759: Unexpected use of '^'.

(no-bitwise)


[error] 759-759: Unexpected use of '>>>'.

(no-bitwise)


[error] 760-760: Unexpected use of '>>>'.

(no-bitwise)


[error] 760-760: Unexpected use of '^'.

(no-bitwise)


[error] 760-760: Invalid group length in numeric value.

(unicorn/numeric-separators-style)

packages/cli/src/commands/bridges/config.ts

[error] 37-37: Expected "discord" to come before "whatsapp".

(perfectionist/sort-array-includes)


[error] 37-37: Expected "gmessages" to come before "slack".

(perfectionist/sort-array-includes)


[error] 37-37: Expected "meta" to come before "signal".

(perfectionist/sort-array-includes)


[error] 37-37: Expected "bluesky" to come before "twitter".

(perfectionist/sort-array-includes)


[error] 48-48: Expected blank line before this statement.

(@stylistic/padding-line-between-statements)

🔇 Additional comments (15)
packages/cli/src/commands/bridges/proxy.ts (1)

6-23: LGTM!

Note: the /_hungryserv/${username} segment you build into homeserverURL is silently dropped downstream — see the comment in websocket-proxy.ts (Line 54).

packages/cli/scripts/generate-command-map.ts (1)

12-23: LGTM!

packages/cli/scripts/sync-bridge-manager-config.ts (1)

52-78: LGTM!

The ast-grep ReDoS warnings on the dynamic new RegExp(...) are false positives here — name is always a hardcoded literal ('websocketBridges', 'bridgeIPSuffix'), never external input.

packages/cli/src/commands/bridges/run.ts (1)

40-90: LGTM!

Also applies to: 135-170

packages/cli/src/commands/bridges/login.ts (1)

9-36: LGTM!

packages/cli/src/commands/bridges/login-password.ts (1)

9-37: LGTM!

packages/cli/src/commands/bridges/list.ts (1)

6-33: LGTM!

packages/cli/src/lib/bridges/go-template.ts (2)

77-145: LGTM!


59-61: ⚡ Quick win

Fix {{- left-trim to remove preceding newlines/whitespace
In packages/cli/src/lib/bridges/go-template.ts (lines 59-61), left-trim currently does text.replace(/[ \t]*$/, ''), which trims spaces/tabs but not the preceding newline—making {{- ... }} whitespace handling asymmetric with the right-trim that already consumes an optional newline.

🐛 Proposed fix to match Go trim semantics
-    if (trimLeft) text = text.replace(/[ \t]*$/, '')
+    if (trimLeft) text = text.replace(/\s*$/, '')
packages/cli/src/lib/bridges/manager.ts (1)

124-153: LGTM!

packages/cli/src/commands/bridges/config.ts (1)

18-50: LGTM!

packages/cli/src/commands/bridges/register.ts (1)

22-29: ⚡ Quick win

force is inherited from BeeperCommand.baseFlags.

BridgeCommand spreads ...BeeperCommand.baseFlags (packages/cli/src/lib/bridges/command.ts), and BeeperCommand.baseFlags defines force in packages/cli/src/lib/command.ts, so flags.force used in register.ts is expected to be present.

packages/cli/src/commands/bridges/logout.ts (1)

13-19: LGTM!

packages/cli/src/lib/bridges/command.ts (1)

4-9: LGTM!

packages/cli/src/commands/bridges/show.ts (1)

18-36: LGTM!

Comment on lines +27 to +37
if (!flags.force) {
const info = await whoami(env)
const bridgeInfo = info.user.bridges?.[args.bridge]
if (!bridgeInfo) throw new Error(`You don't have a ${args.bridge} bridge.`)
if (!bridgeInfo.bridgeState?.isSelfHosted) throw new Error(`Your ${args.bridge} bridge is not self-hosted.`)
}

if (!isForce(flags)) {
const confirmed = await confirm(`Are you sure you want to permanently delete ${args.bridge}?`)
if (!confirmed) throw new Error('bridge delete cancelled')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep --pattern 'export function isForce($_) { $$$ }'
rg -nP --type=ts -C3 '\bisForce\b' -g '!**/*.test.ts'

Repository: beeper/cli

Length of output: 3052


Fix inconsistent force/yes gating in bridge delete

bridge delete skips the confirmation when isForce(flags) is true (it includes flags.force, flags.yes, and process.env.BEEPER_FORCE === '1'), but it still runs the whoami + self-hosted validation whenever flags.force is not set. Align both gates on the same predicate.

🐛 Proposed fix
-    if (!flags.force) {
+    if (!isForce(flags)) {
       const info = await whoami(env)
       const bridgeInfo = info.user.bridges?.[args.bridge]
       if (!bridgeInfo) throw new Error(`You don't have a ${args.bridge} bridge.`)
       if (!bridgeInfo.bridgeState?.isSelfHosted) throw new Error(`Your ${args.bridge} bridge is not self-hosted.`)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!flags.force) {
const info = await whoami(env)
const bridgeInfo = info.user.bridges?.[args.bridge]
if (!bridgeInfo) throw new Error(`You don't have a ${args.bridge} bridge.`)
if (!bridgeInfo.bridgeState?.isSelfHosted) throw new Error(`Your ${args.bridge} bridge is not self-hosted.`)
}
if (!isForce(flags)) {
const confirmed = await confirm(`Are you sure you want to permanently delete ${args.bridge}?`)
if (!confirmed) throw new Error('bridge delete cancelled')
}
if (!isForce(flags)) {
const info = await whoami(env)
const bridgeInfo = info.user.bridges?.[args.bridge]
if (!bridgeInfo) throw new Error(`You don't have a ${args.bridge} bridge.`)
if (!bridgeInfo.bridgeState?.isSelfHosted) throw new Error(`Your ${args.bridge} bridge is not self-hosted.`)
}
if (!isForce(flags)) {
const confirmed = await confirm(`Are you sure you want to permanently delete ${args.bridge}?`)
if (!confirmed) throw new Error('bridge delete cancelled')
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/bridges/delete.ts` around lines 27 - 37, The
current delete flow uses flags.force for skipping the whoami/self-hosted checks
but uses isForce(flags) to skip the confirmation, causing inconsistent gating;
update the logic so both checks use the same predicate (preferably
isForce(flags)) — i.e., replace occurrences of checks against flags.force with
isForce(flags) when deciding whether to call whoami() and validate bridgeInfo
(refer to whoami, bridgeInfo, args.bridge, flags.force and isForce(flags)), and
keep the confirmation gated by isForce(flags) and confirm() as currently written
to ensure consistent behavior.

Comment thread packages/cli/src/lib/bridges/manager.ts Outdated
import { createInterface } from 'node:readline/promises'
import { stdin as input, stdout as output } from 'node:process'
import { constants as fsConstants } from 'node:fs'
import { access, chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused readFile import.

readFile is imported but not used anywhere in this module, which trips the no-unused-vars lint and can fail CI.

🧹 Proposed fix
-import { access, chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
+import { access, chmod, mkdir, rm, writeFile } from 'node:fs/promises'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { access, chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { access, chmod, mkdir, rm, writeFile } from 'node:fs/promises'
🧰 Tools
🪛 ESLint

[error] 5-5: 'readFile' is defined but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/bridges/manager.ts` at line 5, The import list at the
top of manager.ts includes readFile but it's unused and causing a lint error;
remove readFile from the named import (i.e., change "import { access, chmod,
mkdir, readFile, rm, writeFile } ..." to exclude readFile) and run the linter/CI
to confirm the no-unused-vars violation is resolved.

Comment on lines +53 to +55
async function runSingleProxy(signal: AbortSignal, homeserverURL: string, registration: AppserviceRegistration): Promise<void> {
const wsURL = new URL('/_matrix/client/unstable/fi.mau.as_sync', homeserverURL)
wsURL.protocol = wsURL.protocol === 'http:' ? 'ws:' : 'wss:'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# How is homeserver_url constructed/used, and is the _hungryserv prefix expected for as_sync?
rg -nP --type=ts -C3 'homeserver_url|_hungryserv|fi\.mau\.as_sync'

Repository: beeper/cli

Length of output: 50366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/cli/src/lib/bridges/websocket-proxy.ts"

# Show the relevant sections around the reported lines
nl -ba "$FILE" | sed -n '1,140p'

Repository: beeper/cli

Length of output: 97


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/cli/src/lib/bridges/websocket-proxy.ts"

# Focus searches for abort listener + message handler/JSON.parse
rg -n --type=ts "addEventListener\(|removeEventListener\(|abort|JSON\.parse|handleMessage|message\s*[\)\]]" "$FILE" || true

Repository: beeper/cli

Length of output: 822


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/cli/src/lib/bridges/websocket-proxy.ts"

# Find reconnection loop / retry logic
rg -n --type=ts -C2 "reconnect|retry|backoff|setTimeout|onopen|onclose|onerror|close\W*=>" "$FILE" || true

Repository: beeper/cli

Length of output: 1220


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/cli/src/lib/bridges/websocket-proxy.ts"

# Show the key sections with line numbers
cat -n "$FILE" | sed -n '1,140p'
cat -n "$FILE" | sed -n '140,260p'

Repository: beeper/cli

Length of output: 7622


🏁 Script executed:

rg -n --type=ts --type=tsx --type=js --type=json --type=md --type=yaml --type=yml "fi\.mau\.as_sync|websocket_proxy|_hungryserv" .

Repository: beeper/cli

Length of output: 82


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Re-run without unsupported file types
rg -n "fi\.mau\.as_sync|websocket_proxy|_hungryserv" .

Repository: beeper/cli

Length of output: 17539


Fix websocket URL construction to preserve /_hungryserv/<username>

runSingleProxy() builds the websocket URL with new URL('/_matrix/client/unstable/fi.mau.as_sync', homeserverURL) (websocket-proxy.ts:54). But homeserverURL is constructed as https://matrix.${env.domain}/_hungryserv/${encodeURIComponent(username)} (proxy.ts:19; manager.ts:683). Using an absolute path drops the /_hungryserv/<username> prefix, so the websocket targets https://matrix.${env.domain}/_matrix/... instead of the per-user hungryserv endpoint.

🔧 Proposed fix (preserve base path)
-  const wsURL = new URL('/_matrix/client/unstable/fi.mau.as_sync', homeserverURL)
+  const wsURL = new URL(`${homeserverURL.replace(/\/$/, '')}/_matrix/client/unstable/fi.mau.as_sync`)
   wsURL.protocol = wsURL.protocol === 'http:' ? 'ws:' : 'wss:'
  • ws.on('message', ...) parses JSON inline: JSON.parse(...) happens before handleMessage(...).catch(...), so non-JSON frames can throw synchronously and bypass the catch (websocket-proxy.ts:75).
  • Each reconnect adds signal.addEventListener('abort', () => ws.close()) without removing it, accumulating listeners across reconnects (websocket-proxy.ts:64).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function runSingleProxy(signal: AbortSignal, homeserverURL: string, registration: AppserviceRegistration): Promise<void> {
const wsURL = new URL('/_matrix/client/unstable/fi.mau.as_sync', homeserverURL)
wsURL.protocol = wsURL.protocol === 'http:' ? 'ws:' : 'wss:'
async function runSingleProxy(signal: AbortSignal, homeserverURL: string, registration: AppserviceRegistration): Promise<void> {
const wsURL = new URL(`${homeserverURL.replace(/\/$/, '')}/_matrix/client/unstable/fi.mau.as_sync`)
wsURL.protocol = wsURL.protocol === 'http:' ? 'ws:' : 'wss:'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/bridges/websocket-proxy.ts` around lines 53 - 55,
runSingleProxy builds wsURL using an absolute path which drops any base path
like /_hungryserv/<username>, and also has two bugs around message parsing and
accumulating abort listeners; fix by constructing wsURL using the
homeserverURL’s pathname as the base (e.g., join homeserverURL.pathname with the
relative path '_matrix/client/unstable/fi.mau.as_sync' so the per-user prefix is
preserved) instead of new URL('/_matrix/...', homeserverURL), change the
ws.on('message', ...) handler so JSON.parse is wrapped in try/catch or moved
inside the async handleMessage call so parse errors are handled by the existing
.catch, and avoid adding a new abort listener on every reconnect by registering
a single named abort handler (or add the listener once before reconnect attempts
and remove it when appropriate) rather than calling
signal.addEventListener('abort', () => ws.close()) repeatedly.

Comment on lines +74 to +78
ws.on('message', data => {
handleMessage(ws, registration, JSON.parse(String(data))).catch(error => {
process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`)
})
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

JSON.parse runs before handleMessage, so a parse error escapes the .catch.

JSON.parse(String(data)) is evaluated as the argument expression before handleMessage is invoked, so it throws synchronously inside the 'message' listener and is not caught by the attached .catch. A single non-JSON or truncated frame would surface as an uncaught exception.

🛡️ Proposed fix
     ws.on('message', data => {
-      handleMessage(ws, registration, JSON.parse(String(data))).catch(error => {
-        process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`)
-      })
+      Promise.resolve()
+        .then(() => handleMessage(ws, registration, JSON.parse(String(data))))
+        .catch(error => {
+          process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`)
+        })
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ws.on('message', data => {
handleMessage(ws, registration, JSON.parse(String(data))).catch(error => {
process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`)
})
})
ws.on('message', data => {
Promise.resolve()
.then(() => handleMessage(ws, registration, JSON.parse(String(data))))
.catch(error => {
process.stderr.write(`Failed to handle websocket message: ${(error as Error).message}\n`)
})
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/bridges/websocket-proxy.ts` around lines 74 - 78, The
'message' listener currently calls JSON.parse(...) as an argument to
handleMessage, so a parse error will throw synchronously and bypass the
handleMessage(...).catch; fix by parsing inside the listener with a try/catch:
call JSON.parse(String(data)) inside a try block, on parse error write to
process.stderr and return early, otherwise call handleMessage(ws, registration,
parsed).catch(...) to handle async errors from handleMessage; reference the
ws.on('message', ...) listener and handleMessage function in your change.

Comment thread packages/cli/src/types/ws.d.ts Outdated
@@ -0,0 +1,23 @@
declare module 'ws' {
import type { IncomingHttpHeaders } from 'node:http'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the unused IncomingHttpHeaders import.

It's imported but never referenced (headers are typed as Record<string, string>), and ESLint flags this as an error that will fail lint/CI.

🧹 Proposed fix
 declare module 'ws' {
-  import type { IncomingHttpHeaders } from 'node:http'
-
   class WebSocket {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { IncomingHttpHeaders } from 'node:http'
declare module 'ws' {
class WebSocket {
🧰 Tools
🪛 ESLint

[error] 2-2: 'IncomingHttpHeaders' is defined but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/types/ws.d.ts` at line 2, Remove the unused import
IncomingHttpHeaders from the top of the ws.d.ts file; it’s imported but never
referenced (headers are typed as Record<string, string>), so delete the line
"import type { IncomingHttpHeaders } from 'node:http'" or replace any intended
usage with the existing Record<string, string> type and save to satisfy
ESLint/CI.

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 (2)
packages/cli/src/lib/installations.ts (1)

205-209: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Sanitize artifact filenames before building file paths.

Line 205 trusts server-provided filename data and uses it directly in join(...), which permits path traversal/absolute-path writes (for both finalPath and tmpPath).

🔒 Proposed fix
-  const filename = filenameFromResponse(response) ?? (basename(new URL(response.url).pathname) || `beeper-download-${Date.now()}`)
-  const finalPath = join(destinationDir, filename)
-  const tmpPath = join(tmpdir(), `${filename}.${process.pid}.${Date.now()}.tmp`)
+  const rawFilename = filenameFromResponse(response) ?? (basename(new URL(response.url).pathname) || `beeper-download-${Date.now()}`)
+  const filename = basename(rawFilename).replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_')
+  if (!filename || filename === '.' || filename === '..') throw new Error('Invalid download filename.')
+  const finalPath = join(destinationDir, filename)
+  const tmpPath = join(tmpdir(), `${filename}.${process.pid}.${Date.now()}.tmp`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/installations.ts` around lines 205 - 209, The code
trusts the server-provided filename and embeds it into paths (filename,
finalPath, tmpPath) which enables path traversal/absolute-write risks; fix by
sanitizing the value returned from filenameFromResponse before using join — call
a sanitizer that strips any directory components and path separators (e.g.,
path.basename or equivalent), forbids absolute paths, removes nulls/control
chars, replaces suspicious sequences like ".." and "/" or "\" with safe
characters, and enforces a reasonable max length and a safe fallback (e.g.,
beeper-download-<ts>) so that both finalPath and tmpPath are constructed from
the sanitized filename prior to calling writeResponseToFile/rename.
packages/cli/src/lib/profiles.ts (1)

49-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use platform-specific desktop launch command.

launchDesktopApp now discovers Windows/Linux app paths but still always executes open (macOS-only). This breaks desktop profile startup on non-macOS hosts.

🛠️ Proposed fix
 export async function launchDesktopApp(target?: Target): Promise<{ id: string; startedAt: string }> {
   const appPath = await findDesktopAppPath()
-  const args = appPath ? ['-n', appPath, '--args'] : ['-n', '-a', 'Beeper', '--args']
-  args.push('--no-enforce-app-location')
-  if (target?.port) args.push(`--pas-port=${target.port}`)
-  if (target?.serverEnv) args.push(`--server-env=${target.serverEnv}`)
+  const appArgs = ['--no-enforce-app-location']
+  if (target?.port) appArgs.push(`--pas-port=${target.port}`)
+  if (target?.serverEnv) appArgs.push(`--server-env=${target.serverEnv}`)
   const env = target?.dataDir
     ? {
         ...process.env,
         ALLOW_MULTIPLE_INSTANCES: 'true',
         BEEPER_PROFILE: target.profile ?? target.id,
         BEEPER_USER_DATA_DIR: target.dataDir,
       }
     : process.env
-  spawn('open', args, { detached: true, stdio: 'ignore', env }).unref()
+  if (process.platform === 'darwin') {
+    const openArgs = appPath ? ['-n', appPath, '--args', ...appArgs] : ['-n', '-a', 'Beeper', '--args', ...appArgs]
+    spawn('open', openArgs, { detached: true, stdio: 'ignore', env }).unref()
+  } else {
+    if (!appPath) throw new Error('Beeper Desktop is not installed.')
+    spawn(appPath, appArgs, { detached: true, stdio: 'ignore', env }).unref()
+  }
   return { id: target?.id ?? 'desktop', startedAt: new Date().toISOString() }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/profiles.ts` around lines 49 - 64, launchDesktopApp
currently always spawns the macOS "open" command which fails on Windows/Linux;
update launchDesktopApp to pick the platform-specific launcher based on
process.platform (e.g., 'open' for darwin, 'cmd' with [' /c', 'start', ...] or
using the app executable on win32, and 'xdg-open' or the discovered Linux app
path on linux) and use the same args/env/detached/stdio handling as before;
ensure you still call findDesktopAppPath to prefer a discovered path, build the
appropriate argument array for each platform, and call spawn with the chosen
command and args so BEEPER_PROFILE/BEEPER_USER_DATA_DIR and port/serverEnv flags
are preserved when launching on non-macOS systems.
🧹 Nitpick comments (3)
packages/cli/src/cli/policy.ts (1)

15-23: ⚡ Quick win

Consider wrapping YAML parsing in try/catch for clearer error messages.

If the YAML file is malformed, the yaml package throws an error that may not clearly indicate it's a safety profile parsing issue. Wrapping with a descriptive error would improve debugging.

Suggested improvement
 function loadSafetyProfile(nameOrPath: string): { allow: Set<string>; name: string } {
   const path = resolveProfilePath(nameOrPath)
   if (!path) throw usage(`unknown safety profile "${nameOrPath}"`)
-  const root = parseYAML(readFileSync(path, 'utf8')) as Record<string, unknown> | undefined
+  let root: Record<string, unknown> | undefined
+  try {
+    root = parseYAML(readFileSync(path, 'utf8')) as Record<string, unknown> | undefined
+  } catch (error) {
+    const detail = error instanceof Error ? error.message : String(error)
+    throw usage(`failed to load safety profile "${nameOrPath}": ${detail}`)
+  }
   return {
     allow: rules(root?.allow),
     name: typeof root?.name === 'string' && root.name.trim() ? root.name.trim() : 'unnamed',
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/policy.ts` around lines 15 - 23, The YAML parsing in
loadSafetyProfile should be wrapped in a try/catch so malformed YAML yields a
clear, contextual error; surround the call to parseYAML(readFileSync(path,
'utf8')) with try/catch, on error throw a usage(...) (same style as the existing
unknown profile error) that includes the profile path/name and the original
error.message (or error.toString()) to aid debugging, then continue to call
rules(root?.allow) and compute the name as before.
packages/cli/src/cli/mcp.ts (1)

12-30: 💤 Low value

Consider using splice instead of setting length = 0 for clarity.

buffer.length = 0 works but buffer.splice(0) or reassigning buffer = [] (if using let) is more idiomatic. However, this is a minor style preference.

Alternative approach
-    buffer.length = 0
+    buffer.splice(0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/mcp.ts` around lines 12 - 30, In serveMcp, replace the
non-idiomatic buffer clearing line "buffer.length = 0" with a clearer operation;
because buffer is declared as a const array, use buffer.splice(0) to empty it
(or change the declaration to let buffer = [] and reassign buffer = [] if you
prefer reassignment). Update the buffer clearing inside serveMcp accordingly so
the code uses buffer.splice(0) (or switch to let and reassign) instead of
setting buffer.length = 0.
packages/cli/src/cli/commands.ts (1)

1671-1678: 💤 Low value

Consider adding return type annotation for apiClient.

Using any as return type loses type safety. If BeeperDesktop has a proper TypeScript interface, consider using that type or at least documenting what methods are expected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/commands.ts` around lines 1671 - 1678, The apiClient
function is typed as Promise<any>, losing type safety; change its return
annotation from Promise<any> to a concrete type that matches BeeperDesktop
(e.g., Promise<BeeperDesktop> or a defined interface describing the methods
used), import or export the BeeperDesktop type if available, or define a minimal
interface (with the specific methods your callers use) and use
Promise<ThatInterface> as the return type for apiClient; update the function
signature and any necessary imports so callers get proper typings while leaving
implementation (resolveTarget, targetToken, new BeeperDesktop(...)) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/cli/src/lib/installations.ts`:
- Around line 205-209: The code trusts the server-provided filename and embeds
it into paths (filename, finalPath, tmpPath) which enables path
traversal/absolute-write risks; fix by sanitizing the value returned from
filenameFromResponse before using join — call a sanitizer that strips any
directory components and path separators (e.g., path.basename or equivalent),
forbids absolute paths, removes nulls/control chars, replaces suspicious
sequences like ".." and "/" or "\" with safe characters, and enforces a
reasonable max length and a safe fallback (e.g., beeper-download-<ts>) so that
both finalPath and tmpPath are constructed from the sanitized filename prior to
calling writeResponseToFile/rename.

In `@packages/cli/src/lib/profiles.ts`:
- Around line 49-64: launchDesktopApp currently always spawns the macOS "open"
command which fails on Windows/Linux; update launchDesktopApp to pick the
platform-specific launcher based on process.platform (e.g., 'open' for darwin,
'cmd' with [' /c', 'start', ...] or using the app executable on win32, and
'xdg-open' or the discovered Linux app path on linux) and use the same
args/env/detached/stdio handling as before; ensure you still call
findDesktopAppPath to prefer a discovered path, build the appropriate argument
array for each platform, and call spawn with the chosen command and args so
BEEPER_PROFILE/BEEPER_USER_DATA_DIR and port/serverEnv flags are preserved when
launching on non-macOS systems.

---

Nitpick comments:
In `@packages/cli/src/cli/commands.ts`:
- Around line 1671-1678: The apiClient function is typed as Promise<any>, losing
type safety; change its return annotation from Promise<any> to a concrete type
that matches BeeperDesktop (e.g., Promise<BeeperDesktop> or a defined interface
describing the methods used), import or export the BeeperDesktop type if
available, or define a minimal interface (with the specific methods your callers
use) and use Promise<ThatInterface> as the return type for apiClient; update the
function signature and any necessary imports so callers get proper typings while
leaving implementation (resolveTarget, targetToken, new BeeperDesktop(...))
unchanged.

In `@packages/cli/src/cli/mcp.ts`:
- Around line 12-30: In serveMcp, replace the non-idiomatic buffer clearing line
"buffer.length = 0" with a clearer operation; because buffer is declared as a
const array, use buffer.splice(0) to empty it (or change the declaration to let
buffer = [] and reassign buffer = [] if you prefer reassignment). Update the
buffer clearing inside serveMcp accordingly so the code uses buffer.splice(0)
(or switch to let and reassign) instead of setting buffer.length = 0.

In `@packages/cli/src/cli/policy.ts`:
- Around line 15-23: The YAML parsing in loadSafetyProfile should be wrapped in
a try/catch so malformed YAML yields a clear, contextual error; surround the
call to parseYAML(readFileSync(path, 'utf8')) with try/catch, on error throw a
usage(...) (same style as the existing unknown profile error) that includes the
profile path/name and the original error.message (or error.toString()) to aid
debugging, then continue to call rules(root?.allow) and compute the name as
before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1444ea2-a6fb-49ae-9345-72a07fd8b87a

📥 Commits

Reviewing files that changed from the base of the PR and between cf9a5bd and 044405c.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (221)
  • .changeset/README.md
  • .changeset/config.json
  • .github/workflows/ci.yml
  • .github/workflows/publish-release.yml
  • .github/workflows/release-doctor.yml
  • README.md
  • eslint.config.mjs
  • package.json
  • packages/cli-plugin-cloudflare/LICENSE
  • packages/cli-plugin-cloudflare/README.md
  • packages/cli-plugin-cloudflare/package.json
  • packages/cli-plugin-cloudflare/src/commands/targets/tunnel.ts
  • packages/cli-plugin-cloudflare/src/index.ts
  • packages/cli-plugin-cloudflare/test/smoke.mjs
  • packages/cli-plugin-cloudflare/tsconfig.json
  • packages/cli/CHANGELOG.md
  • packages/cli/README.md
  • packages/cli/SECURITY.md
  • packages/cli/beeper-setup-redesign-spec.md
  • packages/cli/bin/binary-bootstrap.js
  • packages/cli/bin/check-release-environment
  • packages/cli/bin/cli.js
  • packages/cli/bin/dev.js
  • packages/cli/bin/logo.js
  • packages/cli/bin/run.js
  • packages/cli/links.txt
  • packages/cli/package.json
  • packages/cli/release-please-config.json
  • packages/cli/safety-profiles/agent-safe.yaml
  • packages/cli/safety-profiles/full.yaml
  • packages/cli/safety-profiles/readonly.yaml
  • packages/cli/scripts/bootstrap
  • packages/cli/scripts/build
  • packages/cli/scripts/build-binaries.ts
  • packages/cli/scripts/build-homebrew-archive.ts
  • packages/cli/scripts/check-api-copy.ts
  • packages/cli/scripts/check-manifest.ts
  • packages/cli/scripts/format
  • packages/cli/scripts/generate-command-map.ts
  • packages/cli/scripts/generate-readme.ts
  • packages/cli/scripts/link
  • packages/cli/scripts/lint
  • packages/cli/scripts/mock
  • packages/cli/scripts/publish-homebrew-formula.ts
  • packages/cli/scripts/publish-local-release.ts
  • packages/cli/scripts/read-signing-secrets.rb
  • packages/cli/scripts/run
  • packages/cli/scripts/sign-macos-binaries.ts
  • packages/cli/scripts/test
  • packages/cli/scripts/unlink
  • packages/cli/src/cli/commands.ts
  • packages/cli/src/cli/main.ts
  • packages/cli/src/cli/mcp.ts
  • packages/cli/src/cli/output.ts
  • packages/cli/src/cli/parse.ts
  • packages/cli/src/cli/policy.ts
  • packages/cli/src/cli/schema.ts
  • packages/cli/src/cli/setup.ts
  • packages/cli/src/cli/types.ts
  • packages/cli/src/commands.generated.ts
  • packages/cli/src/commands/_complete.ts
  • packages/cli/src/commands/accounts/add.ts
  • packages/cli/src/commands/accounts/list.ts
  • packages/cli/src/commands/accounts/remove.ts
  • packages/cli/src/commands/accounts/show.ts
  • packages/cli/src/commands/accounts/use.ts
  • packages/cli/src/commands/api/get.ts
  • packages/cli/src/commands/api/post.ts
  • packages/cli/src/commands/api/request.ts
  • packages/cli/src/commands/auth/email/response.ts
  • packages/cli/src/commands/auth/email/start.ts
  • packages/cli/src/commands/auth/logout.ts
  • packages/cli/src/commands/auth/status.ts
  • packages/cli/src/commands/autocomplete.ts
  • packages/cli/src/commands/bridges/list.ts
  • packages/cli/src/commands/bridges/show.ts
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/chats/description.ts
  • packages/cli/src/commands/chats/disappear.ts
  • packages/cli/src/commands/chats/draft.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/chats/list.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/chats/mute.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/chats/priority.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/chats/rename.ts
  • packages/cli/src/commands/chats/search.ts
  • packages/cli/src/commands/chats/show.ts
  • packages/cli/src/commands/chats/start.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/completion.ts
  • packages/cli/src/commands/config/get.ts
  • packages/cli/src/commands/config/path.ts
  • packages/cli/src/commands/config/reset.ts
  • packages/cli/src/commands/config/set.ts
  • packages/cli/src/commands/contacts/list.ts
  • packages/cli/src/commands/contacts/search.ts
  • packages/cli/src/commands/contacts/show.ts
  • packages/cli/src/commands/docs.ts
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/export.ts
  • packages/cli/src/commands/install/desktop.ts
  • packages/cli/src/commands/install/server.ts
  • packages/cli/src/commands/man.ts
  • packages/cli/src/commands/media/download.ts
  • packages/cli/src/commands/messages/context.ts
  • packages/cli/src/commands/messages/delete.ts
  • packages/cli/src/commands/messages/edit.ts
  • packages/cli/src/commands/messages/export.ts
  • packages/cli/src/commands/messages/list.ts
  • packages/cli/src/commands/messages/search.ts
  • packages/cli/src/commands/messages/show.ts
  • packages/cli/src/commands/plugins.ts
  • packages/cli/src/commands/plugins/available.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/commands/rpc.ts
  • packages/cli/src/commands/send/file.ts
  • packages/cli/src/commands/send/react.ts
  • packages/cli/src/commands/send/sticker.ts
  • packages/cli/src/commands/send/text.ts
  • packages/cli/src/commands/send/unreact.ts
  • packages/cli/src/commands/send/voice.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/commands/status.ts
  • packages/cli/src/commands/targets/add/desktop.ts
  • packages/cli/src/commands/targets/add/remote.ts
  • packages/cli/src/commands/targets/add/server.ts
  • packages/cli/src/commands/targets/disable.ts
  • packages/cli/src/commands/targets/enable.ts
  • packages/cli/src/commands/targets/list.ts
  • packages/cli/src/commands/targets/logs.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/commands/targets/restart.ts
  • packages/cli/src/commands/targets/show.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/targets/status.ts
  • packages/cli/src/commands/targets/stop.ts
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/update.ts
  • packages/cli/src/commands/verify.ts
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/commands/verify/cancel.ts
  • packages/cli/src/commands/verify/list.ts
  • packages/cli/src/commands/verify/qr-confirm.ts
  • packages/cli/src/commands/verify/qr-scan.ts
  • packages/cli/src/commands/verify/recovery-key.ts
  • packages/cli/src/commands/verify/reset-recovery-key.ts
  • packages/cli/src/commands/verify/sas-confirm.ts
  • packages/cli/src/commands/verify/sas.ts
  • packages/cli/src/commands/verify/show.ts
  • packages/cli/src/commands/verify/start.ts
  • packages/cli/src/commands/verify/status.ts
  • packages/cli/src/commands/version.ts
  • packages/cli/src/commands/watch.ts
  • packages/cli/src/lib/account-login.ts
  • packages/cli/src/lib/api-values.ts
  • packages/cli/src/lib/app-api.ts
  • packages/cli/src/lib/app-state.ts
  • packages/cli/src/lib/argv.ts
  • packages/cli/src/lib/client.ts
  • packages/cli/src/lib/cloudflare-tunnel.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/lib/copy.ts
  • packages/cli/src/lib/desktop-auth.ts
  • packages/cli/src/lib/did-you-mean.ts
  • packages/cli/src/lib/env.ts
  • packages/cli/src/lib/errors.ts
  • packages/cli/src/lib/export.ts
  • packages/cli/src/lib/ink/components.tsx
  • packages/cli/src/lib/ink/format.ts
  • packages/cli/src/lib/ink/render.tsx
  • packages/cli/src/lib/ink/spinner.tsx
  • packages/cli/src/lib/ink/theme.ts
  • packages/cli/src/lib/installations.ts
  • packages/cli/src/lib/local-desktop.ts
  • packages/cli/src/lib/manifest.ts
  • packages/cli/src/lib/oauth.ts
  • packages/cli/src/lib/output.ts
  • packages/cli/src/lib/paging.ts
  • packages/cli/src/lib/pkce.ts
  • packages/cli/src/lib/profiles.ts
  • packages/cli/src/lib/prompts.ts
  • packages/cli/src/lib/recommended-plugins.ts
  • packages/cli/src/lib/resolve.ts
  • packages/cli/src/lib/runner.ts
  • packages/cli/src/lib/send-message.ts
  • packages/cli/src/lib/server-env.ts
  • packages/cli/src/lib/setup-login.ts
  • packages/cli/src/lib/target-status.ts
  • packages/cli/src/lib/targets.ts
  • packages/cli/src/lib/update-banner.ts
  • packages/cli/src/lib/wait.ts
  • packages/cli/src/plugin-sdk.ts
  • packages/cli/src/types/qrcode.d.ts
  • packages/cli/test/account-login.test.ts
  • packages/cli/test/cli-smoke.ts
  • packages/cli/test/cloudflare-tunnel.test.ts
  • packages/cli/test/e2e-staging.ts
  • packages/cli/test/e2e-staging/README.md
  • packages/cli/test/errors.test.ts
  • packages/cli/test/fixtures/fake-client.ts
  • packages/cli/test/messages-list-filter.test.ts
  • packages/cli/test/messages-search-validation.test.ts
  • packages/cli/test/plugin-sdk.test.ts
  • packages/cli/test/resolve.test.ts
  • packages/cli/test/watch-filter.test.ts
  • packages/cli/tsconfig.json
  • packages/npm/.gitignore
  • packages/npm/package.json
  • packages/npm/scripts/build.ts
  • run.sh
  • scripts/publish-packages.ts
  • scripts/release.ts
💤 Files with no reviewable changes (167)
  • packages/cli-plugin-cloudflare/LICENSE
  • packages/cli/release-please-config.json
  • packages/cli/scripts/mock
  • packages/cli/scripts/test
  • packages/cli/scripts/lint
  • packages/cli/CHANGELOG.md
  • packages/cli/beeper-setup-redesign-spec.md
  • packages/cli/SECURITY.md
  • packages/cli/scripts/unlink
  • packages/cli-plugin-cloudflare/src/index.ts
  • packages/cli/scripts/build-binaries.ts
  • packages/cli/scripts/check-manifest.ts
  • .changeset/config.json
  • packages/cli/src/commands/chats/search.ts
  • packages/cli/src/commands/version.ts
  • packages/cli/scripts/check-api-copy.ts
  • packages/cli/src/commands/plugins.ts
  • packages/cli/src/commands/config/path.ts
  • packages/cli/src/commands/contacts/list.ts
  • packages/cli/src/commands/update.ts
  • packages/cli-plugin-cloudflare/package.json
  • packages/cli/src/lib/client.ts
  • packages/cli/src/lib/pkce.ts
  • packages/cli/src/lib/runner.ts
  • packages/cli/src/commands/status.ts
  • packages/cli/src/commands/rpc.ts
  • packages/cli/src/lib/env.ts
  • packages/cli/src/commands/auth/status.ts
  • packages/cli/src/lib/copy.ts
  • packages/cli/scripts/build-homebrew-archive.ts
  • packages/cli/src/commands/accounts/list.ts
  • packages/cli/src/commands/_complete.ts
  • packages/cli/src/commands/auth/logout.ts
  • packages/cli/src/commands/verify/status.ts
  • packages/cli/scripts/run
  • packages/cli/src/commands/docs.ts
  • eslint.config.mjs
  • packages/cli/src/commands/auth/email/start.ts
  • packages/cli/src/lib/did-you-mean.ts
  • packages/cli/scripts/publish-local-release.ts
  • .github/workflows/release-doctor.yml
  • packages/cli/src/commands/chats/show.ts
  • packages/cli/src/lib/send-message.ts
  • packages/cli/src/lib/wait.ts
  • packages/cli/scripts/format
  • packages/cli/src/lib/argv.ts
  • packages/cli/src/commands/autocomplete.ts
  • packages/cli/src/commands/media/download.ts
  • packages/cli/src/commands/chats/list.ts
  • packages/cli/src/commands/verify/list.ts
  • packages/cli/src/commands/accounts/show.ts
  • packages/cli/src/commands/contacts/show.ts
  • packages/cli/src/commands/messages/export.ts
  • packages/cli/links.txt
  • packages/cli/src/commands/verify/approve.ts
  • packages/cli/src/commands/verify/sas-confirm.ts
  • packages/cli/scripts/build
  • packages/cli/src/lib/ink/spinner.tsx
  • packages/cli-plugin-cloudflare/tsconfig.json
  • packages/cli/scripts/read-signing-secrets.rb
  • packages/cli/src/lib/recommended-plugins.ts
  • packages/cli/bin/run.js
  • packages/cli-plugin-cloudflare/test/smoke.mjs
  • packages/cli/src/commands/targets/add/server.ts
  • packages/cli/src/commands/targets/enable.ts
  • packages/cli/src/commands/verify/reset-recovery-key.ts
  • packages/cli/src/lib/ink/components.tsx
  • packages/cli-plugin-cloudflare/README.md
  • packages/cli/bin/logo.js
  • packages/cli/scripts/generate-readme.ts
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/messages/search.ts
  • packages/cli/src/lib/update-banner.ts
  • packages/cli/bin/binary-bootstrap.js
  • packages/cli/src/commands/targets/logs.ts
  • packages/cli/src/lib/output.ts
  • packages/cli/scripts/bootstrap
  • packages/cli/src/commands/api/request.ts
  • packages/cli/src/commands/messages/context.ts
  • packages/cli/src/lib/ink/format.ts
  • packages/cli/scripts/link
  • packages/cli/bin/check-release-environment
  • packages/cli/src/lib/server-env.ts
  • packages/cli/src/commands/plugins/available.ts
  • packages/cli/src/commands/verify/qr-scan.ts
  • packages/cli/src/commands/chats/mark-read.ts
  • packages/cli/src/commands/verify/show.ts
  • packages/cli/src/commands/targets/restart.ts
  • packages/cli/src/commands/targets/show.ts
  • .changeset/README.md
  • packages/cli/src/commands/completion.ts
  • packages/cli/src/commands/config/get.ts
  • packages/cli-plugin-cloudflare/src/commands/targets/tunnel.ts
  • packages/cli/src/commands/chats/priority.ts
  • packages/cli/src/commands/api/post.ts
  • packages/cli/src/commands/chats/unmute.ts
  • packages/cli/src/commands/accounts/use.ts
  • packages/cli/src/commands/man.ts
  • packages/cli/src/commands/send/text.ts
  • packages/cli/src/lib/target-status.ts
  • packages/cli/src/commands/export.ts
  • packages/cli/src/commands/accounts/remove.ts
  • packages/cli/scripts/publish-homebrew-formula.ts
  • packages/cli/src/commands/send/react.ts
  • packages/cli/src/commands/watch.ts
  • packages/cli/src/commands/api/get.ts
  • packages/cli/src/commands/install/desktop.ts
  • packages/cli/src/commands/install/server.ts
  • packages/cli/src/commands/chats/pin.ts
  • packages/cli/src/commands/messages/list.ts
  • packages/cli/src/commands/messages/show.ts
  • packages/cli/src/commands/verify/sas.ts
  • packages/cli/src/commands/accounts/add.ts
  • packages/cli/src/commands/verify.ts
  • packages/cli/src/commands/send/sticker.ts
  • packages/cli/src/commands/targets/stop.ts
  • packages/cli/src/commands/chats/start.ts
  • packages/cli/src/commands/chats/focus.ts
  • packages/cli/src/commands/messages/edit.ts
  • packages/cli/src/commands/chats/draft.ts
  • packages/cli/src/commands/targets/list.ts
  • packages/cli/src/commands/chats/unremind.ts
  • packages/cli/src/commands/contacts/search.ts
  • packages/cli/src/commands/targets/remove.ts
  • packages/cli/src/commands/chats/mark-unread.ts
  • packages/cli/src/commands/messages/delete.ts
  • packages/cli/src/commands/send/unreact.ts
  • packages/cli/src/lib/ink/theme.ts
  • packages/cli/scripts/sign-macos-binaries.ts
  • packages/cli/src/commands/presence.ts
  • packages/cli/src/commands/bridges/list.ts
  • packages/cli/src/commands/chats/description.ts
  • packages/cli/src/commands.generated.ts
  • packages/cli/src/commands/chats/remind.ts
  • packages/cli/src/commands/verify/recovery-key.ts
  • packages/cli/src/commands/auth/email/response.ts
  • packages/cli/src/lib/manifest.ts
  • packages/cli/src/commands/targets/disable.ts
  • packages/cli/src/commands/targets/use.ts
  • packages/cli/src/commands/send/file.ts
  • packages/cli/src/commands/chats/unpin.ts
  • packages/cli/README.md
  • packages/cli/src/commands/chats/archive.ts
  • packages/cli/src/commands/chats/disappear.ts
  • packages/cli/src/commands/chats/mute.ts
  • packages/cli/src/commands/config/reset.ts
  • packages/cli/src/commands/chats/unarchive.ts
  • packages/cli/src/commands/bridges/show.ts
  • packages/cli/src/commands/targets/add/desktop.ts
  • packages/cli/src/commands/chats/avatar.ts
  • packages/cli/src/commands/targets/add/remote.ts
  • packages/cli/scripts/generate-command-map.ts
  • packages/cli/src/commands/config/set.ts
  • packages/cli/src/commands/send/voice.ts
  • packages/cli/src/commands/targets/start.ts
  • packages/cli/src/commands/verify/qr-confirm.ts
  • packages/cli/src/lib/setup-login.ts
  • packages/cli/src/commands/chats/notify-anyway.ts
  • packages/cli/src/commands/chats/rename.ts
  • packages/cli/src/lib/ink/render.tsx
  • packages/cli/src/commands/verify/cancel.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/commands/verify/start.ts
  • packages/cli/src/commands/targets/status.ts
  • packages/cli/src/commands/setup.ts
  • packages/cli/src/lib/targets.ts
  • packages/cli/src/lib/resolve.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/cli/safety-profiles/readonly.yaml
📜 Review details
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: README.md:178-178
Timestamp: 2026-05-27T22:40:21.941Z
Learning: In the beeper CLI, the correct command for recovery-key verification is `beeper verify recovery-key -t <target> --key "<key>"` (top-level `verify` subcommand, `--key` flag). The `auth.md` docs previously had incorrect syntax (`beeper auth verify recovery-key --code KEY`), which was fixed in commit be4ec8b of PR `#13`.
📚 Learning: 2026-05-27T22:40:16.795Z
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: README.md:178-178
Timestamp: 2026-05-27T22:40:16.795Z
Learning: In beeper CLI documentation, the correct recovery-key verification command syntax is:
`beeper verify recovery-key -t <target> --key "<key>"`.
Use the top-level `verify` subcommand and pass the recovery key via the `--key` flag. Do not use the previously incorrect form like `beeper auth verify recovery-key --code KEY`—update any doc snippets to match the supported syntax.

Applied to files:

  • README.md
🔇 Additional comments (30)
.github/workflows/ci.yml (1)

2-2: LGTM!

.github/workflows/publish-release.yml (1)

27-41: LGTM!

README.md (1)

3-5: LGTM!

package.json (1)

2-2: LGTM!

Also applies to: 14-14, 16-16, 18-18

packages/cli/bin/cli.js (1)

2-2: LGTM!

Also applies to: 4-4

packages/cli/bin/dev.js (1)

2-2: LGTM!

Also applies to: 4-4

packages/cli/package.json (1)

2-2: LGTM!

Also applies to: 8-8, 14-14, 16-16, 21-21, 25-25, 31-32, 37-37

packages/cli/safety-profiles/agent-safe.yaml (1)

1-23: LGTM!

packages/cli/safety-profiles/full.yaml (1)

1-5: LGTM!

packages/cli/src/cli/commands.ts (1)

98-738: LGTM!

Also applies to: 740-767, 769-1993

packages/cli/src/cli/main.ts (1)

1-33: LGTM!

packages/cli/src/cli/mcp.ts (1)

53-86: LGTM!

Also applies to: 88-122

packages/cli/src/cli/output.ts (1)

1-155: LGTM!

packages/cli/src/cli/parse.ts (1)

1-183: LGTM!

packages/cli/src/cli/policy.ts (1)

8-13: LGTM!

Also applies to: 25-53

packages/cli/src/cli/schema.ts (1)

1-103: LGTM!

packages/cli/src/cli/setup.ts (1)

1-639: LGTM!

packages/cli/src/lib/account-login.ts (1)

1-380: LGTM!

packages/cli/src/lib/api-values.ts (1)

1-11: LGTM!

packages/cli/src/lib/app-api.ts (1)

1-25: LGTM!

packages/cli/src/lib/app-state.ts (1)

1-132: LGTM!

packages/cli/src/lib/cloudflare-tunnel.ts (1)

1-217: LGTM!

packages/cli/src/lib/errors.ts (1)

1-45: LGTM!

packages/cli/src/lib/export.ts (1)

1-545: LGTM!

packages/cli/src/lib/local-desktop.ts (1)

7-8: LGTM!

Also applies to: 48-60, 99-108, 152-161

packages/cli/src/lib/oauth.ts (1)

1-2: LGTM!

Also applies to: 25-33, 64-78

packages/cli/src/lib/paging.ts (1)

1-9: LGTM!

packages/cli/src/lib/prompts.ts (1)

5-12: LGTM!

Also applies to: 14-18, 20-35

packages/cli/src/cli/types.ts (1)

1-49: LGTM!

packages/cli/src/lib/desktop-auth.ts (1)

1-3: LGTM!

Also applies to: 35-57, 60-69

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: 10

Caution

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

⚠️ Outside diff range comments (1)
packages/cli/src/cli/policy.ts (1)

54-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Recognize Windows-style relative profile paths.

resolveProfilePath only treats custom paths as explicit when isAbsolute(nameOrPath) or nameOrPath.includes('/') is true; Windows-style paths using \ (e.g., .\team.yaml or profiles\team.yaml) won’t resolve and will instead fall back to safety-profiles/<name>.yaml.

Proposed fix
 function resolveProfilePath(nameOrPath: string): string | undefined {
-  if (isAbsolute(nameOrPath) || nameOrPath.includes('/')) return existsSync(nameOrPath) ? nameOrPath : undefined
+  if (isAbsolute(nameOrPath) || /[\\/]/.test(nameOrPath)) {
+    return existsSync(nameOrPath) ? nameOrPath : undefined
+  }
   const filename = nameOrPath.endsWith('.yaml') ? nameOrPath : `${nameOrPath}.yaml`
   const here = dirname(fileURLToPath(import.meta.url))
   const path = join(dirname(dirname(here)), 'safety-profiles', filename)
   return existsSync(path) ? path : undefined
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/policy.ts` around lines 54 - 59, In resolveProfilePath,
detect Windows-style relative paths as explicit paths by treating backslashes
the same as forward slashes; change the conditional that currently uses
nameOrPath.includes('/') to check for either slash or backslash (e.g.,
/[\\/]/.test(nameOrPath) or nameOrPath.includes('\\') ||
nameOrPath.includes('/')), so inputs like ".\\team.yaml" or
"profiles\\team.yaml" will call existsSync(nameOrPath) directly instead of
falling back to safety-profiles resolution; keep the rest of the logic (filename
creation and safety-profiles lookup) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/docs/commands/messages-search.md`:
- Line 21: The docs show the same flag `-a, --account <value>, --acct` used
twice with conflicting meanings; update the `messages search` docs so the flag
has a single, unambiguous purpose: either (A) filter search results by account
(e.g., "Limit search results to account selector; repeatable") or (B) set the
active account context for the command (e.g., "Select active account context for
the command"), and remove or rename the duplicate entry that describes the
alternate behavior; search for usages of `-a`, `--account`, and `--acct` in the
`messages search` documentation and make the descriptions consistent and unique
across the file.

In `@packages/cli/docs/commands/README.md`:
- Around line 5-75: The generated commands index dropped the top-level verify
commands; restore entries by adding table rows for `verify` and its `verify
recovery-key` subcommand pointing to their docs (e.g., add links to verify.md
and verify/recovery-key.md) so the recovery-key flow is discoverable; update the
description for `verify recovery-key` to mention the syntax `beeper verify
recovery-key -t <target> --key "<key>"` and reference the existing command
implementations `packages/cli/src/commands/verify.ts` and
`packages/cli/src/commands/verify/recovery-key.ts` when locating where to add
these rows.

In `@packages/cli/scripts/generate-command-docs.ts`:
- Around line 67-70: The alias rendering omits value placeholders for
non-boolean flags: update the aliases mapping (the expression using
flag.aliases?.map(...)) to append the same placeholder as the primary flag when
flag.type !== 'boolean' (e.g. map alias => `--${alias}${flag.type === 'boolean'
? '' : ` <${flag.placeholder ?? 'value'}>`}`), so flag.aliases, flag.type and
flag.placeholder are used consistently when building the joined array of flag
strings.
- Around line 7-8: The script constructs root using new URL('..',
import.meta.url).pathname which produces unsafe Windows paths; replace that with
fileURLToPath(new URL('..', import.meta.url')) to get a proper filesystem path
and then compute docsDir as join(root, 'docs', 'commands'); update any uses of
docsDir (rm, mkdir, writeFile) to rely on this corrected path. Ensure you import
fileURLToPath from 'url' at the top and keep existing join usage intact so
functions like rm, mkdir, and writeFile operate on a valid filesystem path.

In `@packages/cli/src/cli/main.ts`:
- Around line 25-30: runWithTimeout currently only races promises so timed-out
commands keep running; change it to create an AbortController and pass
controller.signal into the command execution context so the command can be
cancelled. Update the call in main.ts where you invoke command.run(...) to
include an AbortSignal field (e.g., add signal: controller.signal to the object
passed to command.run) and modify runWithTimeout to abort the controller when
the timeout elapses; also propagate the signal type through the command.run
signature and any helper functions (and have mutating helpers check
signal.aborted or listen for signal) so timeout becomes a real cancellation
boundary rather than just a raced promise.
- Around line 32-34: When parseCommand() throws before parsed is set, the
fallback flag detection only checks argv for "--json" and misses aliases; update
the fallback assignment around parsed?.globalFlags (in main.ts) so the computed
flags passed into writeError() honors JSON aliases by checking
argv.includes('--json') || argv.includes('-j') || argv.includes('--machine')
(and keep the existing events detection), so writeError(error, flags) receives
the correct machine/json flag instead of falling back to human stderr; reference
parsed, argv, writeError, and ExitCodes.Generic when making the change.

In `@packages/cli/src/cli/mcp.ts`:
- Around line 124-133: The withTimeout function uses Promise.race and does not
cancel the underlying tool execution (allowing write-capable MCP tools to
continue mutating after timeout); update withTimeout (and callers like tool.run)
to support cancellation by plumbing an AbortSignal: create an AbortController
inside withTimeout, pass controller.signal into run() (or into tool.run(...))
and call controller.abort() when the timeout fires, and ensure run
implementations check the signal or accept it; if immediate cancellation
plumbing is not possible, restrict withTimeout to only invoke read-only tools
and add a guard (e.g., a flag or API like tool.isReadOnly) to prevent wrapping
mutating tools until they support AbortSignal.
- Around line 139-142: truncate currently slices by character count and appends
"..." which can exceed maxBytes (and mis-handle multibyte UTF-8). Modify the
truncate function to ensure the returned string's Buffer.byteLength is always <=
maxBytes: if truncation is needed, reserve space for the ellipsis bytes (compute
Buffer.byteLength('...')) and iteratively reduce the slice length (or
binary-search on slice end) until Buffer.byteLength(slice + '...') <= maxBytes,
then return that concatenation; if maxBytes is too small to include the
ellipsis, return a properly trimmed prefix that fits without adding "..." (use
Buffer.byteLength throughout). Ensure all references to truncate use this
updated behavior.

In `@packages/cli/src/cli/output.ts`:
- Around line 15-16: The current pipeline applies selectFields to the full
envelope then extracts primaryResult, which causes --results-only to lose
fields; instead compute the base result first by applying flags.resultsOnly via
primaryResult to value (use primaryResult(value) when flags.resultsOnly is
true), then apply selectFields to that result if flags.select is present (call
selectFields(result, flags.select)); update the variable order around
selectFields/primaryResult accordingly so selected/result reflect the requested
flags.

In `@packages/cli/src/lib/errors.ts`:
- Around line 14-16: The exit-code constants collide because AuthRequired and
NotReady both use value 4; change one of them (either AuthRequired or NotReady)
to a unique integer (e.g., bump NotReady to 5 or AuthRequired to 5) in the
constants block (symbols: AuthRequired, NotReady, EmptyResults) and update any
header comment and relevant tests that assert the published beeper exit-codes
contract so they reference the new distinct value; ensure all references in the
repository that rely on the previous numeric value are updated to the new
constant to preserve semantics.

---

Outside diff comments:
In `@packages/cli/src/cli/policy.ts`:
- Around line 54-59: In resolveProfilePath, detect Windows-style relative paths
as explicit paths by treating backslashes the same as forward slashes; change
the conditional that currently uses nameOrPath.includes('/') to check for either
slash or backslash (e.g., /[\\/]/.test(nameOrPath) or nameOrPath.includes('\\')
|| nameOrPath.includes('/')), so inputs like ".\\team.yaml" or
"profiles\\team.yaml" will call existsSync(nameOrPath) directly instead of
falling back to safety-profiles resolution; keep the rest of the logic (filename
creation and safety-profiles lookup) unchanged.
🪄 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: CHILL

Plan: Pro

Run ID: a6b32504-150b-4dd9-bee8-7027428679ff

📥 Commits

Reviewing files that changed from the base of the PR and between 044405c and 6b36b9e.

📒 Files selected for processing (85)
  • INTENT.md
  • packages/cli/README.md
  • packages/cli/docs/commands/README.md
  • packages/cli/docs/commands/accounts-add.md
  • packages/cli/docs/commands/accounts-list.md
  • packages/cli/docs/commands/api-request.md
  • packages/cli/docs/commands/auth-email-response.md
  • packages/cli/docs/commands/auth-email-start.md
  • packages/cli/docs/commands/auth-logout.md
  • packages/cli/docs/commands/chats-archive.md
  • packages/cli/docs/commands/chats-avatar.md
  • packages/cli/docs/commands/chats-description.md
  • packages/cli/docs/commands/chats-disappear.md
  • packages/cli/docs/commands/chats-draft.md
  • packages/cli/docs/commands/chats-focus.md
  • packages/cli/docs/commands/chats-list.md
  • packages/cli/docs/commands/chats-mute.md
  • packages/cli/docs/commands/chats-notify-anyway.md
  • packages/cli/docs/commands/chats-pin.md
  • packages/cli/docs/commands/chats-priority.md
  • packages/cli/docs/commands/chats-read.md
  • packages/cli/docs/commands/chats-remind.md
  • packages/cli/docs/commands/chats-rename.md
  • packages/cli/docs/commands/chats-show.md
  • packages/cli/docs/commands/chats-start.md
  • packages/cli/docs/commands/completion.md
  • packages/cli/docs/commands/config-get.md
  • packages/cli/docs/commands/config-keys.md
  • packages/cli/docs/commands/config-list.md
  • packages/cli/docs/commands/config-path.md
  • packages/cli/docs/commands/config-set.md
  • packages/cli/docs/commands/config-unset.md
  • packages/cli/docs/commands/contacts-list.md
  • packages/cli/docs/commands/doctor.md
  • packages/cli/docs/commands/exit-codes.md
  • packages/cli/docs/commands/export.md
  • packages/cli/docs/commands/install-desktop.md
  • packages/cli/docs/commands/install-server.md
  • packages/cli/docs/commands/mcp.md
  • packages/cli/docs/commands/media-download.md
  • packages/cli/docs/commands/messages-context.md
  • packages/cli/docs/commands/messages-delete.md
  • packages/cli/docs/commands/messages-edit.md
  • packages/cli/docs/commands/messages-list.md
  • packages/cli/docs/commands/messages-search.md
  • packages/cli/docs/commands/remove-account.md
  • packages/cli/docs/commands/remove-target.md
  • packages/cli/docs/commands/resolve-account.md
  • packages/cli/docs/commands/resolve-bridge.md
  • packages/cli/docs/commands/resolve-chat.md
  • packages/cli/docs/commands/resolve-contact.md
  • packages/cli/docs/commands/resolve-target.md
  • packages/cli/docs/commands/schema.md
  • packages/cli/docs/commands/send-file.md
  • packages/cli/docs/commands/send-presence.md
  • packages/cli/docs/commands/send-react.md
  • packages/cli/docs/commands/send-sticker.md
  • packages/cli/docs/commands/send-text.md
  • packages/cli/docs/commands/send-voice.md
  • packages/cli/docs/commands/setup.md
  • packages/cli/docs/commands/status.md
  • packages/cli/docs/commands/targets-add.md
  • packages/cli/docs/commands/targets-list.md
  • packages/cli/docs/commands/targets-logs.md
  • packages/cli/docs/commands/targets-runtime-restart.md
  • packages/cli/docs/commands/targets-runtime-start.md
  • packages/cli/docs/commands/targets-runtime-stop.md
  • packages/cli/docs/commands/targets-tunnel.md
  • packages/cli/docs/commands/use-account.md
  • packages/cli/docs/commands/use-target.md
  • packages/cli/docs/commands/version.md
  • packages/cli/docs/commands/watch.md
  • packages/cli/package.json
  • packages/cli/scripts/generate-command-docs.ts
  • packages/cli/src/cli/commands.ts
  • packages/cli/src/cli/main.ts
  • packages/cli/src/cli/mcp.ts
  • packages/cli/src/cli/output.ts
  • packages/cli/src/cli/parse.ts
  • packages/cli/src/cli/policy.ts
  • packages/cli/src/cli/schema.ts
  • packages/cli/src/cli/types.ts
  • packages/cli/src/lib/errors.ts
  • packages/cli/src/lib/targets.ts
  • packages/cli/test/cli-smoke.ts
✅ Files skipped from review due to trivial changes (58)
  • packages/cli/docs/commands/setup.md
  • packages/cli/docs/commands/chats-draft.md
  • packages/cli/docs/commands/use-target.md
  • packages/cli/docs/commands/schema.md
  • packages/cli/docs/commands/contacts-list.md
  • packages/cli/docs/commands/targets-runtime-restart.md
  • packages/cli/docs/commands/chats-archive.md
  • packages/cli/docs/commands/chats-read.md
  • packages/cli/docs/commands/messages-list.md
  • packages/cli/docs/commands/targets-list.md
  • packages/cli/docs/commands/resolve-account.md
  • packages/cli/docs/commands/targets-logs.md
  • packages/cli/docs/commands/chats-rename.md
  • packages/cli/docs/commands/status.md
  • packages/cli/docs/commands/accounts-list.md
  • packages/cli/docs/commands/targets-runtime-start.md
  • packages/cli/docs/commands/chats-priority.md
  • packages/cli/docs/commands/send-voice.md
  • packages/cli/docs/commands/resolve-target.md
  • packages/cli/docs/commands/version.md
  • packages/cli/docs/commands/targets-runtime-stop.md
  • packages/cli/docs/commands/use-account.md
  • packages/cli/docs/commands/install-desktop.md
  • packages/cli/docs/commands/chats-description.md
  • packages/cli/docs/commands/config-set.md
  • packages/cli/docs/commands/messages-edit.md
  • packages/cli/docs/commands/config-list.md
  • packages/cli/docs/commands/exit-codes.md
  • packages/cli/docs/commands/media-download.md
  • packages/cli/docs/commands/config-keys.md
  • packages/cli/docs/commands/targets-add.md
  • packages/cli/docs/commands/resolve-chat.md
  • packages/cli/docs/commands/resolve-contact.md
  • packages/cli/docs/commands/chats-list.md
  • packages/cli/docs/commands/mcp.md
  • packages/cli/docs/commands/config-get.md
  • packages/cli/docs/commands/chats-start.md
  • packages/cli/docs/commands/chats-remind.md
  • packages/cli/docs/commands/chats-focus.md
  • packages/cli/docs/commands/export.md
  • packages/cli/docs/commands/auth-email-response.md
  • packages/cli/docs/commands/messages-context.md
  • packages/cli/docs/commands/config-unset.md
  • packages/cli/docs/commands/completion.md
  • packages/cli/docs/commands/send-react.md
  • packages/cli/docs/commands/chats-notify-anyway.md
  • packages/cli/docs/commands/send-text.md
  • packages/cli/docs/commands/auth-email-start.md
  • packages/cli/docs/commands/remove-target.md
  • packages/cli/docs/commands/remove-account.md
  • packages/cli/docs/commands/chats-pin.md
  • packages/cli/docs/commands/targets-tunnel.md
  • packages/cli/docs/commands/chats-show.md
  • packages/cli/docs/commands/messages-delete.md
  • packages/cli/docs/commands/chats-disappear.md
  • packages/cli/docs/commands/auth-logout.md
  • packages/cli/docs/commands/install-server.md
  • packages/cli/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/cli/src/cli/schema.ts
  • packages/cli/package.json
  • packages/cli/src/cli/parse.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: README.md:178-178
Timestamp: 2026-05-27T22:40:21.941Z
Learning: In the beeper CLI, the correct command for recovery-key verification is `beeper verify recovery-key -t <target> --key "<key>"` (top-level `verify` subcommand, `--key` flag). The `auth.md` docs previously had incorrect syntax (`beeper auth verify recovery-key --code KEY`), which was fixed in commit be4ec8b of PR `#13`.
📚 Learning: 2026-05-27T22:40:16.795Z
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: README.md:178-178
Timestamp: 2026-05-27T22:40:16.795Z
Learning: In beeper CLI documentation, the correct recovery-key verification command syntax is:
`beeper verify recovery-key -t <target> --key "<key>"`.
Use the top-level `verify` subcommand and pass the recovery key via the `--key` flag. Do not use the previously incorrect form like `beeper auth verify recovery-key --code KEY`—update any doc snippets to match the supported syntax.

Applied to files:

  • packages/cli/docs/commands/watch.md
  • packages/cli/docs/commands/send-sticker.md
  • packages/cli/docs/commands/README.md
  • packages/cli/docs/commands/config-path.md
  • packages/cli/docs/commands/accounts-add.md
  • packages/cli/docs/commands/doctor.md
  • packages/cli/docs/commands/send-file.md
  • packages/cli/docs/commands/chats-avatar.md
  • packages/cli/docs/commands/messages-search.md
  • packages/cli/docs/commands/api-request.md
  • packages/cli/docs/commands/chats-mute.md
  • packages/cli/docs/commands/send-presence.md
  • packages/cli/docs/commands/resolve-bridge.md
  • INTENT.md
📚 Learning: 2026-05-27T22:40:20.614Z
Learnt from: juulieen
Repo: beeper/cli PR: 13
File: packages/cli/docs/setup.md:47-47
Timestamp: 2026-05-27T22:40:20.614Z
Learning: In beeper/cli documentation, ensure the recovery-key verification command is documented as a top-level subcommand: `beeper verify recovery-key --key "RECOVERY_KEY"`. Do not document it as `beeper auth verify recovery-key --code ...`, and use the `--key` flag (not `--code`).

Applied to files:

  • packages/cli/docs/commands/watch.md
  • packages/cli/docs/commands/send-sticker.md
  • packages/cli/docs/commands/README.md
  • packages/cli/docs/commands/config-path.md
  • packages/cli/docs/commands/accounts-add.md
  • packages/cli/docs/commands/doctor.md
  • packages/cli/docs/commands/send-file.md
  • packages/cli/docs/commands/chats-avatar.md
  • packages/cli/docs/commands/messages-search.md
  • packages/cli/docs/commands/api-request.md
  • packages/cli/docs/commands/chats-mute.md
  • packages/cli/docs/commands/send-presence.md
  • packages/cli/docs/commands/resolve-bridge.md
🪛 LanguageTool
INTENT.md

[style] ~113-~113: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... just a second output mode to delete. - Decide whether --events is product behavior ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~114-~114: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...s product behavior or debug plumbing. - Decide whether --wrap-untrusted and safety p...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 OpenGrep (1.22.0)
packages/cli/src/cli/main.ts

[ERROR] 57-57: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


| Name | Description |
| --- | --- |
| `-a, --account <value>, --acct` | Limit to account selector Repeatable. |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Disambiguate the duplicated --account flag docs.

Line 21 and Line 39 document the same -a, --account, --acct switch with different meanings. That makes it unclear whether messages search uses this flag to filter results, select the active account context, or both. Please clarify the distinction here or avoid documenting the same flag twice with conflicting descriptions.

Also applies to: 39-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/docs/commands/messages-search.md` at line 21, The docs show the
same flag `-a, --account <value>, --acct` used twice with conflicting meanings;
update the `messages search` docs so the flag has a single, unambiguous purpose:
either (A) filter search results by account (e.g., "Limit search results to
account selector; repeatable") or (B) set the active account context for the
command (e.g., "Select active account context for the command"), and remove or
rename the duplicate entry that describes the alternate behavior; search for
usages of `-a`, `--account`, and `--acct` in the `messages search` documentation
and make the descriptions consistent and unique across the file.

Comment on lines +5 to +75
| Command | Description | Aliases |
| --- | --- | --- |
| [`accounts add`](accounts-add.md) | Connect a chat account by bridge | |
| [`accounts list`](accounts-list.md) | List connected accounts | |
| [`api request`](api-request.md) | Call a raw Desktop API path with any supported HTTP method | |
| [`auth email response`](auth-email-response.md) | Finish email sign-in for a target | |
| [`auth email start`](auth-email-start.md) | Start email sign-in for a target | |
| [`auth logout`](auth-logout.md) | Clear stored authentication | |
| [`chats archive`](chats-archive.md) | Archive or unarchive a chat | |
| [`chats avatar`](chats-avatar.md) | Set or clear a chat avatar | |
| [`chats description`](chats-description.md) | Set or clear a chat description | |
| [`chats disappear`](chats-disappear.md) | Set a disappearing-message timer | |
| [`chats draft`](chats-draft.md) | Set or clear a chat draft | |
| [`chats focus`](chats-focus.md) | Focus a chat in Beeper | |
| [`chats list`](chats-list.md) | List chats | `chats ls` |
| [`chats mute`](chats-mute.md) | Mute or unmute a chat | |
| [`chats notify-anyway`](chats-notify-anyway.md) | Notify a chat anyway | |
| [`chats pin`](chats-pin.md) | Pin or unpin a chat | |
| [`chats priority`](chats-priority.md) | Set chat priority | |
| [`chats read`](chats-read.md) | Mark a chat read or unread | |
| [`chats remind`](chats-remind.md) | Set or clear a chat reminder | |
| [`chats rename`](chats-rename.md) | Rename a chat | |
| [`chats show`](chats-show.md) | Show chat details | |
| [`chats start`](chats-start.md) | Start a chat | |
| [`completion`](completion.md) | Generate shell completion scripts | |
| [`config get`](config-get.md) | Get a config value | `config show` |
| [`config keys`](config-keys.md) | List available config keys | `config list-keys`, `config names` |
| [`config list`](config-list.md) | List all config values | `config ls`, `config all` |
| [`config path`](config-path.md) | Print config file path | `config where` |
| [`config set`](config-set.md) | Set a config value | `config add`, `config update` |
| [`config unset`](config-unset.md) | Unset a config value | `config rm`, `config del`, `config remove` |
| [`contacts list`](contacts-list.md) | List contacts | `contacts search`, `contacts find` |
| [`doctor`](doctor.md) | Run diagnostics for config, target reachability, auth, and readiness | |
| [`exit-codes`](exit-codes.md) | Print stable exit codes for automation | `agent exit-codes`, `exitcodes` |
| [`export`](export.md) | Export accounts, chats, messages, transcripts, and attachments | |
| [`install desktop`](install-desktop.md) | Install Beeper Desktop locally | |
| [`install server`](install-server.md) | Install Beeper Server locally | |
| [`mcp`](mcp.md) | Run a typed MCP stdio server | |
| [`media download`](media-download.md) | Download message media | |
| [`messages context`](messages-context.md) | Show a message with surrounding context | |
| [`messages delete`](messages-delete.md) | Delete a message | |
| [`messages edit`](messages-edit.md) | Edit a message | |
| [`messages list`](messages-list.md) | List chat messages | `messages ls` |
| [`messages search`](messages-search.md) | Search messages across chats | `messages find` |
| [`remove account`](remove-account.md) | Remove an account | `accounts remove`, `accounts rm` |
| [`remove target`](remove-target.md) | Remove a target | `targets remove`, `targets rm` |
| [`resolve account`](resolve-account.md) | Resolve an account selector | |
| [`resolve bridge`](resolve-bridge.md) | Resolve a bridge selector | |
| [`resolve chat`](resolve-chat.md) | Resolve a chat selector | |
| [`resolve contact`](resolve-contact.md) | Resolve a contact selector | |
| [`resolve target`](resolve-target.md) | Resolve a target selector | |
| [`schema`](schema.md) | Print machine-readable command and flag schema | `help-json`, `helpjson` |
| [`send file`](send-file.md) | Send a file message | |
| [`send presence`](send-presence.md) | Send a typing indicator | |
| [`send react`](send-react.md) | Send or remove a reaction | |
| [`send sticker`](send-sticker.md) | Send a sticker | |
| [`send text`](send-text.md) | Send a text message | |
| [`send voice`](send-voice.md) | Send a voice note | |
| [`setup`](setup.md) | Make the selected target ready for messaging | |
| [`status`](status.md) | Show selected target and setup readiness | `st` |
| [`targets add`](targets-add.md) | Add a remote Beeper Desktop or Server target | |
| [`targets list`](targets-list.md) | List configured Beeper targets | `targets ls` |
| [`targets logs`](targets-logs.md) | Print logs for a local Beeper Desktop or Server install | |
| [`targets runtime restart`](targets-runtime-restart.md) | Restart a local server runtime | |
| [`targets runtime start`](targets-runtime-start.md) | Start a local target runtime | |
| [`targets runtime stop`](targets-runtime-stop.md) | Stop a local server runtime | |
| [`targets tunnel`](targets-tunnel.md) | Expose a target through Cloudflare Tunnel | |
| [`use account`](use-account.md) | Select the default account | `accounts use` |
| [`use target`](use-target.md) | Select the default target | `targets use` |
| [`version`](version.md) | Print CLI version | |
| [`watch`](watch.md) | Stream Desktop API WebSocket events | |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore the verify command family to the generated index.

This table appears to have dropped the top-level verify commands entirely, including the recovery-key flow. That makes a still-supported path undiscoverable in the published docs, even though the CLI still has packages/cli/src/commands/verify.ts and packages/cli/src/commands/verify/recovery-key.ts.

Based on learnings: In beeper CLI documentation, the correct recovery-key verification command syntax is beeper verify recovery-key -t <target> --key "<key>"; use the top-level verify subcommand and the --key flag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/docs/commands/README.md` around lines 5 - 75, The generated
commands index dropped the top-level verify commands; restore entries by adding
table rows for `verify` and its `verify recovery-key` subcommand pointing to
their docs (e.g., add links to verify.md and verify/recovery-key.md) so the
recovery-key flow is discoverable; update the description for `verify
recovery-key` to mention the syntax `beeper verify recovery-key -t <target>
--key "<key>"` and reference the existing command implementations
`packages/cli/src/commands/verify.ts` and
`packages/cli/src/commands/verify/recovery-key.ts` when locating where to add
these rows.

Comment on lines +7 to +8
const root = new URL('..', import.meta.url).pathname
const docsDir = join(root, 'docs', 'commands')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and print the relevant top section with line numbers
if [ -f packages/cli/scripts/generate-command-docs.ts ]; then
  echo "== File: packages/cli/scripts/generate-command-docs.ts (top 80 lines) =="
  nl -ba packages/cli/scripts/generate-command-docs.ts | sed -n '1,120p'
else
  echo "Missing: packages/cli/scripts/generate-command-docs.ts" >&2
  exit 1
fi

# Search for other uses of URL(...).pathname in the repo (to understand patterns)
echo "== Search: URL(...).pathname =="
rg -n "new URL\\([^\\)]*\\)\\.pathname|\\.pathname\\b" packages/cli/scripts packages/cli -S || true

# Search for fileURLToPath usage (to see if already standardized)
echo "== Search: fileURLToPath =="
rg -n "fileURLToPath" packages/cli -S || true

Repository: beeper/cli

Length of output: 188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/cli/scripts/generate-command-docs.ts"

if [ -f "$FILE" ]; then
  echo "== File: $FILE (top 120 lines) =="
  cat -n "$FILE" | sed -n '1,160p'
else
  echo "Missing: $FILE" >&2
  exit 1
fi

echo "== Search: URL(...).pathname in scripts =="
rg -n "new URL\\([^\\)]*\\)\\.pathname|\\.pathname\\b" packages/cli/scripts packages/cli -S || true

echo "== Search: fileURLToPath usage =="
rg -n "fileURLToPath" packages/cli -S || true

Repository: beeper/cli

Length of output: 7037


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/cli/scripts/prepare-desktop-api.ts"
if [ -f "$FILE" ]; then
  echo "== File: $FILE (top 40 lines) =="
  cat -n "$FILE" | sed -n '1,80p'
fi

Repository: beeper/cli

Length of output: 3396


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/cli/scripts/prepare-desktop-api.ts"
if [ -f "$FILE" ]; then
  echo "== File: $FILE (top 80 lines) =="
  cat -n "$FILE" | sed -n '1,90p'
fi

Repository: beeper/cli

Length of output: 3396


Use fileURLToPath() for the script root.

packages/cli/scripts/generate-command-docs.ts derives root from new URL('..', import.meta.url).pathname, which is not a safe filesystem path on Windows (leading slash before drive letter and percent-encoding). Since root feeds into docsDir for rm, mkdir, and writeFile, doc generation can target the wrong location or fail.

Proposed fix
+import { fileURLToPath } from 'node:url'
 import { mkdir, rm, writeFile } from 'node:fs/promises'
 import { join } from 'node:path'
 import { commands } from '../src/cli/commands.js'
 import { globalFlagSpecs } from '../src/cli/parse.js'
 import type { ArgSpec, CommandSpec, FlagSpec } from '../src/cli/types.js'
 
-const root = new URL('..', import.meta.url).pathname
+const root = fileURLToPath(new URL('..', import.meta.url))
 const docsDir = join(root, 'docs', 'commands')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const root = new URL('..', import.meta.url).pathname
const docsDir = join(root, 'docs', 'commands')
import { fileURLToPath } from 'node:url'
import { mkdir, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { commands } from '../src/cli/commands.js'
import { globalFlagSpecs } from '../src/cli/parse.js'
import type { ArgSpec, CommandSpec, FlagSpec } from '../src/cli/types.js'
const root = fileURLToPath(new URL('..', import.meta.url))
const docsDir = join(root, 'docs', 'commands')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/scripts/generate-command-docs.ts` around lines 7 - 8, The script
constructs root using new URL('..', import.meta.url).pathname which produces
unsafe Windows paths; replace that with fileURLToPath(new URL('..',
import.meta.url')) to get a proper filesystem path and then compute docsDir as
join(root, 'docs', 'commands'); update any uses of docsDir (rm, mkdir,
writeFile) to rely on this corrected path. Ensure you import fileURLToPath from
'url' at the top and keep existing join usage intact so functions like rm,
mkdir, and writeFile operate on a valid filesystem path.

Comment on lines +67 to +70
flag.short ? `-${flag.short}` : undefined,
`--${flag.name}${flag.type === 'boolean' ? '' : ` <${flag.placeholder ?? 'value'}>`}`,
...(flag.aliases ?? []).map(alias => `--${alias}`),
].filter(Boolean).join(', ')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Render value placeholders for non-boolean aliases too.

Right now aliased valued flags are emitted like --select <value>, --fields, --project, which documents --fields and --project as bare booleans even though they take the same value as the primary flag.

Proposed fix
 function flagRow(flag: FlagSpec): string {
+  const valueSuffix = flag.type === 'boolean' ? '' : ` <${flag.placeholder ?? 'value'}>`
   const tokens = [
     flag.short ? `-${flag.short}` : undefined,
-    `--${flag.name}${flag.type === 'boolean' ? '' : ` <${flag.placeholder ?? 'value'}>`}`,
-    ...(flag.aliases ?? []).map(alias => `--${alias}`),
+    `--${flag.name}${valueSuffix}`,
+    ...(flag.aliases ?? []).map(alias => `--${alias}${valueSuffix}`),
   ].filter(Boolean).join(', ')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/scripts/generate-command-docs.ts` around lines 67 - 70, The
alias rendering omits value placeholders for non-boolean flags: update the
aliases mapping (the expression using flag.aliases?.map(...)) to append the same
placeholder as the primary flag when flag.type !== 'boolean' (e.g. map alias =>
`--${alias}${flag.type === 'boolean' ? '' : ` <${flag.placeholder ??
'value'}>`}`), so flag.aliases, flag.type and flag.placeholder are used
consistently when building the joined array of flag strings.

Comment on lines +25 to +30
const result = await runWithTimeout(() => command.run({
args: positionals,
commandPath: command.path,
flags,
globalFlags,
}), globalFlags.timeout)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

--timeout does not actually stop the command.

runWithTimeout() only races promises; it never cancels the in-flight command.run(). A timed-out mutating command can keep running and still apply side effects after the CLI has already reported failure.

Thread an AbortSignal through the command context and have the API/helpers honor it so timeout is an actual cancellation boundary, not just an alternate return path.

Also applies to: 38-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/main.ts` around lines 25 - 30, runWithTimeout currently
only races promises so timed-out commands keep running; change it to create an
AbortController and pass controller.signal into the command execution context so
the command can be cancelled. Update the call in main.ts where you invoke
command.run(...) to include an AbortSignal field (e.g., add signal:
controller.signal to the object passed to command.run) and modify runWithTimeout
to abort the controller when the timeout elapses; also propagate the signal type
through the command.run signature and any helper functions (and have mutating
helpers check signal.aborted or listen for signal) so timeout becomes a real
cancellation boundary rather than just a raced promise.

Comment on lines +32 to +34
} catch (error) {
const flags = parsed?.globalFlags ?? { events: argv.includes('--events'), json: argv.includes('--json') }
process.exitCode = writeError(error, flags) || ExitCodes.Generic
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor JSON aliases in the parse-failure fallback path.

When parseCommand() throws before parsed is assigned, Line 33 only checks raw argv for --json. Requests that used -j or --machine fall back to human stderr, so machine-readable error handling breaks exactly on invalid input.

Suggested fix
-    const flags = parsed?.globalFlags ?? { events: argv.includes('--events'), json: argv.includes('--json') }
+    const flags = parsed?.globalFlags ?? {
+      events: argv.includes('--events'),
+      json: argv.includes('--json') || argv.includes('-j') || argv.includes('--machine'),
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
const flags = parsed?.globalFlags ?? { events: argv.includes('--events'), json: argv.includes('--json') }
process.exitCode = writeError(error, flags) || ExitCodes.Generic
} catch (error) {
const flags = parsed?.globalFlags ?? {
events: argv.includes('--events'),
json: argv.includes('--json') || argv.includes('-j') || argv.includes('--machine'),
}
process.exitCode = writeError(error, flags) || ExitCodes.Generic
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/main.ts` around lines 32 - 34, When parseCommand()
throws before parsed is set, the fallback flag detection only checks argv for
"--json" and misses aliases; update the fallback assignment around
parsed?.globalFlags (in main.ts) so the computed flags passed into writeError()
honors JSON aliases by checking argv.includes('--json') || argv.includes('-j')
|| argv.includes('--machine') (and keep the existing events detection), so
writeError(error, flags) receives the correct machine/json flag instead of
falling back to human stderr; reference parsed, argv, writeError, and
ExitCodes.Generic when making the change.

Comment on lines +124 to +133
async function withTimeout<T>(seconds: number, run: () => Promise<T>): Promise<T> {
if (seconds <= 0) throw new Error('--timeout-seconds must be greater than zero')
let timeout: NodeJS.Timeout | undefined
try {
return await Promise.race([
run(),
new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error(`MCP tool timed out after ${seconds}s`)), seconds * 1000)
}),
])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The timeout does not cancel the underlying tool execution.

Promise.race() only stops waiting. For write-capable MCP tools, the command can keep mutating state after the client has already received a timeout and may retry.

If cancellation is not plumbed yet, consider either passing an AbortSignal through tool.run(...) or limiting timeout-enforced MCP execution to read-only tools until commands can be interrupted safely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/mcp.ts` around lines 124 - 133, The withTimeout function
uses Promise.race and does not cancel the underlying tool execution (allowing
write-capable MCP tools to continue mutating after timeout); update withTimeout
(and callers like tool.run) to support cancellation by plumbing an AbortSignal:
create an AbortController inside withTimeout, pass controller.signal into run()
(or into tool.run(...)) and call controller.abort() when the timeout fires, and
ensure run implementations check the signal or accept it; if immediate
cancellation plumbing is not possible, restrict withTimeout to only invoke
read-only tools and add a guard (e.g., a flag or API like tool.isReadOnly) to
prevent wrapping mutating tools until they support AbortSignal.

Comment on lines +139 to +142
function truncate(value: string, maxBytes: number): string {
if (maxBytes <= 0) return value
return Buffer.byteLength(value) <= maxBytes ? value : `${value.slice(0, maxBytes)}...`
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

truncate() can exceed the configured byte cap.

When truncation happens, this returns maxBytes + 3 bytes for ASCII because ... is appended after slicing, and UTF-8 multibyte content makes the bound even less accurate. That breaks the max-output-bytes contract for MCP clients.

Proposed fix
 function truncate(value: string, maxBytes: number): string {
   if (maxBytes <= 0) return value
-  return Buffer.byteLength(value) <= maxBytes ? value : `${value.slice(0, maxBytes)}...`
+  const bytes = Buffer.from(value)
+  if (bytes.byteLength <= maxBytes) return value
+  if (maxBytes <= 3) return '.'.repeat(maxBytes)
+  return `${bytes.subarray(0, maxBytes - 3).toString('utf8')}...`
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/mcp.ts` around lines 139 - 142, truncate currently
slices by character count and appends "..." which can exceed maxBytes (and
mis-handle multibyte UTF-8). Modify the truncate function to ensure the returned
string's Buffer.byteLength is always <= maxBytes: if truncation is needed,
reserve space for the ellipsis bytes (compute Buffer.byteLength('...')) and
iteratively reduce the slice length (or binary-search on slice end) until
Buffer.byteLength(slice + '...') <= maxBytes, then return that concatenation; if
maxBytes is too small to include the ellipsis, return a properly trimmed prefix
that fits without adding "..." (use Buffer.byteLength throughout). Ensure all
references to truncate use this updated behavior.

Comment on lines +15 to +16
const selected = flags.select ? selectFields(value, flags.select) : value
const result = flags.resultsOnly ? primaryResult(selected) : selected
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply --results-only before --select.

Right now selectFields() runs on the full envelope, so --json --results-only --select id,text collapses to {} for responses shaped like { ok: true, data: [...] }. That breaks the new machine-readable flow on enveloped results.

💡 Suggested fix
   if (flags.json) {
-    const selected = flags.select ? selectFields(value, flags.select) : value
-    const result = flags.resultsOnly ? primaryResult(selected) : selected
-    const data = flags.wrapUntrusted ? wrapUntrusted(result) : result
+    const result = flags.resultsOnly ? primaryResult(value) : value
+    const selected = flags.select ? selectFields(result, flags.select) : result
+    const data = flags.wrapUntrusted ? wrapUntrusted(selected) : selected
     process.stdout.write(`${JSON.stringify(data, null, 2)}\n`)
   } else writeText(value, flags.plain, command, flags.full)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selected = flags.select ? selectFields(value, flags.select) : value
const result = flags.resultsOnly ? primaryResult(selected) : selected
if (flags.json) {
const result = flags.resultsOnly ? primaryResult(value) : value
const selected = flags.select ? selectFields(result, flags.select) : result
const data = flags.wrapUntrusted ? wrapUntrusted(selected) : selected
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`)
} else writeText(value, flags.plain, command, flags.full)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/output.ts` around lines 15 - 16, The current pipeline
applies selectFields to the full envelope then extracts primaryResult, which
causes --results-only to lose fields; instead compute the base result first by
applying flags.resultsOnly via primaryResult to value (use primaryResult(value)
when flags.resultsOnly is true), then apply selectFields to that result if
flags.select is present (call selectFields(result, flags.select)); update the
variable order around selectFields/primaryResult accordingly so selected/result
reflect the requested flags.

Comment on lines +14 to 16
EmptyResults: 3,
AuthRequired: 4,
NotReady: 4,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Give AuthRequired and NotReady distinct exit codes.

These constants now collide on value 4, but beeper exit-codes still exposes auth_required and not_ready as separate states. Automation can no longer tell “log in first” from “setup is incomplete”.

Pick a unique code for one of them and update the header comment/tests to match the published contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/errors.ts` around lines 14 - 16, The exit-code constants
collide because AuthRequired and NotReady both use value 4; change one of them
(either AuthRequired or NotReady) to a unique integer (e.g., bump NotReady to 5
or AuthRequired to 5) in the constants block (symbols: AuthRequired, NotReady,
EmptyResults) and update any header comment and relevant tests that assert the
published beeper exit-codes contract so they reference the new distinct value;
ensure all references in the repository that rely on the previous numeric value
are updated to the new constant to preserve semantics.

AuthRequired: 3,
EmptyResults: 3,
AuthRequired: 4,
NotReady: 4,
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: AuthRequired and NotReady collide on value 4. When EmptyResults was inserted at 3, AuthRequired was bumped to 4 but NotReady was left at 4 — the two now share the same exit code. In packages/cli/src/cli/output.ts:errorCode() the AuthRequired branch runs first, so every NotReady failure surfaces as code: "auth_required" to JSON consumers. The new beeper exit-codes command (commands.ts:963-977) still publishes them as two distinct codes, so the documented contract lies. The header comment at L1-9 also dropped its line for not_ready entirely. Bump one of them to a free integer (e.g., NotReady: 7) and restore the header line.

Suggested change
NotReady: 4,
AuthRequired: 4,
NotReady: 7,

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant