Skip to content

CLI v2#7

Open
batuhan wants to merge 53 commits into
mainfrom
codex-typescript-oclif-cli
Open

CLI v2#7
batuhan wants to merge 53 commits into
mainfrom
codex-typescript-oclif-cli

Conversation

@batuhan
Copy link
Copy Markdown
Member

@batuhan batuhan commented May 14, 2026

No description provided.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Complete rewrite of Beeper CLI in TypeScript with oclif framework.
    • Added comprehensive commands for account management, chat operations, messaging, E2EE verification, and data export.
    • Introduced interactive authentication with PKCE OAuth flow and Desktop API integration.
    • Added structured UI rendering with human-readable and JSON output modes.
    • New data export functionality with resumable checkpoints and attachment downloads.
  • Build & Infrastructure

    • Migrated from Go to Node.js/npm/pnpm-based build system.
    • Updated CI/CD workflows for TypeScript compilation and testing.
    • Added Homebrew packaging and release automation.
  • Documentation

    • Completely updated README with TypeScript SDK documentation and command reference.

Walkthrough

Monorepo migrates from Go to a Node/TypeScript oclif CLI. Adds CLI package, commands, UI, auth/export utilities, tests, CI and release workflows, Homebrew packaging, and documentation. Removes Go code, custom encoders, autocomplete, and related tests/config.

Changes

Node CLI migration and decommission Go

Layer / File(s) Summary
End-to-end CLI port, tooling, and removals
.github/..., .changeset/..., packages/cli/..., bin/*, package.json, README.md, pkg/*, internal/*, go.mod
Adds oclif-based CLI with commands, libs, UI, tests, CI/release scripts, Homebrew packaging, and removes legacy Go sources/config/utilities.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as beeper (oclif)
  participant SDK as BeeperDesktop SDK
  participant API as Desktop API
  User->>CLI: beeper chats search --json
  CLI->>SDK: createClient(baseURL, token)
  SDK->>API: GET /v1/chats/search
  API-->>SDK: 200 JSON
  SDK-->>CLI: results
  CLI-->>User: JSON list
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ 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 codex-typescript-oclif-cli

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

Note

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

🟡 Minor comments (14)
.agents/skills/opentui/docs/components/ascii-font.mdx-81-84 (1)

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

Typo: "Pallet" should be "Palette".

The font style name appears to be "Palette" (a range of colors/styles) rather than "Pallet" (a wooden shipping platform).

📝 Proposed fix
-// Pallet style font
+// Palette style font
 {
-  font: "pallet"
+  font: "palette"
 }
🤖 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 @.agents/skills/opentui/docs/components/ascii-font.mdx around lines 81 - 84,
The font style name is misspelled as "pallet" in the font config object; update
the value for the font key (the object containing font: "pallet") to "palette"
so the style name reads font: "palette" (ensure you update any identical
occurrences of "pallet" in the same component to maintain consistency).
.agents/skills/opentui/docs/keymap/hosts.mdx-55-55 (1)

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

Use correct "macOS" branding.

Apple's operating system should be capitalized as "macOS" per official branding guidelines.

📝 Proposed fix
-| `platform`        | `macos`, `windows`, `linux` or `unknown`         |
+| `platform`        | `macOS`, `windows`, `linux` or `unknown`         |
🤖 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 @.agents/skills/opentui/docs/keymap/hosts.mdx at line 55, Update the platform
table entry so Apple’s OS uses the correct branding: replace the literal `macos`
string with `macOS` in the table row that documents the `platform` field (the
cell currently showing `macos`, `windows`, `linux` or `unknown`), preserving the
rest of the format and punctuation.
.agents/skills/opentui/docs/components/tab-select.mdx-158-158 (1)

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

Use the exported event constant instead of a string literal.

Line 158 uses the string literal "itemSelected" while the rest of the documentation consistently demonstrates TabSelectRenderableEvents.ITEM_SELECTED. Use the constant to maintain consistency with the earlier examples and prevent silent drift if event names change.

Proposed doc fix
-tabs.on("itemSelected", (index, option) => {
+tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {
🤖 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 @.agents/skills/opentui/docs/components/tab-select.mdx at line 158, Replace
the hard-coded event string in the event listener with the exported
enum/constant to keep docs consistent: change the tabs.on("itemSelected",
(index, option) => { ...) usage to use TabSelectRenderableEvents.ITEM_SELECTED
so the listener references the exported event constant
(TabSelectRenderableEvents.ITEM_SELECTED) instead of the string literal.
.agents/skills/opentui/docs/components/select.mdx-79-82 (1)

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

Add SelectOption type import to event handler snippets.

The code examples at lines 81 and 91 use SelectOption as a type annotation in function parameters but don't import it. Copy-pasting these snippets into a TypeScript file will fail to compile with "SelectOption is not defined."

Fix
-import { SelectRenderableEvents } from "@opentui/core"
+import { SelectRenderableEvents, type SelectOption } from "@opentui/core"

Also applies to line 91.

🤖 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 @.agents/skills/opentui/docs/components/select.mdx around lines 79 - 82, The
examples register a handler using menu.on(SelectRenderableEvents.ITEM_SELECTED,
...) and annotate the callback parameter with SelectOption but never import that
type, causing TypeScript compile errors; fix by adding an import for
SelectOption from "@opentui/core" alongside the existing SelectRenderableEvents
import so the two snippets (the handler at the menu.on call shown and the
similar one on line 91) reference a defined type and compile correctly.
.agents/skills/opentui/docs/keymap/react.mdx-115-117 (1)

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

Add React and OpenTUI imports to the reactiveMatcherFromStore() code example.

The snippet on lines 115–127 uses useMemo, reactiveMatcherFromStore, and useBindings without showing their imports. While the "Basic usage" example earlier includes complete imports, this pattern example omits them, making it fail if copied into a standalone file.

+import { useMemo } from "react"
+import { reactiveMatcherFromStore, useBindings } from "@opentui/keymap/react"
+
 const matcher = useMemo(
   () => reactiveMatcherFromStore(store.subscribe, store.getSnapshot, (mode) => mode === "normal"),
   [store],
 )
 
 useBindings(
🤖 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 @.agents/skills/opentui/docs/keymap/react.mdx around lines 115 - 117, The
example is missing imports for React hooks and OpenTUI helpers; add imports for
useMemo (from 'react' or 'preact' depending on the project), and import
reactiveMatcherFromStore and useBindings from the OpenTUI package so the snippet
using useMemo, reactiveMatcherFromStore, and useBindings is complete and
copy-pasteable; update the top of the example to include these imports and
ensure the named symbols (useMemo, reactiveMatcherFromStore, useBindings) match
the existing exported names used elsewhere in the docs.
src/commands/unarchive.ts-7-7 (1)

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

Use unarchive-specific help summary.

Line 7 points to apiCopy.chats.archive, so help text for this command is likely misleading. Use an unarchive-specific summary key/string.

Suggested change
-  static override summary = apiCopy.chats.archive
+  static override summary = apiCopy.chats.unarchive
🤖 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 `@src/commands/unarchive.ts` at line 7, The command's help summary wrongly
reuses apiCopy.chats.archive; update the static override summary in the
unarchive command (the static override summary field) to reference the
unarchive-specific copy key/string (e.g., apiCopy.chats.unarchive or the
appropriate unarchive summary key) so the CLI help shows an accurate description
for the unarchive command.
src/commands/message-expiry.ts-24-24 (1)

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

Align validation error text with actual rule.

Line 24 allows 0 but the message says “positive integer”. Use “non-negative integer” (or change the check to <= 0).

Proposed fix
-    if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('SECONDS must be a positive integer or "off"')
+    if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('SECONDS must be a non-negative integer or "off"')
🤖 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 `@src/commands/message-expiry.ts` at line 24, The validation message says
"positive integer" but the code permits 0; update the check and message to be
consistent: either change the condition in the message/throw in the message to
say "non-negative integer" (so keep the current condition using expiry !== null
&& (!Number.isInteger(expiry) || expiry < 0)) or tighten the validation to
forbid 0 by using expiry <= 0 and keep "positive integer" in the Error; locate
the validation around the expiry variable in the message-expiry command (the if
that throws Error('SECONDS must be a positive integer or "off"')) and make the
corresponding change so the thrown error text matches the numeric check.
scripts/check-api-copy.mjs-21-22 (1)

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

Add drift coverage for contacts.list.

apiCopy.contacts.list is defined but not included in checks, so this script won’t detect drift for that entry.

Proposed fix
   ['chats.start', 'resources/chats/chats.d.ts', 'start'],
+  ['contacts.list', 'resources/accounts/contacts.d.ts', 'list'],
   ['contacts.search', 'resources/accounts/contacts.d.ts', 'search'],
🤖 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 `@scripts/check-api-copy.mjs` around lines 21 - 22, The checks array in
scripts/check-api-copy.mjs is missing coverage for apiCopy.contacts.list, so add
an entry for it (e.g., ['contacts.list', 'resources/contacts.d.ts', 'list']) to
the list of checks so the script will detect drift for contacts.list; update the
checks array near the existing ['contacts.search', ...] and ['messages.delete',
...] entries to include that tuple referencing the apiCopy key (contacts.list),
the d.ts resource (resources/contacts.d.ts), and the method name ('list').
src/commands/config/set.ts-8-14 (1)

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

Validate baseURL before writing config.

Line 13 persists raw input, so malformed URLs can poison config and cause later command failures.

Suggested fix
   async run(): Promise<void> {
     const { args } = await this.parse(ConfigSet)
+    if (args.key === 'baseURL') {
+      try {
+        new URL(args.value)
+      } catch {
+        this.error(`Invalid baseURL: ${args.value}`)
+      }
+    }
     await updateConfig(config => ({ ...config, [args.key]: args.value }))
     this.log(`${args.key}=${args.value}`)
   }
🤖 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 `@src/commands/config/set.ts` around lines 8 - 14, When handling the
ConfigSet.run() flow, validate args.value when args.key === 'baseURL' before
calling updateConfig to avoid persisting malformed URLs; in run(), check if
args.key === 'baseURL' and attempt to parse args.value with the URL constructor
(or otherwise validate scheme/host), and if parsing fails call this.error(...)
or throw a descriptive error instead of writing to updateConfig, otherwise
proceed with updateConfig(config => ({ ...config, [args.key]: args.value })) and
this.log.
src/commands/send-file.ts-51-55 (1)

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

Add an explicit guard for missing pendingMessageID when --wait is enabled.

The wait path currently assumes pendingMessageID exists; if it does not, this fails deeper in waitForMessage with a less clear error.

Suggested fix
     if (flags.wait) {
+      if (!result.pendingMessageID) {
+        this.error('Cannot wait for resolution: send did not return a pendingMessageID.')
+      }
       const resolved = await waitForMessage(client, chatID, result.pendingMessageID, {
         intervalMs: flags['wait-interval'],
         timeoutMs: flags['wait-timeout'],
       })
🤖 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 `@src/commands/send-file.ts` around lines 51 - 55, When flags.wait is true, the
code calls waitForMessage(client, chatID, result.pendingMessageID, ...) without
verifying result.pendingMessageID; add an explicit guard that checks if
result.pendingMessageID is present before calling waitForMessage and if missing
throw or return a clear error (e.g., log and exit or reject) indicating that
pendingMessageID is required for the --wait path. Update the block around
flags.wait to validate result.pendingMessageID and only call waitForMessage when
it exists, otherwise provide a descriptive error mentioning pendingMessageID and
the --wait flag.
src/commands/create-chat.ts-24-30 (1)

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

Enforce single-chat participant cardinality before sending the request.

--type single currently allows multiple --participant values, which can produce an ambiguous/invalid create payload and fail downstream.

Suggested fix
   async run(): Promise<void> {
     const { flags } = await this.parse(CreateChat)
     const client = await createClient(flags)
     const accountID = await resolveAccountID(client, flags.account)
+    if (flags.type === 'single' && flags.participant.length !== 1) {
+      this.error('`--type single` requires exactly one `--participant`.')
+    }
+
     const result = await client.chats.create({
       accountID,
       messageText: flags.message,
       participantIDs: flags.participant,
       title: flags.title,
       type: flags.type as 'single' | 'group',
     })
🤖 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 `@src/commands/create-chat.ts` around lines 24 - 30, Before calling
client.chats.create, validate the participant cardinality when flags.type ===
'single': check flags.participant (or the parsed participant array) and if type
is 'single' ensure exactly one participant is provided; if not, throw or print a
clear error and exit (or return) to avoid sending an invalid payload to
client.chats.create. Locate the create call (client.chats.create) and the CLI
flags (flags.participant, flags.type, flags.message, etc.) and add this guard so
only a single participant is allowed for 'single' chats.
src/commands/api/post.ts-20-21 (1)

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

Handle invalid --body JSON with a CLI-friendly error.

Line 20 parses untrusted CLI input directly; malformed JSON will throw an unhandled exception. Wrap in try-catch and rethrow with a clear message (other commands like watch.ts and rpc.ts already follow this pattern).

Suggested patch
-    const body = JSON.parse(flags.body) as Record<string, unknown>
+    let body: Record<string, unknown>
+    try {
+      const parsed = JSON.parse(flags.body)
+      if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
+        throw new Error('Body must be a JSON object')
+      }
+      body = parsed as Record<string, unknown>
+    } catch (error) {
+      throw new Error(`Invalid --body JSON: ${(error 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 `@src/commands/api/post.ts` around lines 20 - 21, The code parses flags.body
directly which will throw on malformed JSON; wrap the JSON.parse(flags.body)
call in a try/catch (same pattern used in watch.ts and rpc.ts) and when
JSON.parse fails, throw a CLI-friendly error (e.g., new Error("Invalid JSON for
--body: " + err.message)) so the user sees a clear message; ensure you still
call client.post(args.path, { body }) and printData(await client.post(...),
flags.json ? 'json' : 'human') after successful parsing, referencing flags.body,
client.post, and printData.
src/commands/messages/search.ts-20-23 (1)

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

Add explicit defaults to negatable boolean flags to match help text.

Lines 20 and 22 declare negatable flags without explicit defaults. The help text describes specific intended behaviors ("Exclude low-priority chats" and "Include muted chats"), but when these flags are omitted, they resolve to undefined before being passed to the API (lines 42–43). This creates a mismatch: the help text implies these flags have defaults, but the implementation passes undefined instead.

Align the flag definitions with their documented behavior:

Proposed fix
-    'exclude-low-priority': Flags.boolean({ allowNo: true, description: 'Exclude low-priority chats. Use --no-exclude-low-priority to include all.' }),
+    'exclude-low-priority': Flags.boolean({ default: true, allowNo: true, description: 'Exclude low-priority chats. Use --no-exclude-low-priority to include all.' }),
@@
-    'include-muted': Flags.boolean({ allowNo: true, description: 'Include muted chats. Use --no-include-muted for a tighter search.' }),
+    'include-muted': Flags.boolean({ default: true, allowNo: true, description: 'Include muted chats. Use --no-include-muted for a tighter search.' }),
🤖 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 `@src/commands/messages/search.ts` around lines 20 - 23, The negatable flags
'exclude-low-priority' and 'include-muted' are missing explicit defaults so they
can be undefined when passed to the API; update their Flags.boolean definitions
(the declarations for 'exclude-low-priority' and 'include-muted') to include
explicit default: true (to match the help text semantics: exclude low-priority
by default and include muted by default) so the values read in run()/where you
build the search params and pass them to the API are always booleans rather than
undefined.
src/lib/manifest.ts-66-66 (1)

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

Fix unarchive description to avoid archive/unarchive ambiguity.

Line 66 currently reuses apiCopy.chats.archive, so unarchive is surfaced with archive wording in help/manifest output.

Suggested patch
-  { command: 'unarchive', description: apiCopy.chats.archive },
+  { command: 'unarchive', description: 'Unarchive a chat' },
🤖 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 `@src/lib/manifest.ts` at line 66, The manifest entry for the 'unarchive'
command is using the wrong description (it references apiCopy.chats.archive),
causing help text to show archive wording for unarchive; update the manifest
object for the 'unarchive' command (the entry with command: 'unarchive') to use
the correct description key (e.g., apiCopy.chats.unarchive) or add a new
unarchive string to apiCopy.chats and reference that so the help/manifest
displays the proper unarchive wording.
🧹 Nitpick comments (3)
src/commands/contacts/search.ts (1)

28-30: ⚡ Quick win

Surface per-account lookup failures when debugging.

The catch block currently suppresses all errors silently, which can hide partial failures. Emit a warning in --debug mode so skipped accounts are visible.

Suggested fix
-      } catch {
+      } catch (error) {
         // Some networks reject exact lookups for some identifiers; keep trying the rest.
+        if (flags.debug) {
+          this.warn(
+            `contacts.search failed for account ${accountID}: ${error instanceof Error ? error.message : String(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 `@src/commands/contacts/search.ts` around lines 28 - 30, The empty catch is
hiding per-account lookup failures; change it to catch (err) and, when the CLI
debug flag is enabled (e.g., options.debug / flags.debug / program.opts().debug
or the module's debug variable used in this command), emit a warning including
the account identifier being queried and the error (err.message or err.stack) so
skipped accounts are visible during debugging; keep the existing behavior of
continuing to try other accounts after logging. Reference the catch block in the
contacts search flow (the per-account lookup inside the search command) and the
surrounding variables for the account id and debug flag to implement this.
src/lib/oauth.ts (1)

110-115: ⚖️ Poor tradeoff

Consider using safer URL opening without shell interpretation.

While the URL is constructed via the URL API (which encodes special characters), spawning a shell command with the URL string could theoretically be vulnerable to command injection in edge cases, especially on Windows. Consider using a library like open (npm package) that handles cross-platform URL opening more 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 `@src/lib/oauth.ts` around lines 110 - 115, The openExternal function currently
builds a platform-specific shell command and calls spawn (spawn(...) in
openExternal), which can allow shell interpretation risks; replace this
implementation to use a cross-platform opener library (e.g., the `open` npm
package) instead of manually spawning shell commands. Import and call the
library (e.g., open(url)) from openExternal and remove the platform branching
and spawn/unref logic so URLs are opened safely across macOS/Windows/Linux
without constructing cmd/open/xdg-open arguments.
src/commands/watch.ts (1)

24-24: ⚡ Quick win

Type assertion bypasses WebSocket API type safety.

The cast as unknown as string[] at line 24 forces an options object with headers into a parameter position that doesn't support it. TypeScript's default DOM WebSocket type (no explicit lib configuration in tsconfig) expects the second parameter to be string | string[] for subprotocols, not an options object. While this works at runtime with Node.js's built-in WebSocket (v21+), the type assertion hides a real type mismatch.

Either configure tsconfig.json to include the correct library types for your Node.js environment, or avoid the type cast by using proper WebSocket initialization patterns.

🤖 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 `@src/commands/watch.ts` at line 24, The code is bypassing TypeScript types by
casting the options object to string[] for the WebSocket constructor; remove the
unsafe cast and use the Node.js/ws types so headers are correctly typed: import
the WebSocket implementation/type from 'ws' (or adjust tsconfig to provide Node
WebSocket libs), then replace the instantiation with new WebSocket(url, {
headers: { Authorization: `Bearer ${token}` } }) using the imported WebSocket
type (referencing WebSocket, url, token) so the headers option is accepted by
the constructor without type assertions.
🤖 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 `@scripts/check-api-copy.mjs`:
- Line 5: The code uses new URL('..', import.meta.url).pathname to build root
which yields incorrect Windows paths and doesn't decode percent-encodings;
replace the .pathname usage with Node's fileURLToPath to convert the
import.meta.url-derived URL to a proper filesystem path (import fileURLToPath
from 'node:url'), then call fileURLToPath(new URL('..', import.meta.url)) and
pass that into resolve to set root (refer to the root variable, resolve call,
and new URL(import.meta.url) in the file).

In `@src/commands/auth/login.ts`:
- Around line 38-48: The OAuth branch returns after loginWithPKCE without
persisting the token; update the code in the oauth branch (the block using
loginWithPKCE and the local token variable) to call the same persistence routine
used for other flows—e.g., invoke finishLogin(token) or call
updateConfig/saveAuth with the token fields (clientID, access token, expires_in,
refresh token, scope, etc.) before logging and returning—so the OAuth
credentials are written to the CLI config/state for subsequent commands.

In `@src/commands/auth/logout.ts`:
- Around line 10-17: The logout flow awaits the remote revoke call (fetch to
URL('/oauth/revoke', config.baseURL)) which can hang and prevent clearing local
auth; change the logic in logout.ts so the revoke request is fire-and-forget or
bounded by a timeout and does not block updateConfig. Specifically, call the
revoke endpoint without awaiting it (or wrap the fetch in a Promise.race with a
short timeout) and ensure updateConfig(current => ({ ...current, auth: undefined
})) always runs (use try/finally or simply trigger revoke asynchronously),
referencing the existing token variable, the fetch(...) call, and updateConfig
to locate and modify the code.

In `@src/commands/chats/search.ts`:
- Line 18: The 'include-muted' flag is inverted because allowNo: true without an
explicit default yields false; update the Flags.boolean call for 'include-muted'
to include default: true (e.g., Flags.boolean({ allowNo: true, default: true,
description: 'Include muted chats. Use --no-include-muted for a tighter search.'
})) so muted chats are included by default while still supporting the
--no-include-muted override; ensure the change is applied to the 'include-muted'
flag definition.

In `@src/commands/config/get.ts`:
- Around line 16-18: The current code prints raw auth data which can leak
credentials; before calling printData, redact sensitive fields in the
config.auth object (e.g., accessToken, refreshToken, secret) by replacing them
with a placeholder like "[REDACTED]" when printing the whole config or the auth
key by default. Locate the usage of readConfig, args.key and printData in this
file and, if args.key is undefined or args.key === 'auth', create a shallow copy
of config (or config.auth) and replace known secret properties with "[REDACTED]"
then pass that sanitized value to printData (leave JSON output behavior intact
or respect flags.json if you want to allow machine-readable full dumps).

In `@src/commands/contacts/list.ts`:
- Around line 32-34: The --ids branch calls printIDs(items) but printIDs expects
id/chatID/messageID while contact rows usually carry userID; update the --ids
handling in the list command to pass the correct identifier by mapping items to
userID when present (e.g. items.map(i => i.userID ?? i.id)) before calling
printIDs, or modify printIDs to prefer userID for contact objects; ensure you
reference the flags.ids check and the printIDs call so returned IDs are contact
userIDs, falling back to existing id/chatID/messageID values if userID is
missing.
- Around line 27-31: The loop currently requests up to flags.limit per account,
allowing the final items array to exceed the global limit; fix by computing
remaining = flags.limit - items.length before each fetch, break early if
remaining <= 0, and pass remaining into collectPage instead of flags.limit when
calling client.accounts.contacts.list(accountID, { query: flags.query }); keep
pushing accountID into each item and break the outer loop once items.length >=
flags.limit.

In `@src/commands/description.ts`:
- Around line 24-27: The command handler currently accepts both args.description
and the --clear flag and silently treats description as cleared; change the
validation to reject this conflicting input by checking if flags.clear and
args.description are both truthy and throwing a clear error (e.g., throw new
Error('Provide either DESCRIPTION or --clear, not both')) before calling
createClient/resolveChatID/client.chats.update/printData so the conflict is
detected early and accidental data loss is prevented.

In `@src/commands/doctor.ts`:
- Line 21: The fetch call that probes the server (the request using new
URL('/v1/info', baseURL)) lacks a timeout and can hang; wrap it with an
AbortController, pass controller.signal into fetch, start a timer (e.g., const
timeoutId = setTimeout(() => controller.abort(), <timeoutMs>)) before calling
fetch, and clear the timer (clearTimeout(timeoutId)) after the response or on
error so the request is aborted on timeout and resources are cleaned up;
reference the fetch call, baseURL, and the AbortController usage when making the
change.

In `@src/commands/messages/index.ts`:
- Around line 27-28: The code computes cursor and direction with inconsistent
precedence (cursor = flags.before ?? flags.after but direction = flags.after ?
'after' : flags.before ? 'before' : undefined), which can pair a before cursor
with an after direction; update the pagination handling in the command (use the
flags object) to enforce mutual exclusion or a unified precedence: either (A)
validate and throw/print an error if both flags.before and flags.after are
provided, or (B) pick a single precedence rule and apply it consistently to both
cursor and direction (e.g., if flags.after takes precedence, set cursor =
flags.after and direction = 'after' when both present). Ensure changes touch the
variables cursor and direction so the built pagination params are always
consistent.

In `@src/commands/rpc.ts`:
- Around line 38-42: The error response currently hardcodes id: null in the
process.stdout.write JSON, breaking request correlation; update the error path
that writes the response (the process.stdout.write({...}) block) to preserve the
incoming request id instead of null—e.g. use the existing request id variable in
scope (such as id or request.id, or req.id) with a fallback to null (id: id ??
(request?.id ?? null)) so error responses carry the original request identifier.

In `@src/commands/start-chat.ts`:
- Around line 30-40: The command currently constructs an empty user object and
calls tryStartChat even when no recipient selector is provided; add a pre-check
before calling tryStartChat (around the construction of user) to validate that
either args.query is set or at least one identity flag (flags.email, flags.name,
flags.id, flags.phone, flags.username) is present, and if none are provided fail
fast by returning an error / printing a clear message and exiting (or throwing)
instead of calling tryStartChat; update the code paths around
userQueryFromInput, the local variable user, and the tryStartChat invocation to
enforce this validation.

In `@src/commands/status.ts`:
- Around line 17-19: The fetch in the status probe (the call creating response
with fetch(new URL('/v1/info', baseURL))) has no timeout and can hang; update it
to use AbortSignal.timeout(timeoutMs) (Node v16.14+), pass the resulting signal
to fetch (e.g., fetch(url, { signal })), and handle the abort by catching the
thrown DOMException/Error and converting it into a clear timeout error before
proceeding with the existing response.ok check; keep references to the same URL
'/v1/info' and baseURL and choose a sensible timeout value (e.g., 5s) as
timeoutMs.

In `@src/lib/argv.ts`:
- Around line 24-27: The tokenizer drops empty quoted arguments because token
emission is gated by truthiness (if (current)) — change emission logic in the
loop that handles whitespace and quote boundaries so that you push current even
when it's an empty string if that empty string was produced by closing a quote.
Concretely: introduce a small flag (e.g., closedQuote) that you set when you
close a quote (when quote is toggled off), and then in both emission sites (the
whitespace handler and the end-of-input flush) replace the truthy check with a
condition that pushes current when current !== '' OR closedQuote is true; reset
closedQuote after pushing. This preserves ""/'' as empty tokens while still
preventing spurious empty tokens from normal consecutive spaces.

In `@src/lib/config.ts`:
- Around line 24-33: The readConfig function currently swallows all errors in
its try/catch and silently falls back to default baseURL which hides malformed
JSON or permission errors; update readConfig to distinguish errors: attempt
readFile/configPath and JSON.parse as before, but if readFile fails with a "file
not found" (ENOENT) return the safe fallback, whereas if JSON.parse throws or
any other I/O error occurs, rethrow or surface the error (or log it and then
throw) so callers can detect malformed config; keep the same returned shape
(baseURL and auth when parse succeeds) and reference the existing symbols
readConfig, configPath(), defaultBaseURL, parsed.auth when making the changes.

In `@src/lib/oauth.ts`:
- Around line 94-108: The token exchange omits redirect_uri and the
exchangeToken signature lacks a redirectURI parameter, which violates RFC 6749;
update the exchangeToken function signature (exchangeToken) to accept a
redirectURI argument and include redirect_uri: redirectURI in the
URLSearchParams body used for the POST so the token request matches the original
authorization request.

In `@src/lib/output.ts`:
- Around line 241-246: In collectPage, the current logic treats limit=0 as "no
limit" and pushes one item before breaking; change the loop to check the
explicit presence of a limit before pushing by using if (limit !== undefined &&
items.length >= limit) break at the top of the for-await loop (use strict !==
undefined rather than truthiness) so collectPage(..., 0) returns an empty array
and any explicit numeric limit is honored; update the function collectPage and
its local items/limit handling accordingly.

In `@src/lib/resolve.ts`:
- Around line 98-103: The retrieveChat function currently swallows all errors
from client.chats.retrieve which hides auth/network/server issues; change its
error handling to only return undefined for a genuine “not found” case and
rethrow (or log and rethrow) other errors: inspect the caught error inside
retrieveChat (from client.chats.retrieve) for a not-found indicator (e.g.,
status/code/message) and return undefined only then, otherwise throw the error
(or call processLogger.error before throwing) so upstream can handle
auth/network/server failures appropriately.

---

Minor comments:
In @.agents/skills/opentui/docs/components/ascii-font.mdx:
- Around line 81-84: The font style name is misspelled as "pallet" in the font
config object; update the value for the font key (the object containing font:
"pallet") to "palette" so the style name reads font: "palette" (ensure you
update any identical occurrences of "pallet" in the same component to maintain
consistency).

In @.agents/skills/opentui/docs/components/select.mdx:
- Around line 79-82: The examples register a handler using
menu.on(SelectRenderableEvents.ITEM_SELECTED, ...) and annotate the callback
parameter with SelectOption but never import that type, causing TypeScript
compile errors; fix by adding an import for SelectOption from "@opentui/core"
alongside the existing SelectRenderableEvents import so the two snippets (the
handler at the menu.on call shown and the similar one on line 91) reference a
defined type and compile correctly.

In @.agents/skills/opentui/docs/components/tab-select.mdx:
- Line 158: Replace the hard-coded event string in the event listener with the
exported enum/constant to keep docs consistent: change the
tabs.on("itemSelected", (index, option) => { ...) usage to use
TabSelectRenderableEvents.ITEM_SELECTED so the listener references the exported
event constant (TabSelectRenderableEvents.ITEM_SELECTED) instead of the string
literal.

In @.agents/skills/opentui/docs/keymap/hosts.mdx:
- Line 55: Update the platform table entry so Apple’s OS uses the correct
branding: replace the literal `macos` string with `macOS` in the table row that
documents the `platform` field (the cell currently showing `macos`, `windows`,
`linux` or `unknown`), preserving the rest of the format and punctuation.

In @.agents/skills/opentui/docs/keymap/react.mdx:
- Around line 115-117: The example is missing imports for React hooks and
OpenTUI helpers; add imports for useMemo (from 'react' or 'preact' depending on
the project), and import reactiveMatcherFromStore and useBindings from the
OpenTUI package so the snippet using useMemo, reactiveMatcherFromStore, and
useBindings is complete and copy-pasteable; update the top of the example to
include these imports and ensure the named symbols (useMemo,
reactiveMatcherFromStore, useBindings) match the existing exported names used
elsewhere in the docs.

In `@scripts/check-api-copy.mjs`:
- Around line 21-22: The checks array in scripts/check-api-copy.mjs is missing
coverage for apiCopy.contacts.list, so add an entry for it (e.g.,
['contacts.list', 'resources/contacts.d.ts', 'list']) to the list of checks so
the script will detect drift for contacts.list; update the checks array near the
existing ['contacts.search', ...] and ['messages.delete', ...] entries to
include that tuple referencing the apiCopy key (contacts.list), the d.ts
resource (resources/contacts.d.ts), and the method name ('list').

In `@src/commands/api/post.ts`:
- Around line 20-21: The code parses flags.body directly which will throw on
malformed JSON; wrap the JSON.parse(flags.body) call in a try/catch (same
pattern used in watch.ts and rpc.ts) and when JSON.parse fails, throw a
CLI-friendly error (e.g., new Error("Invalid JSON for --body: " + err.message))
so the user sees a clear message; ensure you still call client.post(args.path, {
body }) and printData(await client.post(...), flags.json ? 'json' : 'human')
after successful parsing, referencing flags.body, client.post, and printData.

In `@src/commands/config/set.ts`:
- Around line 8-14: When handling the ConfigSet.run() flow, validate args.value
when args.key === 'baseURL' before calling updateConfig to avoid persisting
malformed URLs; in run(), check if args.key === 'baseURL' and attempt to parse
args.value with the URL constructor (or otherwise validate scheme/host), and if
parsing fails call this.error(...) or throw a descriptive error instead of
writing to updateConfig, otherwise proceed with updateConfig(config => ({
...config, [args.key]: args.value })) and this.log.

In `@src/commands/create-chat.ts`:
- Around line 24-30: Before calling client.chats.create, validate the
participant cardinality when flags.type === 'single': check flags.participant
(or the parsed participant array) and if type is 'single' ensure exactly one
participant is provided; if not, throw or print a clear error and exit (or
return) to avoid sending an invalid payload to client.chats.create. Locate the
create call (client.chats.create) and the CLI flags (flags.participant,
flags.type, flags.message, etc.) and add this guard so only a single participant
is allowed for 'single' chats.

In `@src/commands/message-expiry.ts`:
- Line 24: The validation message says "positive integer" but the code permits
0; update the check and message to be consistent: either change the condition in
the message/throw in the message to say "non-negative integer" (so keep the
current condition using expiry !== null && (!Number.isInteger(expiry) || expiry
< 0)) or tighten the validation to forbid 0 by using expiry <= 0 and keep
"positive integer" in the Error; locate the validation around the expiry
variable in the message-expiry command (the if that throws Error('SECONDS must
be a positive integer or "off"')) and make the corresponding change so the
thrown error text matches the numeric check.

In `@src/commands/messages/search.ts`:
- Around line 20-23: The negatable flags 'exclude-low-priority' and
'include-muted' are missing explicit defaults so they can be undefined when
passed to the API; update their Flags.boolean definitions (the declarations for
'exclude-low-priority' and 'include-muted') to include explicit default: true
(to match the help text semantics: exclude low-priority by default and include
muted by default) so the values read in run()/where you build the search params
and pass them to the API are always booleans rather than undefined.

In `@src/commands/send-file.ts`:
- Around line 51-55: When flags.wait is true, the code calls
waitForMessage(client, chatID, result.pendingMessageID, ...) without verifying
result.pendingMessageID; add an explicit guard that checks if
result.pendingMessageID is present before calling waitForMessage and if missing
throw or return a clear error (e.g., log and exit or reject) indicating that
pendingMessageID is required for the --wait path. Update the block around
flags.wait to validate result.pendingMessageID and only call waitForMessage when
it exists, otherwise provide a descriptive error mentioning pendingMessageID and
the --wait flag.

In `@src/commands/unarchive.ts`:
- Line 7: The command's help summary wrongly reuses apiCopy.chats.archive;
update the static override summary in the unarchive command (the static override
summary field) to reference the unarchive-specific copy key/string (e.g.,
apiCopy.chats.unarchive or the appropriate unarchive summary key) so the CLI
help shows an accurate description for the unarchive command.

In `@src/lib/manifest.ts`:
- Line 66: The manifest entry for the 'unarchive' command is using the wrong
description (it references apiCopy.chats.archive), causing help text to show
archive wording for unarchive; update the manifest object for the 'unarchive'
command (the entry with command: 'unarchive') to use the correct description key
(e.g., apiCopy.chats.unarchive) or add a new unarchive string to apiCopy.chats
and reference that so the help/manifest displays the proper unarchive wording.

---

Nitpick comments:
In `@src/commands/contacts/search.ts`:
- Around line 28-30: The empty catch is hiding per-account lookup failures;
change it to catch (err) and, when the CLI debug flag is enabled (e.g.,
options.debug / flags.debug / program.opts().debug or the module's debug
variable used in this command), emit a warning including the account identifier
being queried and the error (err.message or err.stack) so skipped accounts are
visible during debugging; keep the existing behavior of continuing to try other
accounts after logging. Reference the catch block in the contacts search flow
(the per-account lookup inside the search command) and the surrounding variables
for the account id and debug flag to implement this.

In `@src/commands/watch.ts`:
- Line 24: The code is bypassing TypeScript types by casting the options object
to string[] for the WebSocket constructor; remove the unsafe cast and use the
Node.js/ws types so headers are correctly typed: import the WebSocket
implementation/type from 'ws' (or adjust tsconfig to provide Node WebSocket
libs), then replace the instantiation with new WebSocket(url, { headers: {
Authorization: `Bearer ${token}` } }) using the imported WebSocket type
(referencing WebSocket, url, token) so the headers option is accepted by the
constructor without type assertions.

In `@src/lib/oauth.ts`:
- Around line 110-115: The openExternal function currently builds a
platform-specific shell command and calls spawn (spawn(...) in openExternal),
which can allow shell interpretation risks; replace this implementation to use a
cross-platform opener library (e.g., the `open` npm package) instead of manually
spawning shell commands. Import and call the library (e.g., open(url)) from
openExternal and remove the platform branching and spawn/unref logic so URLs are
opened safely across macOS/Windows/Linux without constructing cmd/open/xdg-open
arguments.
🪄 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: 7cda4489-7b12-4994-9f64-9eecfa712ee6

📥 Commits

Reviewing files that changed from the base of the PR and between 50ec338 and 4d2ad48.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (151)
  • .agents/skills/opentui/SKILL.md
  • .agents/skills/opentui/config.ts
  • .agents/skills/opentui/docs/bindings/react.mdx
  • .agents/skills/opentui/docs/bindings/solid.mdx
  • .agents/skills/opentui/docs/components/ascii-font.mdx
  • .agents/skills/opentui/docs/components/box.mdx
  • .agents/skills/opentui/docs/components/code.mdx
  • .agents/skills/opentui/docs/components/diff.mdx
  • .agents/skills/opentui/docs/components/frame-buffer.mdx
  • .agents/skills/opentui/docs/components/input.mdx
  • .agents/skills/opentui/docs/components/line-number.mdx
  • .agents/skills/opentui/docs/components/markdown.mdx
  • .agents/skills/opentui/docs/components/scrollbar.mdx
  • .agents/skills/opentui/docs/components/scrollbox.mdx
  • .agents/skills/opentui/docs/components/select.mdx
  • .agents/skills/opentui/docs/components/slider.mdx
  • .agents/skills/opentui/docs/components/tab-select.mdx
  • .agents/skills/opentui/docs/components/text.mdx
  • .agents/skills/opentui/docs/components/textarea.mdx
  • .agents/skills/opentui/docs/core-concepts/audio.mdx
  • .agents/skills/opentui/docs/core-concepts/colors.mdx
  • .agents/skills/opentui/docs/core-concepts/console.mdx
  • .agents/skills/opentui/docs/core-concepts/constructs.mdx
  • .agents/skills/opentui/docs/core-concepts/keyboard.mdx
  • .agents/skills/opentui/docs/core-concepts/layout.mdx
  • .agents/skills/opentui/docs/core-concepts/lifecycle.mdx
  • .agents/skills/opentui/docs/core-concepts/notifications.mdx
  • .agents/skills/opentui/docs/core-concepts/renderables-vs-constructs.mdx
  • .agents/skills/opentui/docs/core-concepts/renderables.mdx
  • .agents/skills/opentui/docs/core-concepts/renderer.mdx
  • .agents/skills/opentui/docs/getting-started.mdx
  • .agents/skills/opentui/docs/keymap/addons.mdx
  • .agents/skills/opentui/docs/keymap/core.mdx
  • .agents/skills/opentui/docs/keymap/custom-addons.mdx
  • .agents/skills/opentui/docs/keymap/hosts.mdx
  • .agents/skills/opentui/docs/keymap/overview.mdx
  • .agents/skills/opentui/docs/keymap/react.mdx
  • .agents/skills/opentui/docs/keymap/solid.mdx
  • .agents/skills/opentui/docs/plugins/core.mdx
  • .agents/skills/opentui/docs/plugins/react.mdx
  • .agents/skills/opentui/docs/plugins/slots.mdx
  • .agents/skills/opentui/docs/plugins/solid.mdx
  • .agents/skills/opentui/docs/reference/color-matrix.mdx
  • .agents/skills/opentui/docs/reference/env-vars.mdx
  • .agents/skills/opentui/docs/reference/tree-sitter.mdx
  • .github/workflows/ci.yml
  • .github/workflows/publish-release.yml
  • .gitignore
  • .goreleaser.yml
  • README.md
  • TYPESCRIPT_CLI.md
  • bin/check-release-environment
  • bin/dev.js
  • bin/run.js
  • package.json
  • release-please-config.json
  • scripts/build-homebrew-archive.mjs
  • scripts/check-api-copy.mjs
  • scripts/publish-homebrew-formula.mjs
  • skills-lock.json
  • src/commands/accounts.ts
  • src/commands/api/get.ts
  • src/commands/api/post.ts
  • src/commands/app/e2ee/recovery-code/mark-backed-up.ts
  • src/commands/app/e2ee/recovery-code/reset/begin.ts
  • src/commands/app/e2ee/recovery-code/reset/confirm.ts
  • src/commands/app/e2ee/recovery-code/verify.ts
  • src/commands/app/status.ts
  • src/commands/archive.ts
  • src/commands/assets/download.ts
  • src/commands/assets/upload.ts
  • src/commands/auth/login.ts
  • src/commands/auth/logout.ts
  • src/commands/auth/status.ts
  • src/commands/avatar.ts
  • src/commands/chat.ts
  • src/commands/chat/open.ts
  • src/commands/chats/index.ts
  • src/commands/chats/search.ts
  • src/commands/clear-draft.ts
  • src/commands/commands.ts
  • src/commands/config/get.ts
  • src/commands/config/path.ts
  • src/commands/config/reset.ts
  • src/commands/config/set.ts
  • src/commands/contacts/list.ts
  • src/commands/contacts/search.ts
  • src/commands/create-chat.ts
  • src/commands/current-user.ts
  • src/commands/delete-message.ts
  • src/commands/description.ts
  • src/commands/doctor.ts
  • src/commands/draft.ts
  • src/commands/edit.ts
  • src/commands/export.ts
  • src/commands/focus.ts
  • src/commands/inbox.ts
  • src/commands/interactive.ts
  • src/commands/llm.ts
  • src/commands/login.ts
  • src/commands/logout.ts
  • src/commands/low-priority.ts
  • src/commands/mark-read.ts
  • src/commands/mark-unread.ts
  • src/commands/message-expiry.ts
  • src/commands/message.ts
  • src/commands/messages/index.ts
  • src/commands/messages/search.ts
  • src/commands/mute.ts
  • src/commands/notify-anyway.ts
  • src/commands/pin.ts
  • src/commands/react.ts
  • src/commands/read.ts
  • src/commands/remind.ts
  • src/commands/reply-file.ts
  • src/commands/reply.ts
  • src/commands/rpc.ts
  • src/commands/search.ts
  • src/commands/send-file.ts
  • src/commands/send.ts
  • src/commands/shell.ts
  • src/commands/start-chat.ts
  • src/commands/status.ts
  • src/commands/tail.ts
  • src/commands/thread.ts
  • src/commands/threads.ts
  • src/commands/title.ts
  • src/commands/unarchive.ts
  • src/commands/unmute.ts
  • src/commands/unpin.ts
  • src/commands/unreact.ts
  • src/commands/unread.ts
  • src/commands/unremind.ts
  • src/commands/watch.ts
  • src/commands/whoami.ts
  • src/lib/app-api.ts
  • src/lib/argv.ts
  • src/lib/client.ts
  • src/lib/config.ts
  • src/lib/copy.ts
  • src/lib/export/index.ts
  • src/lib/interactive.ts
  • src/lib/manifest.ts
  • src/lib/oauth.ts
  • src/lib/output.ts
  • src/lib/pkce.ts
  • src/lib/resolve.ts
  • src/lib/runner.ts
  • src/lib/wait.ts
  • test/cli-smoke.mjs
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • .goreleaser.yml

Comment thread packages/cli/scripts/check-api-copy.mjs Outdated
Comment thread src/commands/auth/login.ts Outdated
Comment thread src/commands/auth/logout.ts Outdated
Comment thread packages/cli/src/commands/chats/search.ts Outdated
Comment thread src/commands/config/get.ts Outdated
Comment thread packages/cli/src/lib/argv.ts
Comment thread packages/cli/src/lib/config.ts
Comment thread packages/cli/src/lib/oauth.ts Outdated
Comment thread packages/cli/src/lib/output.ts
Comment thread packages/cli/src/lib/resolve.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: 2

🧹 Nitpick comments (1)
src/commands/bridge/login.ts (1)

36-37: ⚡ Quick win

Always emit final human-readable output in guided mode.

In non-JSON mode, guided execution currently skips printBridgeLoginStep(...) entirely in this command. Consider printing the final result for consistent CLI output across modes.

Proposed change
-    if (flags.json) printData(result, 'json')
-    else if (!flags.guided) printBridgeLoginStep(result)
+    if (flags.json) printData(result, 'json')
+    else printBridgeLoginStep(result)
🤖 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 `@src/commands/bridge/login.ts` around lines 36 - 37, The CLI currently skips
emitting the final human-readable result when running in guided mode because the
conditional only calls printBridgeLoginStep(result) when !flags.guided; change
the branching so that if flags.json you call printData(result, 'json'),
otherwise always call printBridgeLoginStep(result) (i.e., remove the
!flags.guided guard), ensuring printBridgeLoginStep(result) runs for both guided
and non-guided non-JSON flows.
🤖 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 `@src/commands/bridge/login/submit-cookies.ts`:
- Line 23: Wrap the JSON.parse(flags.body) call in a try/catch and validate the
resulting value before casting: catch JSON parse errors and emit a clear CLI
error referencing the --body flag (rather than letting it throw), then ensure
the parsed value is an object and every property value is a string (i.e. the
intended Record<string,string> shape); if validation fails emit a descriptive
CLI error and exit/return. Use the existing flags.body variable and the const
body assignment location in submit-cookies to implement this guard and
validation.

In `@src/commands/bridge/login/submit-user-input.ts`:
- Line 23: Wrap the JSON.parse of flags.body in a guarded parse (try/catch) and
return a clear CLI error on parse failure; after parsing, validate that the
result is an object (not null/array) and that each property value is a string
(or coerce/validate accordingly) before proceeding to API submission — update
the parsing around const body = JSON.parse(flags.body) so malformed JSON or
invalid shapes produce actionable error messages (e.g., via the same CLI error
path) instead of throwing.

---

Nitpick comments:
In `@src/commands/bridge/login.ts`:
- Around line 36-37: The CLI currently skips emitting the final human-readable
result when running in guided mode because the conditional only calls
printBridgeLoginStep(result) when !flags.guided; change the branching so that if
flags.json you call printData(result, 'json'), otherwise always call
printBridgeLoginStep(result) (i.e., remove the !flags.guided guard), ensuring
printBridgeLoginStep(result) runs for both guided and non-guided non-JSON flows.
🪄 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: 0b7ad9c4-fe51-481c-8f53-5cab32253393

📥 Commits

Reviewing files that changed from the base of the PR and between 4d2ad48 and d3926c7.

📒 Files selected for processing (19)
  • package.json
  • src/commands/app/e2ee/verification/accept.ts
  • src/commands/app/e2ee/verification/cancel.ts
  • src/commands/app/e2ee/verification/qr/confirm-scanned.ts
  • src/commands/app/e2ee/verification/qr/scan.ts
  • src/commands/app/e2ee/verification/sas/confirm.ts
  • src/commands/app/e2ee/verification/sas/start.ts
  • src/commands/app/e2ee/verification/start.ts
  • src/commands/bridge/flows.ts
  • src/commands/bridge/login.ts
  • src/commands/bridge/login/start.ts
  • src/commands/bridge/login/submit-cookies.ts
  • src/commands/bridge/login/submit-user-input.ts
  • src/commands/bridge/login/wait.ts
  • src/commands/bridge/logins.ts
  • src/commands/bridge/logout.ts
  • src/commands/bridge/whoami.ts
  • src/lib/bridge-login.ts
  • src/lib/manifest.ts
✅ Files skipped from review due to trivial changes (3)
  • src/commands/app/e2ee/verification/cancel.ts
  • src/lib/bridge-login.ts
  • src/commands/app/e2ee/verification/sas/confirm.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/manifest.ts
  • package.json
📜 Review details
🔇 Additional comments (12)
src/commands/app/e2ee/verification/start.ts (1)

1-23: LGTM!

src/commands/app/e2ee/verification/qr/confirm-scanned.ts (1)

1-24: LGTM!

src/commands/app/e2ee/verification/sas/start.ts (1)

1-24: LGTM!

src/commands/app/e2ee/verification/accept.ts (1)

1-24: LGTM!

src/commands/app/e2ee/verification/qr/scan.ts (1)

1-24: LGTM!

src/commands/bridge/logins.ts (1)

1-21: LGTM!

src/commands/bridge/logout.ts (1)

1-22: LGTM!

src/commands/bridge/whoami.ts (1)

1-21: LGTM!

src/commands/bridge/flows.ts (1)

1-21: LGTM!

src/commands/bridge/login/start.ts (1)

1-29: LGTM!

src/commands/bridge/login/wait.ts (1)

1-29: LGTM!

src/commands/bridge/login.ts (1)

1-35: LGTM!

Also applies to: 38-39

Comment thread src/commands/bridge/login/submit-cookies.ts Outdated
Comment thread src/commands/bridge/login/submit-user-input.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: 3

🤖 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 `@src/commands/accounts/add.ts`:
- Line 22: The code currently falls back to loginFlows[0] when the --flow flag
is omitted and the chooser is skipped (e.g., flags.json or flags.guided ===
false); change this so we do not silently pick the first backend flow: when
flags.flow is undefined and the chooser is skipped (check the same
chooserSkipped/flags.json || flags.guided === false condition used near the flow
selection), throw a user-facing error asking the caller to provide --flow rather
than assigning loginFlows[0]. Update the selection logic that references
loginFlows and the flow flag (the flow: Flags.string flag parsing and the code
that sets selectedFlow = flags.flow ?? loginFlows[0]) to validate and require
flags.flow in chooser-skipped paths and only use a default when the interactive
chooser runs.

In `@src/lib/account-login.ts`:
- Around line 101-110: The cookie prompt currently uses promptText and therefore
echoes sensitive cookie values to the terminal; update the 'cookies' branch in
the handler (the block gated by if ('cookies' in step)) to use the
masked/no-echo prompt function instead of promptText (e.g., promptSecret /
promptPassword / promptNoEcho) when collecting body[field.name], preserving the
existing checks that use options.cookies and options.nonInteractive and the same
error message on non-interactive; apply the same replacement for the second
cookie collection occurrence referenced around lines 120-123 so all cookie
inputs are captured without terminal echo.
- Around line 30-58: printAccountLoginStep currently writes human-readable
progress to process.stdout which corrupts machine-readable output; change it to
write to process.stderr (or accept an optional writable stream parameter
defaulting to process.stderr) and use that stream for all writes in
printAccountLoginStep; apply the same change to the other helper functions in
this file that emit interactive output so callers wanting JSON can still use
stdout unmodified.
🪄 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: 78f2f52a-be95-4973-8447-befb9ad53f55

📥 Commits

Reviewing files that changed from the base of the PR and between d3926c7 and 8f585a1.

📒 Files selected for processing (4)
  • package.json
  • src/commands/accounts/add.ts
  • src/lib/account-login.ts
  • src/lib/manifest.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • src/lib/manifest.ts

Comment thread packages/cli/src/commands/accounts/add.ts
Comment thread packages/cli/src/lib/account-login.ts
Comment thread packages/cli/src/lib/account-login.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: 1

🧹 Nitpick comments (2)
test/e2e-staging.mjs (2)

13-14: ⚡ Quick win

Hardcoded developer-specific path reduces test portability.

The default path /Users/batuhan/.codex/worktrees/43bc/beeper/desktop is specific to a particular developer's machine and won't work for other developers or CI environments.

Consider either requiring BEEPER_DESKTOP_ROOT to be set (throwing an error if missing) or using a more generic default like a relative path from the repo root. This makes the test more portable and prevents confusing failures when other developers run it.

🤖 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 `@test/e2e-staging.mjs` around lines 13 - 14, defaultDesktopRoot is set to a
developer-specific absolute path and makes desktopRoot unreliable for other
environments; change the logic around defaultDesktopRoot/desktopRoot so that
either (a) if process.env.BEEPER_DESKTOP_ROOT is not set the script throws a
clear error asking the caller to set BEEPER_DESKTOP_ROOT, or (b) compute a
portable default (e.g., a repo-relative path using __dirname or process.cwd())
and fall back to that; update the code that reads defaultDesktopRoot/desktopRoot
to implement one of these approaches and ensure error messaging or the
relative-path computation mentions BEEPER_DESKTOP_ROOT, defaultDesktopRoot, and
desktopRoot for clarity.

982-982: 💤 Low value

Inconsistent SIGKILL timeout.

Line 982 waits 1000ms between SIGTERM and SIGKILL, while line 858 in runProcess waits 2000ms for the same pattern. Consider using a consistent timeout duration for graceful shutdown attempts.

🤖 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 `@test/e2e-staging.mjs` at line 982, The sleep call waiting 1000ms between
SIGTERM and SIGKILL is inconsistent with the 2000ms used in runProcess; replace
the magic number with a shared constant (e.g., GRACEFUL_SHUTDOWN_TIMEOUT) or
update the sleep(1000) to sleep(2000) so both the standalone wait and the wait
inside runProcess use the same duration; locate the sleep call and the
runProcess implementation and use the same identifier
(GRACEFUL_SHUTDOWN_TIMEOUT) or matching literal to ensure consistent graceful
shutdown timing.
🤖 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 `@src/commands/app/e2ee/recovery-code/reset/begin.ts`:
- Line 10: The current CLI defines a sensitive 'recovery-code' flag using
Flags.string which can expose E2EE recovery codes in shell history and process
listings; change the implementation in begin.ts so that if the 'recovery-code'
flag is omitted the command prompts interactively with hidden input (or reads
from stdin/file) instead of relying on a visible flag, add a note in the command
help/documentation recommending environment variables or secure input methods
for automation, and if you must keep the flag for automation, clearly document
the security risks and consider marking the flag as deprecated or providing an
alternative secure input flow.

---

Nitpick comments:
In `@test/e2e-staging.mjs`:
- Around line 13-14: defaultDesktopRoot is set to a developer-specific absolute
path and makes desktopRoot unreliable for other environments; change the logic
around defaultDesktopRoot/desktopRoot so that either (a) if
process.env.BEEPER_DESKTOP_ROOT is not set the script throws a clear error
asking the caller to set BEEPER_DESKTOP_ROOT, or (b) compute a portable default
(e.g., a repo-relative path using __dirname or process.cwd()) and fall back to
that; update the code that reads defaultDesktopRoot/desktopRoot to implement one
of these approaches and ensure error messaging or the relative-path computation
mentions BEEPER_DESKTOP_ROOT, defaultDesktopRoot, and desktopRoot for clarity.
- Line 982: The sleep call waiting 1000ms between SIGTERM and SIGKILL is
inconsistent with the 2000ms used in runProcess; replace the magic number with a
shared constant (e.g., GRACEFUL_SHUTDOWN_TIMEOUT) or update the sleep(1000) to
sleep(2000) so both the standalone wait and the wait inside runProcess use the
same duration; locate the sleep call and the runProcess implementation and use
the same identifier (GRACEFUL_SHUTDOWN_TIMEOUT) or matching literal to ensure
consistent graceful shutdown timing.
🪄 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: b91035be-31dd-4c91-897b-d7d9626707a2

📥 Commits

Reviewing files that changed from the base of the PR and between 8f585a1 and c81e6f7.

📒 Files selected for processing (8)
  • README.md
  • TYPESCRIPT_CLI.md
  • package.json
  • src/commands/app/e2ee/recovery-code/reset/begin.ts
  • src/commands/app/e2ee/verification/start.ts
  • src/commands/auth/login.ts
  • src/lib/oauth.ts
  • test/e2e-staging.mjs
✅ Files skipped from review due to trivial changes (2)
  • package.json
  • TYPESCRIPT_CLI.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/commands/app/e2ee/verification/start.ts
  • src/lib/oauth.ts
  • src/commands/auth/login.ts
📜 Review details
🧰 Additional context used
🪛 LanguageTool
README.md

[uncategorized] ~251-~251: The official name of this software platform is spelled with a capital “H”.
Context: ...pm and Homebrew. Push a v* tag to run .github/workflows/publish-release.yml. The re...

(GITHUB)

🔇 Additional comments (2)
src/commands/app/e2ee/recovery-code/reset/begin.ts (1)

14-21: LGTM!

test/e2e-staging.mjs (1)

753-785: LGTM!

Comment thread packages/cli/src/commands/app/e2ee/recovery-code/reset/begin.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: 3

🤖 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 @.github/workflows/publish-release.yml:
- Around line 3-5: Remove the unused OIDC permission by deleting the id-token:
write entry from the workflow permissions block so only contents: write remains;
update the permissions mapping (the permissions key and its children) to no
longer include the id-token field, leaving the workflow with just contents:
write for GitHub release operations.

In `@README.md`:
- Line 129: Update the example command line that currently reads beeper remind
"Family" 2026-05-13T12:00:00Z to use a future or placeholder ISO-8601 timestamp
(e.g., 2026-12-01T12:00:00Z or <FUTURE_TIMESTAMP>) so copy/paste users won't hit
an already-passed time; change the literal in the README example accordingly
wherever the remind example appears.
- Around line 210-211: The README's examples show `commands --json` and `llm` as
standalone binaries; update the documentation to show them as subcommands by
prefixing each with the CLI root `beeper` (e.g., `beeper commands --json` and
`beeper llm`) so copy/paste yields correct invocation; locate the lines
referencing `commands --json` and `llm` and replace them with the prefixed forms
and update any surrounding explanatory text to reflect they are subcommands of
`beeper`.
🪄 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: 170a2bb2-2c9d-4c5c-a5fb-8d8753b5e903

📥 Commits

Reviewing files that changed from the base of the PR and between c81e6f7 and 19882b0.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (68)
  • .github/actions/setup-go/action.yml
  • .github/workflows/publish-release.yml
  • README.md
  • cmd/beeper-desktop-cli/main.go
  • go.mod
  • internal/apiform/encoder.go
  • internal/apiform/form.go
  • internal/apiform/form_test.go
  • internal/apiquery/encoder.go
  • internal/apiquery/query.go
  • internal/apiquery/query_test.go
  • internal/autocomplete/autocomplete.go
  • internal/autocomplete/autocomplete_test.go
  • internal/autocomplete/shellscripts/bash_autocomplete.bash
  • internal/autocomplete/shellscripts/fish_autocomplete.fish
  • internal/autocomplete/shellscripts/pwsh_autocomplete.ps1
  • internal/autocomplete/shellscripts/zsh_autocomplete.zsh
  • internal/binaryparam/binary_param.go
  • internal/binaryparam/binary_param_test.go
  • internal/debugmiddleware/debug_middleware.go
  • internal/debugmiddleware/debug_middleware_test.go
  • internal/jsonview/explorer.go
  • internal/jsonview/staticdisplay.go
  • internal/mocktest/mocktest.go
  • internal/requestflag/innerflag.go
  • internal/requestflag/innerflag_test.go
  • internal/requestflag/requestflag.go
  • internal/requestflag/requestflag_test.go
  • package.json
  • pkg/cmd/account.go
  • pkg/cmd/account_test.go
  • pkg/cmd/accountcontact.go
  • pkg/cmd/accountcontact_test.go
  • pkg/cmd/asset.go
  • pkg/cmd/asset_test.go
  • pkg/cmd/beeperdesktopapi.go
  • pkg/cmd/beeperdesktopapi_test.go
  • pkg/cmd/chat.go
  • pkg/cmd/chat_test.go
  • pkg/cmd/chatmessagereaction.go
  • pkg/cmd/chatmessagereaction_test.go
  • pkg/cmd/chatreminder.go
  • pkg/cmd/chatreminder_test.go
  • pkg/cmd/cmd.go
  • pkg/cmd/cmdutil.go
  • pkg/cmd/cmdutil_test.go
  • pkg/cmd/cmdutil_unix.go
  • pkg/cmd/cmdutil_windows.go
  • pkg/cmd/flagoptions.go
  • pkg/cmd/flagoptions_test.go
  • pkg/cmd/info.go
  • pkg/cmd/info_test.go
  • pkg/cmd/message.go
  • pkg/cmd/message_test.go
  • pkg/cmd/suggest.go
  • pkg/cmd/version.go
  • release-please-config.json
  • scripts/bootstrap
  • scripts/build
  • scripts/build-homebrew-archive.mjs
  • scripts/link
  • scripts/lint
  • scripts/publish-homebrew-formula.mjs
  • scripts/run
  • scripts/test
  • scripts/unlink
  • scripts/utils/upload-artifact.sh
  • src/commands/llm.ts
💤 Files with no reviewable changes (41)
  • internal/autocomplete/shellscripts/bash_autocomplete.bash
  • pkg/cmd/chatmessagereaction.go
  • internal/apiform/encoder.go
  • .github/actions/setup-go/action.yml
  • pkg/cmd/asset_test.go
  • pkg/cmd/account_test.go
  • pkg/cmd/chat_test.go
  • pkg/cmd/beeperdesktopapi.go
  • pkg/cmd/chatreminder_test.go
  • internal/autocomplete/shellscripts/zsh_autocomplete.zsh
  • go.mod
  • pkg/cmd/accountcontact_test.go
  • pkg/cmd/chat.go
  • pkg/cmd/chatmessagereaction_test.go
  • pkg/cmd/accountcontact.go
  • internal/jsonview/staticdisplay.go
  • pkg/cmd/beeperdesktopapi_test.go
  • internal/autocomplete/shellscripts/fish_autocomplete.fish
  • pkg/cmd/account.go
  • internal/debugmiddleware/debug_middleware_test.go
  • internal/apiform/form.go
  • internal/autocomplete/autocomplete.go
  • internal/apiquery/query.go
  • internal/binaryparam/binary_param_test.go
  • internal/requestflag/innerflag.go
  • pkg/cmd/cmd.go
  • internal/debugmiddleware/debug_middleware.go
  • internal/binaryparam/binary_param.go
  • pkg/cmd/chatreminder.go
  • internal/apiquery/encoder.go
  • internal/jsonview/explorer.go
  • pkg/cmd/asset.go
  • internal/requestflag/innerflag_test.go
  • internal/autocomplete/shellscripts/pwsh_autocomplete.ps1
  • cmd/beeper-desktop-cli/main.go
  • internal/requestflag/requestflag_test.go
  • internal/requestflag/requestflag.go
  • internal/apiquery/query_test.go
  • internal/autocomplete/autocomplete_test.go
  • internal/mocktest/mocktest.go
  • internal/apiform/form_test.go
📜 Review details
🧰 Additional context used
🪛 LanguageTool
README.md

[uncategorized] ~248-~248: The official name of this software platform is spelled with a capital “H”.
Context: ...GitHub Releases. Push a v* tag to run .github/workflows/publish-release.yml. The re...

(GITHUB)

🔇 Additional comments (3)
package.json (1)

1-95: LGTM!

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

15-47: LGTM!

README.md (1)

1-128: LGTM!

Also applies to: 130-209, 212-260

Comment thread .github/workflows/publish-release.yml Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
batuhan added 5 commits May 15, 2026 13:52
Multiple refactors and improvements across the CLI:

- commands/current-user: use appRequest helper instead of manual fetch and token handling.
- lib/client: prefer BEEPER_ACCESS_TOKEN env or config auth token before falling back to desktop auth; requireToken reads env/config first.
- lib/desktop-auth: prefer probing the explicit baseURL first and concurrently probe remaining candidates via Promise.any for faster discovery.
- lib/export: major rewrite to stream message and attachment data to disk using file handles, append to partial files, emit an attachments manifest as JSONL, and avoid accumulating large in-memory arrays; optimize chat collection for provided chat IDs with Promise.all; change downloadMessageAttachments to return counts and accept a write-entry callback; remove appendJSONL helper and add a MIME extension map helper.
- lib/output: lazy-load Ink renderers to reduce startup cost, centralize JSON/JSONL writing, and make startStream async; rendering functions now dynamically import from ink/render.
- lib/ink/render and lib/ui: small cleanup (removed comment and kept exports).

Purpose: reduce memory pressure for large exports, speed up desktop discovery, support environment-provided tokens, and defer heavy Ink module loads to improve CLI startup performance.
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

♻️ Duplicate comments (1)
README.md (1)

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

Prefix CLI subcommands with beeper for copy/paste correctness.

Line 100 and Line 101 currently read like standalone binaries; they should be documented as beeper subcommands.

Suggested fix
-`commands --json` prints a compact command manifest for tools and agents.
-`llm` prints a concise human-readable command guide.
+`beeper commands --json` prints a compact command manifest for tools and agents.
+`beeper llm` prints a concise human-readable command guide.
🤖 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 `@README.md` around lines 100 - 101, Update the README lines that describe the
CLI so the two commands are shown as beeper subcommands; change the text
referencing `commands --json` to `beeper commands --json` and the text
referencing `llm` to `beeper llm` so examples are copy/paste accurate and
clearly indicate these are subcommands of the `beeper` binary.
🤖 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 `@README.md`:
- Around line 674-680: Several table cells contain unescaped pipe characters
that break Markdown table parsing (MD056); update each option cell value to
escape the internal '|' characters (e.g., in the entries for
`--inbox=<primary|low-priority|archive>`, `--scope=<titles|participants>`, and
`--type=<single|group|any>`) so the pipes are rendered literally (use `\|`) and
ensure the same change is applied to the other occurrences referenced in the
comment (the cells at the other listed locations).

---

Duplicate comments:
In `@README.md`:
- Around line 100-101: Update the README lines that describe the CLI so the two
commands are shown as beeper subcommands; change the text referencing `commands
--json` to `beeper commands --json` and the text referencing `llm` to `beeper
llm` so examples are copy/paste accurate and clearly indicate these are
subcommands of the `beeper` binary.
🪄 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: a5cb5fdc-7740-4e22-9396-b6fd19069ad0

📥 Commits

Reviewing files that changed from the base of the PR and between c81e6f7 and 221f03d.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (153)
  • .github/actions/setup-go/action.yml
  • .github/workflows/publish-release.yml
  • README.md
  • cmd/beeper-desktop-cli/main.go
  • go.mod
  • internal/apiform/encoder.go
  • internal/apiform/form.go
  • internal/apiform/form_test.go
  • internal/apiquery/encoder.go
  • internal/apiquery/query.go
  • internal/apiquery/query_test.go
  • internal/autocomplete/autocomplete.go
  • internal/autocomplete/autocomplete_test.go
  • internal/autocomplete/shellscripts/bash_autocomplete.bash
  • internal/autocomplete/shellscripts/fish_autocomplete.fish
  • internal/autocomplete/shellscripts/pwsh_autocomplete.ps1
  • internal/autocomplete/shellscripts/zsh_autocomplete.zsh
  • internal/binaryparam/binary_param.go
  • internal/binaryparam/binary_param_test.go
  • internal/debugmiddleware/debug_middleware.go
  • internal/debugmiddleware/debug_middleware_test.go
  • internal/jsonview/explorer.go
  • internal/jsonview/staticdisplay.go
  • internal/mocktest/mocktest.go
  • internal/requestflag/innerflag.go
  • internal/requestflag/innerflag_test.go
  • internal/requestflag/requestflag.go
  • internal/requestflag/requestflag_test.go
  • package.json
  • pkg/cmd/account.go
  • pkg/cmd/account_test.go
  • pkg/cmd/accountcontact.go
  • pkg/cmd/accountcontact_test.go
  • pkg/cmd/asset.go
  • pkg/cmd/asset_test.go
  • pkg/cmd/beeperdesktopapi.go
  • pkg/cmd/beeperdesktopapi_test.go
  • pkg/cmd/chat.go
  • pkg/cmd/chat_test.go
  • pkg/cmd/chatmessagereaction.go
  • pkg/cmd/chatmessagereaction_test.go
  • pkg/cmd/chatreminder.go
  • pkg/cmd/chatreminder_test.go
  • pkg/cmd/cmd.go
  • pkg/cmd/cmdutil.go
  • pkg/cmd/cmdutil_test.go
  • pkg/cmd/cmdutil_unix.go
  • pkg/cmd/cmdutil_windows.go
  • pkg/cmd/flagoptions.go
  • pkg/cmd/flagoptions_test.go
  • pkg/cmd/info.go
  • pkg/cmd/info_test.go
  • pkg/cmd/message.go
  • pkg/cmd/message_test.go
  • pkg/cmd/suggest.go
  • pkg/cmd/version.go
  • release-please-config.json
  • scripts/bootstrap
  • scripts/build
  • scripts/build-homebrew-archive.mjs
  • scripts/generate-readme.mjs
  • scripts/link
  • scripts/lint
  • scripts/publish-homebrew-formula.mjs
  • scripts/run
  • scripts/test
  • scripts/unlink
  • scripts/utils/upload-artifact.sh
  • src/commands/accounts.ts
  • src/commands/accounts/add.ts
  • src/commands/api/get.ts
  • src/commands/api/post.ts
  • src/commands/app/e2ee/recovery-code/mark-backed-up.ts
  • src/commands/app/e2ee/recovery-code/reset/begin.ts
  • src/commands/app/e2ee/recovery-code/reset/confirm.ts
  • src/commands/app/e2ee/recovery-code/verify.ts
  • src/commands/app/e2ee/verification/accept.ts
  • src/commands/app/e2ee/verification/cancel.ts
  • src/commands/app/e2ee/verification/qr/confirm-scanned.ts
  • src/commands/app/e2ee/verification/qr/scan.ts
  • src/commands/app/e2ee/verification/sas/confirm.ts
  • src/commands/app/e2ee/verification/sas/start.ts
  • src/commands/app/e2ee/verification/start.ts
  • src/commands/app/status.ts
  • src/commands/archive.ts
  • src/commands/assets/download.ts
  • src/commands/assets/upload.ts
  • src/commands/auth/login.tsx
  • src/commands/auth/logout.ts
  • src/commands/auth/status.ts
  • src/commands/avatar.ts
  • src/commands/chat.ts
  • src/commands/chats/index.ts
  • src/commands/chats/search.ts
  • src/commands/clear-draft.ts
  • src/commands/commands.ts
  • src/commands/config/get.ts
  • src/commands/config/path.ts
  • src/commands/config/reset.ts
  • src/commands/config/set.ts
  • src/commands/contacts/list.ts
  • src/commands/contacts/search.ts
  • src/commands/create-chat.ts
  • src/commands/current-user.ts
  • src/commands/delete-message.ts
  • src/commands/description.ts
  • src/commands/doctor.ts
  • src/commands/draft.ts
  • src/commands/edit.ts
  • src/commands/focus.ts
  • src/commands/inbox.ts
  • src/commands/llm.ts
  • src/commands/low-priority.ts
  • src/commands/message-expiry.ts
  • src/commands/message.ts
  • src/commands/messages/index.ts
  • src/commands/messages/search.ts
  • src/commands/mute.ts
  • src/commands/notify-anyway.ts
  • src/commands/pin.ts
  • src/commands/react.ts
  • src/commands/read.ts
  • src/commands/remind.ts
  • src/commands/reply-file.ts
  • src/commands/reply.ts
  • src/commands/search.ts
  • src/commands/send-file.ts
  • src/commands/send.ts
  • src/commands/start-chat.ts
  • src/commands/status.ts
  • src/commands/title.ts
  • src/commands/unarchive.ts
  • src/commands/unmute.ts
  • src/commands/unpin.ts
  • src/commands/unreact.ts
  • src/commands/unread.ts
  • src/commands/unremind.ts
  • src/commands/watch.ts
  • src/lib/client.ts
  • src/lib/config.ts
  • src/lib/desktop-auth.ts
  • src/lib/export/index.ts
  • src/lib/ink/components.tsx
  • src/lib/ink/format.ts
  • src/lib/ink/render.tsx
  • src/lib/ink/spinner.tsx
  • src/lib/ink/theme.ts
  • src/lib/manifest.ts
  • src/lib/output.ts
  • src/lib/ui.ts
  • test/cli-smoke.mjs
  • test/e2e-staging.mjs
  • tsconfig.json
💤 Files with no reviewable changes (28)
  • internal/binaryparam/binary_param.go
  • internal/apiform/form_test.go
  • internal/apiquery/query.go
  • internal/autocomplete/autocomplete_test.go
  • internal/autocomplete/shellscripts/bash_autocomplete.bash
  • pkg/cmd/account_test.go
  • internal/mocktest/mocktest.go
  • internal/requestflag/innerflag.go
  • go.mod
  • internal/debugmiddleware/debug_middleware_test.go
  • internal/autocomplete/shellscripts/pwsh_autocomplete.ps1
  • cmd/beeper-desktop-cli/main.go
  • internal/requestflag/innerflag_test.go
  • internal/autocomplete/shellscripts/fish_autocomplete.fish
  • internal/autocomplete/shellscripts/zsh_autocomplete.zsh
  • internal/jsonview/staticdisplay.go
  • .github/actions/setup-go/action.yml
  • internal/apiquery/query_test.go
  • internal/requestflag/requestflag.go
  • internal/requestflag/requestflag_test.go
  • internal/jsonview/explorer.go
  • pkg/cmd/account.go
  • internal/apiform/form.go
  • internal/apiform/encoder.go
  • internal/autocomplete/autocomplete.go
  • internal/apiquery/encoder.go
  • internal/binaryparam/binary_param_test.go
  • internal/debugmiddleware/debug_middleware.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/publish-release.yml
📜 Review details
🧰 Additional context used
🪛 LanguageTool
README.md

[uncategorized] ~1781-~1781: The official name of this software platform is spelled with a capital “H”.
Context: ...GitHub Releases. Push a v* tag to run .github/workflows/publish-release.yml. The re...

(GITHUB)

🪛 markdownlint-cli2 (0.22.1)
README.md

[warning] 674-674: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 679-679: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 680-680: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 823-823: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 1223-1223: Table column count
Expected: 3; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 1230-1230: Table column count
Expected: 3; Actual: 7; Too many cells, extra data will be missing

(MD056, table-column-count)

🔇 Additional comments (1)
package.json (1)

1-100: LGTM!

Comment thread packages/cli/README.md Outdated
batuhan added 2 commits May 16, 2026 04:37
Promote login/logout and send subcommands to canonical commands, remove compatibility aliases, and add shared base command flags for JSON, events, and read-only handling. Regenerate README and update smoke/e2e coverage for the new command names.
@batuhan
Copy link
Copy Markdown
Member Author

batuhan commented May 16, 2026

@indent review

@indent
Copy link
Copy Markdown

indent Bot commented May 16, 2026

PR Summary

Reworks the Beeper Desktop CLI from Go to TypeScript on top of oclif and the official @beeper/desktop-api SDK, restructures the repo into a pnpm workspace under packages/cli/, and switches distribution from goreleaser binaries to a Homebrew tap pipeline. Output adopts Ink for human rendering with --json/--jsonl/--events/--read-only modes shared via a BeeperCommand base, while the most recent commits add a streaming/resumable export, lazy Ink loading in lib/output.ts, and a simplified command surface (canonical login/logout/send text|file).

  • Replaces all Go code (cmd/, pkg/cmd/, internal/*, goreleaser config) with packages/cli/src/{commands,lib} and adds an opentui docs/skill bundle under .agents/.
  • Adds OAuth2 PKCE login with a local 127.0.0.1 callback (lib/oauth.ts, lib/pkce.ts) plus a port-scanning Desktop discovery probe (lib/desktop-auth.ts) and optional --app-login email-code flow.
  • Introduces a streaming export pipeline (lib/export/index.ts) with per-chat checkpoint state, partial JSONL spool, attachments manifest, and Markdown/HTML transcripts.
  • Promotes shared base flags (base-url, debug, events, json, read-only), adds ensureWritable, and centralizes JSON/JSONL/Ink output in lib/output.ts with lazily-loaded renderers.
  • Adds shell/rpc (which re-spawn the CLI via runCli), watch (WebSocket stream), doctor, commands, llm, auth status, app status, and config get/set/path/reset.
  • Reworks CI/release: new .github/workflows/ci.yml and publish-release.yml plus scripts/build-homebrew-archive.mjs, scripts/publish-homebrew-formula.mjs, scripts/check-api-copy.mjs, and scripts/generate-readme.mjs.

Issues

2 potential issues found:

  • Release workflow's "Publish Homebrew formula" step still uses the wrong path: it runs node scripts/publish-homebrew-formula.mjs from the repo root, but the script lives at packages/cli/scripts/publish-homebrew-formula.mjs (no scripts/ directory exists at the root after the monorepo move), so the step will fail with Cannot find module. → Autofix
  • pnpm typecheck fails on client.bridges and client.matrix references in commands/accounts/add.ts and lib/account-login.ts (TS2339: Property does not exist on type BeeperDesktop); this blocks pack:homebrew (which runs pnpm build first) so CI's Typecheck/Test/Package steps will all fail until the SDK is upgraded or the calls are routed through client.get/post paths. → Autofix
12 issues already resolved
  • Nit: packages/cli/src/commands/accounts.ts imports cliCopy but never references it; remove the unused import. (fixed by commit cc2281d)
  • List commands fire-and-forget printList in human mode: chats, chats search, messages, and messages search call printList(items, ...) without await, so the Ink render races the process exit and human-mode output is intermittently truncated or missing while JSON mode (which writes synchronously) hides the bug. (fixed by commit bf5f1db)
  • CI/release workflows invoke pnpm-based scripts without installing pnpm: root package.json defines typecheck/test/build as pnpm -r ..., but neither workflow runs pnpm/action-setup or corepack enable, so even after fixing the lockfile issue npm run typecheck and npm test would fail with pnpm: command not found. (fixed by commit 6065dc5)
  • Latent: lib/resolve.ts calls client.chats.retrieve(input, { maxParticipants: 0 }) but the SDK option is maxParticipantCount (used in commands/chat.ts and lib/export/index.ts); the unknown key is silently dropped, so the chat-selector fast path fetches full participant lists on every lookup and will break outright if the SDK starts validating unknown params. (fixed by commit 5a3bf3d)
  • Latent: client.accounts.list() is handled as both a raw array and {items: [...]} across the codebase \u2014 commands/accounts.ts defensively unwraps both, but lib/resolve.ts and lib/export/index.ts assume a raw array; if the SDK ever returns the wrapped shape, resolution silently matches nothing and accounts.json gets written as {"items":[...]} with accounts.length === undefined. (fixed by commit 5a3bf3d)
  • CI workflow cannot install dependencies: .github/workflows/ci.yml runs npm ci (and uses cache: npm) but the repo only ships pnpm-lock.yaml, so every push/PR fails at the dependency step before typecheck/tests/smoke/README/api-copy checks ever run. (fixed by commit 6065dc5)
  • beeper watch never sends its Authorization header: the WebSocket is created with new WebSocket(url, { headers: { Authorization: ... } } as unknown as string[]), but Node's global WebSocket follows the WHATWG signature (url, protocols?) and ignores the headers object (the cast just silences TypeScript), so the handshake either drops the bearer entirely or is sent the headers object as a subprotocol list. (fixed by commit fe4a861)
  • Latent: in exportChatMessages a message is appended to messages.partial.jsonl before its attachments are downloaded, and attachmentCount is only flushed at the end of each page; on a crash mid-message the resumed run treats the message as seen and never re-fetches its attachments, leaving manifest counts and on-disk files inconsistent. (fixed by commit 5a3bf3d)
  • Homebrew packager requires a package-lock.json that doesn't exist: scripts/build-homebrew-archive.mjs hard-codes packageLockPath = join(root, 'package-lock.json') and runs npm ci inside the staged libexec, but the repo only ships pnpm-lock.yaml, so the cp errors with ENOENT before npm ci even starts. (fixed by commit 60c4e8b)
  • npm run pack:homebrew is not defined at the workspace root: both workflows call it from the repo root, but the script only exists in packages/cli/package.json, so the pack/release step fails with Missing script: pack:homebrew. (fixed by commit 6065dc5)
  • Release workflow uploads from the wrong path: gh release upload "${tag}" dist/release/*.tar.gz dist/release/homebrew.json runs from the repo root, but scripts/build-homebrew-archive.mjs writes to packages/cli/dist/release/, so the glob matches nothing and no asset is ever attached to the release. (fixed by commit 6065dc5)
  • Nit: in contacts/list.ts, the ?? [args.account] fallback after resolveAccountIDs(...) is dead code \u2014 with a non-empty input the helper either returns a non-empty array or throws. (fixed by commit 88f2b3e)

CI Checks

Both test jobs still fail at actions/setup-node@v6 with Dependencies lock file is not found... Supported file patterns: package-lock.json,npm-shrinkwrap.json,yarn.lock. Same root cause as the prior run and as the critical CI issue already reported above — the workflow uses cache: npm + npm ci, but the repo only ships pnpm-lock.yaml. The latest commit (ca16e32 "Add CLI plugin SDK and plugin support") did not touch .github/workflows/, so the install step continues to fail before anything else runs.

Failing test (run 25952380577)→ Autofix
  • setup-node with cache: npm errored with 'Dependencies lock file is not found' — the repo only has pnpm-lock.yaml. Same root cause as the critical CI issue already reported on this PR.
Failing test (run 25952379405)→ Autofix
  • Identical failure: setup-node cannot find a package-lock.json to hash for cache: npm. Resolved once the workflow migrates to pnpm.

⚡ Autofix All

@batuhan
Copy link
Copy Markdown
Member Author

batuhan commented May 16, 2026

@coderabbitai Review-body items handled:

  • Fixed the OpenTUI ascii-font Palette typo in 23cfa16.

  • The bridge/login guided final-output concern is stale in this checkout: the referenced command path is not present.

  • The test/e2e-staging.mjs portability concern is stale in this checkout: the referenced test file is not present.

  • The duplicate README command-prefix concern is covered by ac1e610 for the generated CLI README; the root README.md referenced by the thread is not present in this checkout.

  • Batuhan's agent

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Tip

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

@batuhan Thanks for the detailed update! To summarize:

  • OpenTUI ascii-font Palette typo — confirmed fixed in 23cfa16.
  • Bridge/login guided final-output concern — acknowledged as stale; the referenced command path is no longer present in this checkout.
  • test/e2e-staging.mjs portability concern — acknowledged as stale; the file is not present in this checkout.
  • Duplicate README command-prefix concern — covered by ac1e610 for the generated CLI README; the root README.md thread is moot as that file isn't in this checkout.

Would you like me to resolve the corresponding open review comments for any of these items?

Delete the vendored OpenTUI skill and documentation from .agents/skills so this repository no longer carries that agent-specific reference material.
@indent
Copy link
Copy Markdown

indent Bot commented May 16, 2026

Review posted to the banner above. Headline findings:

Critical \u2014 release/CI pipeline is non-functional as-shipped:

  1. .github/workflows/ci.yml runs npm ci / cache: npm, but the repo only ships pnpm-lock.yaml (the root declares packageManager: pnpm@10.25.0). Every CI run fails at the install step before tests, smoke, README check, or api-copy check execute.
  2. Root scripts are pnpm -r ..., but neither workflow installs pnpm (no pnpm/action-setup / corepack enable), so even after fixing the lockfile, npm run typecheck / npm test would fail with pnpm: command not found.
  3. npm run pack:homebrew doesn't exist at the workspace root \u2014 it's only declared in packages/cli/package.json.
  4. publish-release.yml uploads dist/release/*.tar.gz from the repo root, but scripts/build-homebrew-archive.mjs writes to packages/cli/dist/release/, so gh release upload never finds the archive.
  5. scripts/build-homebrew-archive.mjs hard-codes package-lock.json and runs npm ci inside libexec, but the repo doesn't have a package-lock.json anywhere.

Functional:
6. beeper watch casts { headers: { Authorization: ... } } to string[] and passes it as the WebSocket protocols arg. Node's global WebSocket follows the WHATWG spec (no headers in constructor) and there's no ws dependency \u2014 the bearer token is never sent, so the command can't connect to any authed server.
7. chats, chats search, messages, messages search call printList(...) without await in human mode, so the Ink render races process exit. accounts / contacts list / contacts search already await.

Latent:
8. lib/resolve.ts uses maxParticipants while commands/chat.ts and lib/export/index.ts use the real SDK key maxParticipantCount \u2014 the chat-selector fast path silently fetches the full participant list and breaks the day the SDK validates unknown keys.
9. accounts.list() shape (array vs {items}) is handled defensively in one place but assumed to be a raw array everywhere else \u2014 a future SDK change would break resolution and export's accounts.json.
10. exportChatMessages writes a message to messages.partial.jsonl before downloading its attachments and only flushes attachmentCount per page, so a mid-message crash leaves orphan files and an undercounted manifest after resume.

Nits called out inline (unused import in accounts.ts, dead fallback in contacts/list.ts).

Tag me with @indent fix <issue> and I'll push commits.

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/publish-release.yml
Comment thread packages/cli/scripts/build-homebrew-archive.mjs
Comment thread packages/cli/src/commands/watch.ts
Comment thread packages/cli/src/commands/chats/index.ts Outdated
Comment thread packages/cli/src/lib/resolve.ts Outdated
Comment thread packages/cli/src/lib/export/index.ts
Comment thread packages/cli/src/lib/export/index.ts
Comment thread packages/cli/src/commands/accounts.ts
Comment thread packages/cli/src/commands/contacts/list.ts
batuhan added 8 commits May 16, 2026 06:09
Introduce a stable plugin SDK entrypoint for third-party oclif plugins by adding src/plugin-sdk.ts and exporting it from package.json. Document plugin usage and author guidance in the CLI README, and enable plugin management by adding @oclif/plugin-plugins to the oclif plugins list and dependencies.
@batuhan
Copy link
Copy Markdown
Member Author

batuhan commented May 16, 2026

@indent Addressed the headline findings in the pushed commits: workflows now use pnpm/Corepack and correct Homebrew package/upload paths (6065dc5), archive packaging uses pnpm instead of package-lock/npm ci (60c4e8b), watch uses ws with Authorization headers (fe4a861), human list renderers are awaited (bf5f1db), resolve/export normalize account list shapes and use maxParticipantCount while making attachment resume safer (5a3bf3d), and the two nits are fixed (cc2281d, 88f2b3e). Validation was attempted; typecheck/build still fail on pre-existing Desktop SDK typing errors for bridges/matrix in accounts add/account-login.\n\n- Batuhan's agent

@indent
Copy link
Copy Markdown

indent Bot commented May 16, 2026

Verified all 11 fixes against the diffs in 6065dc5, 60c4e8b, fe4a861, bf5f1db, 5a3bf3d, cc2281d, 88f2b3e and resolved each thread.

Two follow-ups worth pulling into this PR before the release pipeline goes green:

  1. Critical \u2014 publish-release.yml formula step still uses the wrong path. The upload step was correctly moved to packages/cli/dist/release/..., but the next step still runs node scripts/publish-homebrew-formula.mjs from the repo root. There's no scripts/ directory at the root after the monorepo move (it lives at packages/cli/scripts/publish-homebrew-formula.mjs), so the formula publish will fail with Cannot find module even after the assets upload succeeds. Fix:

    - name: Publish Homebrew formula
      ...
      run: node packages/cli/scripts/publish-homebrew-formula.mjs
  2. Functional \u2014 the bridges / matrix SDK typecheck failures you flagged are real CI blockers. pnpm pack:homebrew runs pnpm build first, so the TS2339 errors on client.bridges (in commands/accounts/add.ts) and client.matrix (in lib/account-login.ts and commands/accounts/add.ts) will fail the Typecheck, Test, and Package Homebrew archive steps in CI. Either bump @beeper/desktop-api to a version that exports bridges / matrix.bridges.auth, or temporarily route those calls through client.get / client.post so the typed surface isn't required.

Tag me with @indent fix <issue> and I'll push patches for either.

MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} No newline at end of file
run: node scripts/publish-homebrew-formula.mjs
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same scripts/ path bug as the upload step had — only this one wasn't fixed. After the monorepo move there's no scripts/ at the repo root (ls scripts/ returns No such file or directory); the publish script lives at packages/cli/scripts/publish-homebrew-formula.mjs. node scripts/publish-homebrew-formula.mjs from the repo root will fail with Cannot find module, so the formula publish step never runs even after the release assets upload succeeds. Fix with node packages/cli/scripts/publish-homebrew-formula.mjs or set working-directory: packages/cli for this step.

Suggested change
run: node scripts/publish-homebrew-formula.mjs
run: node packages/cli/scripts/publish-homebrew-formula.mjs

const client = await createClient(flags)

if (!args.account) {
const bridges = await client.bridges.list()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Typecheck regression you flagged. As you noted in the PR comments, pnpm build (and therefore pnpm pack:homebrew) fails with TS2339 on client.bridges (lines 33, 43) and client.matrix (lines 52, 64), and the same client.matrix errors hit lib/account-login.ts (lines 73, 98, 113). The pinned @beeper/desktop-api@^5.0.0 doesn't expose these resources, so CI's Typecheck → Test → Package Homebrew archive steps will all fail even after the pnpm migration. Bump the SDK to a version that exports bridges and matrix.bridges.auth, or fall back to client.get/client.post calls until it's published.

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

Note

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

🟡 Minor comments (9)
packages/cli/src/commands/api/post.ts-20-21 (1)

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

Handle invalid --body JSON with a CLI-friendly error.

On Line 20, malformed JSON throws a raw parse error. Wrap parsing so users get a clear actionable message instead of an uncaught-style failure.

Proposed fix
+import { Args, Flags, Errors } from '`@oclif/core`'
 import { BeeperCommand, ensureWritable } from '../../lib/command.js'
 import { createClient } from '../../lib/client.js'
 import { printData } from '../../lib/output.js'
@@
   async run(): Promise<void> {
     const { args, flags } = await this.parse(ApiPost)
     ensureWritable(flags)
     const client = await createClient(flags)
-    const body = JSON.parse(flags.body) as Record<string, unknown>
+    let body: Record<string, unknown>
+    try {
+      body = JSON.parse(flags.body) as Record<string, unknown>
+    } catch {
+      throw new Errors.CLIError('Invalid JSON for --body')
+    }
     await printData(await client.post(args.path, { body }), 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/api/post.ts` around lines 20 - 21, Wrap the
JSON.parse(flags.body) call in a try/catch to detect malformed JSON and produce
a CLI-friendly error: catch the parse error thrown by JSON.parse(flags.body),
log a clear message like "Invalid JSON in --body: <parse error message>" (or
throw a CLI error), and exit with a non-zero status instead of letting the raw
exception bubble up; then only call client.post(args.path, { body }) and
printData(...) when parsing succeeds.
packages/cli/src/commands/avatar.ts-23-27 (1)

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

Reject conflicting PATH and --clear together.

Line 23 validates missing input, but not mutually exclusive input. Passing both currently clears the avatar and ignores PATH, which is easy to do by mistake.

Suggested fix
-    if (!flags.clear && !args.path) throw new Error('Provide PATH or pass --clear')
+    if (flags.clear && args.path) throw new Error('Pass PATH or --clear, not both')
+    if (!flags.clear && !args.path) throw new Error('Provide PATH or pass --clear')
🤖 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/avatar.ts` around lines 23 - 27, Add a
mutual-exclusion check so callers cannot pass both PATH and --clear: in the
avatar command handler (where flags.clear and args.path are currently checked
before calling createClient, resolveChatID, client.chats.update, and printData)
validate that exactly one of flags.clear or args.path is provided; if both are
present throw a clear error (e.g., "Cannot provide both PATH and --clear") and
if neither is present keep the existing "Provide PATH or pass --clear" behavior.
packages/cli/src/lib/wait.ts-14-20 (1)

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

Timeout can exceed requested timeoutMs by one poll interval.

After a failed attempt, the unconditional sleep can push total wait beyond the configured timeout. Bound sleep to remaining time before timeout.

Proposed fix
   while (Date.now() - started < timeoutMs) {
     try {
       return await client.messages.retrieve(pendingMessageID, { chatID })
     } catch (error) {
       lastError = error
-      await sleep(intervalMs)
+      const elapsedMs = Date.now() - started
+      const remainingMs = timeoutMs - elapsedMs
+      if (remainingMs <= 0) break
+      await sleep(Math.min(intervalMs, remainingMs))
     }
   }
🤖 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/wait.ts` around lines 14 - 20, The loop in wait.ts can
exceed timeoutMs because it always sleeps intervalMs after a failed retrieve;
update the catch block in the loop that calls
client.messages.retrieve(pendingMessageID, { chatID }) so that before sleeping
you compute remainingMs = timeoutMs - (Date.now() - started) and if remainingMs
<= 0 break/throw, otherwise await sleep(Math.min(intervalMs, remainingMs)); this
ensures sleep is bounded to the remaining time and prevents the total wait from
exceeding timeoutMs while preserving lastError behavior.
packages/cli/src/commands/message-expiry.ts-23-23 (1)

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

Fix validation message to match accepted values.

Line 23 allows 0 but says “positive integer,” which excludes 0. Please align message with actual validation rule.

Suggested patch
-    if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('SECONDS must be a positive integer or "off"')
+    if (expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) throw new Error('SECONDS must be a non-negative integer or "off"')
🤖 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/message-expiry.ts` at line 23, The validation
message is incorrect: the check at the expiry validation (the if that tests
expiry !== null && (!Number.isInteger(expiry) || expiry < 0)) accepts 0 but the
error text says "positive integer"; update the thrown message to state
"non-negative integer or \"off\"" (or similar wording like "a non-negative
integer or \"off\"") so it matches the implemented rule in the expiry
validation.
packages/cli/src/commands/accounts/add.ts-21-21 (1)

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

Update --flow flag help to match actual behavior.

Line 21 says omitted --flow uses a default flow, but Line 55–57 requires explicit --flow when multiple methods exist in non-interactive/JSON paths. This makes --help misleading.

Suggested patch
-    flow: Flags.string({ description: 'Login flow ID. If omitted, Desktop chooses the default flow.' }),
+    flow: Flags.string({ description: 'Login flow ID. If omitted, you can choose interactively; in non-interactive/JSON mode with multiple methods, pass --flow.' }),
🤖 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` at line 21, Update the help text
for the flow flag so it reflects actual behavior: modify the description passed
to Flags.string for the flow flag (symbol "flow") in the Accounts add command to
state that the CLI will pick a default flow in interactive mode but that when
using non-interactive or JSON output paths (see the command's run method logic)
you must supply --flow explicitly if multiple login methods exist; ensure the
wording mentions both interactive default selection and the requirement for
explicit --flow in non-interactive/JSON scenarios.
packages/cli/src/lib/resolve.ts-64-68 (1)

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

Validate pick explicitly instead of relying on truthiness.

Line 64 skips validation when --pick 0 is provided, which falls through to ambiguity handling. Validate pick >= 1 whenever it is present.

Suggested patch
-  if (options.pick) {
+  if (options.pick !== undefined) {
+    if (!Number.isInteger(options.pick) || options.pick < 1) {
+      throw new Error('--pick must be a positive integer')
+    }
     const selected = matches[options.pick - 1]
     if (!selected) throw new Error(`--pick ${options.pick} is outside the ${matches.length} matching chats`)
     return String(selected.id)
   }
🤖 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/resolve.ts` around lines 64 - 68, The current branch
uses if (options.pick) so a value of 0 is treated as falsy and skips validation;
change the check to explicitly detect presence (e.g., options.pick !==
undefined) and validate the numeric range (ensure options.pick is >= 1 and <=
matches.length) before indexing matches; if out of range throw the existing
error message (`--pick ${options.pick} is outside the ${matches.length} matching
chats`), then compute selected = matches[options.pick - 1] and return
String(selected.id).
packages/cli/test/cli-smoke.mjs-14-21 (1)

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

Use per-run config directories to avoid smoke-test cross-contamination.

These hard-coded /tmp/... config directories are shared across runs, which can leak state and cause flaky results in parallel CI/local reruns. Generate unique temp dirs per run and clean them up at the end.

💡 Suggested fix
-const run = (...args) => spawnSync(process.execPath, ['./bin/run.js', ...args], {
+const smokeConfigDir = mkdtempSync(join(tmpdir(), 'beeper-cli-smoke-config-'))
+const run = (...args) => spawnSync(process.execPath, ['./bin/run.js', ...args], {
   cwd: root,
   encoding: 'utf8',
   env: {
     ...process.env,
-    BEEPER_CLI_CONFIG_DIR: '/tmp/beeper-cli-test',
+    BEEPER_CLI_CONFIG_DIR: smokeConfigDir,
   },
 })
@@
-const configDir = '/tmp/beeper-cli-test-config'
+const configDir = mkdtempSync(join(tmpdir(), 'beeper-cli-test-config-'))
@@
-      BEEPER_CLI_CONFIG_DIR: '/tmp/beeper-cli-accounts-test',
+      BEEPER_CLI_CONFIG_DIR: mkdtempSync(join(tmpdir(), 'beeper-cli-accounts-test-')),
@@
 rmSync(exportRoot, { recursive: true, force: true })
+rmSync(smokeConfigDir, { recursive: true, force: true })
+rmSync(configDir, { recursive: true, force: true })

Also applies to: 86-87, 146-147

🤖 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/test/cli-smoke.mjs` around lines 14 - 21, The smoke-test helper
'run' currently sets a hard-coded BEEPER_CLI_CONFIG_DIR
('/tmp/beeper-cli-test'), which can leak state across runs; change the test to
create a unique temp directory per invocation (e.g., using fs.mkdtempSync or
tmp-promise) and set BEEPER_CLI_CONFIG_DIR to that path in the env passed to run
(also update the two other similar usages at the locations referenced around
lines 86-87 and 146-147), and ensure tests remove the temp dirs after the run
(or create/cleanup in before/after hooks) so each invocation uses an isolated
config directory.
packages/cli/src/lib/export/index.ts-168-168 (1)

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

Honor explicit zero limits for chats/messages.

Line 168, Line 223, and Line 234 use truthy checks, so 0 is treated as “no limit”. That makes --limit-chats 0 / --limit-messages 0 export data instead of none.

Suggested fix
-    if (options.limitChats && chats.length >= options.limitChats) break
+    if (options.limitChats !== undefined && chats.length >= options.limitChats) break
...
-        if (options.limitMessages && messagesWritten >= options.limitMessages) break
+        if (options.limitMessages !== undefined && messagesWritten >= options.limitMessages) break
...
-      if (options.limitMessages && messagesWritten >= options.limitMessages) break
+      if (options.limitMessages !== undefined && messagesWritten >= options.limitMessages) break

Also applies to: 223-223, 234-234

🤖 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/export/index.ts` at line 168, The checks use truthy
checks so an explicit 0 is treated as “no limit”; change them to explicit
null/undefined checks so 0 is honored. Replace occurrences like "if
(options.limitChats && chats.length >= options.limitChats) break" with a guard
that checks the option is defined (e.g. "if (options.limitChats !== undefined &&
options.limitChats !== null && chats.length >= options.limitChats) break"), and
do the same for options.limitMessages checks (the other two occurrences
referencing messages.length and options.limitMessages). Ensure you update all
three occurrences (limitChats and both limitMessages checks) to use explicit
defined checks rather than truthiness.
packages/cli/test/e2e-staging.mjs-13-14 (1)

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

Remove the machine-specific default Desktop root.

Line 13 bakes in a user-local absolute path, which makes this script non-portable unless env override is always set.

Suggested fix
-const defaultDesktopRoot = '/Users/batuhan/.codex/worktrees/43bc/beeper/desktop'
-const desktopRoot = process.env.BEEPER_DESKTOP_ROOT || defaultDesktopRoot
+const desktopRoot = process.env.BEEPER_DESKTOP_ROOT
+if (!desktopRoot) {
+  throw new Error('Set BEEPER_DESKTOP_ROOT to your Beeper Desktop checkout path')
+}
🤖 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/test/e2e-staging.mjs` around lines 13 - 14, Remove the hardcoded
user-specific path in the defaultDesktopRoot variable and make desktopRoot
portable by deriving a default from the current user's home directory or
requiring the environment variable; specifically, replace the literal
'/Users/batuhan/…' assigned to defaultDesktopRoot with a computed path using the
platform-safe home directory (e.g., path.join(os.homedir(), '.codex',
'worktrees', '43bc', 'beeper', 'desktop')) or make desktopRoot fall back to
throwing a clear error if process.env.BEEPER_DESKTOP_ROOT is not set, updating
the references to defaultDesktopRoot and desktopRoot accordingly.
🧹 Nitpick comments (1)
packages/cli/scripts/lint (1)

8-8: ⚡ Quick win

Switch lint script to repo's declared package manager (pnpm).

The workspace declares pnpm@10.25.0 at the root but packages/cli/scripts/lint invokes npm run typecheck. This mismatch can cause inconsistent behavior between local environments and CI. Other scripts in the same directory (run, build, generate-readme.mjs) have the same issue. Update all script runners to use pnpm run for consistency.

🤖 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/lint` at line 8, Replace usage of the npm CLI with pnpm
in the scripts to match the repository package manager: change the "npm run
typecheck" call in packages/cli/scripts/lint to "pnpm run typecheck" and make
the same substitution in the other script files in the same directory
(packages/cli/scripts/run, packages/cli/scripts/build,
packages/cli/scripts/generate-readme.mjs) so all script runners use "pnpm run"
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/README.md`:
- Around line 154-155: The generated CLI command reference is including
non-user-facing declaration artifacts like "accounts.d", "accounts add.d", and
"api get.d"; update the command-manifest generation logic (the routine that
builds the command reference/manifest) to filter out entries whose command names
end with ".d" or that are marked as declaration/generated before writing docs.
Locate the function that collects commands for the README (e.g., the manifest or
reference builder) and add a predicate to exclude names matching /\.d$/ or a
generated/declaration flag so only real user commands appear in the docs.

In `@packages/cli/scripts/publish-homebrew-formula.mjs`:
- Around line 2-8: The root path conversion is using .pathname which breaks on
Windows and with URL-encoded chars; import fileURLToPath from 'node:url' and
replace the current root assignment (root = new URL('..',
import.meta.url).pathname) with a call that uses fileURLToPath(new URL('..',
import.meta.url)) so the variable root is a correct cross-platform filesystem
path.

In `@packages/cli/src/commands/app/e2ee/recovery-code/reset/begin.ts`:
- Around line 31-36: The stty invocations (execFileSync('stty', ['-echo'], ...)
and execFileSync('stty', ['echo'], ...)) can throw on platforms without stty;
wrap both calls in try/catch and silently ignore errors so input still proceeds
(i.e., attempt to disable echo before calling rl.question and re-enable in the
finally block, but if execFileSync throws just continue), ensuring rl.question,
rl.close and output.write behavior is unchanged; reference execFileSync,
rl.question, rl.close and output.write when making the change.

In `@packages/cli/src/commands/contacts/search.ts`:
- Around line 25-30: The current catch block around
client.accounts.contacts.search(accountID, ...) silently swallows failures which
can hide operational issues; modify the loop to record per-account errors (e.g.,
push error + accountID into an errors array or set a failedAccounts list) and
only swallow individual errors while continuing, then after the loop check if
collected.length === 0 and errors.length > 0 and surface an
aggregated/meaningful error (or rethrow) so callers see an operational failure
instead of an empty “no matches”; use the existing symbols
client.accounts.contacts.search, collected, result.items and accountID to locate
and implement the change.

In `@packages/cli/src/commands/export.ts`:
- Line 2: The export command must respect read-only mode: call and await
ensureWritable(flags) at the start of the command handler (before instantiating
the client and before any export/write actions) so the write-guard is enforced;
add the ensureWritable import if missing and insert await ensureWritable(flags)
in the BeeperCommand-derived command's run/execute method immediately prior to
creating the client/exporting files.

In `@packages/cli/src/lib/account-login.ts`:
- Around line 130-137: promptSecret currently calls execFileSync('stty', ...)
without handling failures or tracking whether echo was actually disabled; wrap
the disabling call in a try/catch and set a boolean (e.g., echoDisabled) only if
execFileSync succeeds, fall back to calling promptText(label) when execFileSync
throws (or when input.isTTY is false), and in the finally block only call
execFileSync('stty', ['echo'], ...) and output.write('\n') if echoDisabled is
true; reference promptSecret, promptText, execFileSync, input.isTTY, and
output.write when applying the change.

In `@packages/cli/src/lib/app-api.ts`:
- Around line 22-43: The appRequest function can return undefined for HTTP 204
but is declared as Promise<T>, so update the contract: change the function
signature of appRequest<T> to return Promise<T | undefined>, adjust the 204
branch to return undefined as T | undefined, and update any callers (or their
typings) expecting T to handle undefined responses; locate the function
appRequest in app-api.ts and update its return type and the return expressions
accordingly.

In `@packages/cli/src/lib/export/index.ts`:
- Around line 291-297: The current code in the export flow accepts
attacker-controlled attachment URLs and will copy local files when sourceURL
startsWith 'file://' or '/' (see sourceURL, copyFile, fileURLToPath,
absoluteTarget, target), which allows exfiltration of sensitive local files;
restrict this by rejecting local-file schemes by default, only allowing them
when an explicit opt-in flag is present and the resolved path is inside a
configured allowlist of trusted roots (normalize/resolve paths and verify
against allowed roots before calling fileURLToPath/copyFile), and return a clear
error when local sources are not permitted.

In `@packages/cli/src/lib/ink/components.tsx`:
- Around line 624-626: Top-level masking uses a regex but nested objects only
match exact keys, so update the rendering in components.tsx to reuse a shared
secret-key regex and mask nested secret-like keys too: introduce a single
secretKeyRegex (e.g. /(token|secret|key)/i), use that instead of the current
/^(accesstoken|token|secret|key)$/i test, and when rendering object values
(where the code currently uses typeof v === 'object' ? JSON.stringify(v) :
String(v)) iterate the object's entries and apply secretKeyRegex.test(propName)
to decide whether to call maskToken(value) for each nested property so keys like
refreshToken or clientSecret are masked consistently.
- Around line 9-15: The OSC-8 sequences are malformed: update the constants so
the ESC introducer is explicit and the closing OSC-8 segment is emitted; set
OSC8_START to '\x1b]8;;', BEL to '\x07', and OSC8_END to '\x1b]8;;\x07' and
ensure the Hyperlink component (and its use of supportsHyperlinks and Text)
concatenates OSC8_START + url + BEL + (children ?? url) + OSC8_END so terminals
can parse the hyperlink correctly.

In `@packages/cli/src/lib/ink/render.tsx`:
- Around line 289-291: The push method currently appends events unboundedly;
modify push (in the same object where push(event) calls setState with current
and events) to cap the events array to a fixed max (e.g., MAX_EVENTS = 1000) by
creating a new array that appends the incoming event and then takes only the
last MAX_EVENTS entries (use array slice or similar) before calling setState;
ensure you reference the existing symbols push, setState, current, and events so
the change preserves order while preventing unbounded memory growth.
- Around line 266-282: The interruptHandlers array is never populated so the
onInterrupt callback ends up no-op; fix this by exposing a way to register
handlers into that array when binding the StreamView: after setting api = hooks
in the bind block, add a registration method (e.g., hooks.registerInterrupt or
api.registerInterrupt) that pushes a provided callback into interruptHandlers
(interruptHandlers.push(fn)), and ensure callers use that registerInterrupt to
add their Ctrl+C handlers; keep the existing hooks.onInterrupt invocation that
iterates interruptHandlers to invoke them.

In `@packages/cli/test/e2e-staging.mjs`:
- Around line 96-106: The failure arises because pseudo-commands (the `.d`
entries in manifest) are counted as real commands; update the missing
calculation to ignore such pseudo-commands by first filtering manifest items
whose command endsWith('.d') (or otherwise identifies them as non-user) before
mapping and checking against coveredCommands so that report.missingCommands and
the subsequent throw only consider real CLI commands (adjust the expression that
defines missing and ensure writeReport()/error logic uses this filtered
missing).

---

Minor comments:
In `@packages/cli/src/commands/accounts/add.ts`:
- Line 21: Update the help text for the flow flag so it reflects actual
behavior: modify the description passed to Flags.string for the flow flag
(symbol "flow") in the Accounts add command to state that the CLI will pick a
default flow in interactive mode but that when using non-interactive or JSON
output paths (see the command's run method logic) you must supply --flow
explicitly if multiple login methods exist; ensure the wording mentions both
interactive default selection and the requirement for explicit --flow in
non-interactive/JSON scenarios.

In `@packages/cli/src/commands/api/post.ts`:
- Around line 20-21: Wrap the JSON.parse(flags.body) call in a try/catch to
detect malformed JSON and produce a CLI-friendly error: catch the parse error
thrown by JSON.parse(flags.body), log a clear message like "Invalid JSON in
--body: <parse error message>" (or throw a CLI error), and exit with a non-zero
status instead of letting the raw exception bubble up; then only call
client.post(args.path, { body }) and printData(...) when parsing succeeds.

In `@packages/cli/src/commands/avatar.ts`:
- Around line 23-27: Add a mutual-exclusion check so callers cannot pass both
PATH and --clear: in the avatar command handler (where flags.clear and args.path
are currently checked before calling createClient, resolveChatID,
client.chats.update, and printData) validate that exactly one of flags.clear or
args.path is provided; if both are present throw a clear error (e.g., "Cannot
provide both PATH and --clear") and if neither is present keep the existing
"Provide PATH or pass --clear" behavior.

In `@packages/cli/src/commands/message-expiry.ts`:
- Line 23: The validation message is incorrect: the check at the expiry
validation (the if that tests expiry !== null && (!Number.isInteger(expiry) ||
expiry < 0)) accepts 0 but the error text says "positive integer"; update the
thrown message to state "non-negative integer or \"off\"" (or similar wording
like "a non-negative integer or \"off\"") so it matches the implemented rule in
the expiry validation.

In `@packages/cli/src/lib/export/index.ts`:
- Line 168: The checks use truthy checks so an explicit 0 is treated as “no
limit”; change them to explicit null/undefined checks so 0 is honored. Replace
occurrences like "if (options.limitChats && chats.length >= options.limitChats)
break" with a guard that checks the option is defined (e.g. "if
(options.limitChats !== undefined && options.limitChats !== null && chats.length
>= options.limitChats) break"), and do the same for options.limitMessages checks
(the other two occurrences referencing messages.length and
options.limitMessages). Ensure you update all three occurrences (limitChats and
both limitMessages checks) to use explicit defined checks rather than
truthiness.

In `@packages/cli/src/lib/resolve.ts`:
- Around line 64-68: The current branch uses if (options.pick) so a value of 0
is treated as falsy and skips validation; change the check to explicitly detect
presence (e.g., options.pick !== undefined) and validate the numeric range
(ensure options.pick is >= 1 and <= matches.length) before indexing matches; if
out of range throw the existing error message (`--pick ${options.pick} is
outside the ${matches.length} matching chats`), then compute selected =
matches[options.pick - 1] and return String(selected.id).

In `@packages/cli/src/lib/wait.ts`:
- Around line 14-20: The loop in wait.ts can exceed timeoutMs because it always
sleeps intervalMs after a failed retrieve; update the catch block in the loop
that calls client.messages.retrieve(pendingMessageID, { chatID }) so that before
sleeping you compute remainingMs = timeoutMs - (Date.now() - started) and if
remainingMs <= 0 break/throw, otherwise await sleep(Math.min(intervalMs,
remainingMs)); this ensures sleep is bounded to the remaining time and prevents
the total wait from exceeding timeoutMs while preserving lastError behavior.

In `@packages/cli/test/cli-smoke.mjs`:
- Around line 14-21: The smoke-test helper 'run' currently sets a hard-coded
BEEPER_CLI_CONFIG_DIR ('/tmp/beeper-cli-test'), which can leak state across
runs; change the test to create a unique temp directory per invocation (e.g.,
using fs.mkdtempSync or tmp-promise) and set BEEPER_CLI_CONFIG_DIR to that path
in the env passed to run (also update the two other similar usages at the
locations referenced around lines 86-87 and 146-147), and ensure tests remove
the temp dirs after the run (or create/cleanup in before/after hooks) so each
invocation uses an isolated config directory.

In `@packages/cli/test/e2e-staging.mjs`:
- Around line 13-14: Remove the hardcoded user-specific path in the
defaultDesktopRoot variable and make desktopRoot portable by deriving a default
from the current user's home directory or requiring the environment variable;
specifically, replace the literal '/Users/batuhan/…' assigned to
defaultDesktopRoot with a computed path using the platform-safe home directory
(e.g., path.join(os.homedir(), '.codex', 'worktrees', '43bc', 'beeper',
'desktop')) or make desktopRoot fall back to throwing a clear error if
process.env.BEEPER_DESKTOP_ROOT is not set, updating the references to
defaultDesktopRoot and desktopRoot accordingly.

---

Nitpick comments:
In `@packages/cli/scripts/lint`:
- Line 8: Replace usage of the npm CLI with pnpm in the scripts to match the
repository package manager: change the "npm run typecheck" call in
packages/cli/scripts/lint to "pnpm run typecheck" and make the same substitution
in the other script files in the same directory (packages/cli/scripts/run,
packages/cli/scripts/build, packages/cli/scripts/generate-readme.mjs) so all
script runners use "pnpm run" 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: 66560138-a4b4-41ef-bf4b-0dedb2881227

📥 Commits

Reviewing files that changed from the base of the PR and between c81e6f7 and 88f2b3e.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (186)
  • .changeset/README.md
  • .changeset/config.json
  • .github/actions/setup-go/action.yml
  • .github/workflows/ci.yml
  • .github/workflows/publish-release.yml
  • README.md
  • cmd/beeper-desktop-cli/main.go
  • go.mod
  • internal/apiform/encoder.go
  • internal/apiform/form.go
  • internal/apiform/form_test.go
  • internal/apiquery/encoder.go
  • internal/apiquery/query.go
  • internal/apiquery/query_test.go
  • internal/autocomplete/autocomplete.go
  • internal/autocomplete/autocomplete_test.go
  • internal/autocomplete/shellscripts/bash_autocomplete.bash
  • internal/autocomplete/shellscripts/fish_autocomplete.fish
  • internal/autocomplete/shellscripts/pwsh_autocomplete.ps1
  • internal/autocomplete/shellscripts/zsh_autocomplete.zsh
  • internal/binaryparam/binary_param.go
  • internal/binaryparam/binary_param_test.go
  • internal/debugmiddleware/debug_middleware.go
  • internal/debugmiddleware/debug_middleware_test.go
  • internal/jsonview/explorer.go
  • internal/jsonview/staticdisplay.go
  • internal/mocktest/mocktest.go
  • internal/requestflag/innerflag.go
  • internal/requestflag/innerflag_test.go
  • internal/requestflag/requestflag.go
  • internal/requestflag/requestflag_test.go
  • package.json
  • packages/cli/CHANGELOG.md
  • packages/cli/LICENSE
  • packages/cli/README.md
  • packages/cli/SECURITY.md
  • packages/cli/bin/check-release-environment
  • packages/cli/bin/dev.js
  • packages/cli/bin/run.js
  • packages/cli/package.json
  • packages/cli/release-please-config.json
  • packages/cli/scripts/bootstrap
  • packages/cli/scripts/build
  • packages/cli/scripts/build-homebrew-archive.mjs
  • packages/cli/scripts/check-api-copy.mjs
  • packages/cli/scripts/format
  • packages/cli/scripts/generate-readme.mjs
  • packages/cli/scripts/link
  • packages/cli/scripts/lint
  • packages/cli/scripts/mock
  • packages/cli/scripts/publish-homebrew-formula.mjs
  • packages/cli/scripts/run
  • packages/cli/scripts/test
  • packages/cli/scripts/unlink
  • packages/cli/src/commands/accounts.ts
  • packages/cli/src/commands/accounts/add.ts
  • packages/cli/src/commands/api/get.ts
  • packages/cli/src/commands/api/post.ts
  • packages/cli/src/commands/app/e2ee/recovery-code/mark-backed-up.ts
  • packages/cli/src/commands/app/e2ee/recovery-code/reset/begin.ts
  • packages/cli/src/commands/app/e2ee/recovery-code/reset/confirm.ts
  • packages/cli/src/commands/app/e2ee/recovery-code/verify.ts
  • packages/cli/src/commands/app/e2ee/verification/accept.ts
  • packages/cli/src/commands/app/e2ee/verification/cancel.ts
  • packages/cli/src/commands/app/e2ee/verification/qr/confirm-scanned.ts
  • packages/cli/src/commands/app/e2ee/verification/qr/scan.ts
  • packages/cli/src/commands/app/e2ee/verification/sas/confirm.ts
  • packages/cli/src/commands/app/e2ee/verification/sas/start.ts
  • packages/cli/src/commands/app/e2ee/verification/start.ts
  • packages/cli/src/commands/app/status.ts
  • packages/cli/src/commands/archive.ts
  • packages/cli/src/commands/assets/download.ts
  • packages/cli/src/commands/assets/upload.ts
  • packages/cli/src/commands/auth/status.ts
  • packages/cli/src/commands/avatar.ts
  • packages/cli/src/commands/chat.ts
  • packages/cli/src/commands/chats/index.ts
  • packages/cli/src/commands/chats/search.ts
  • packages/cli/src/commands/clear-draft.ts
  • packages/cli/src/commands/commands.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/create-chat.ts
  • packages/cli/src/commands/current-user.ts
  • packages/cli/src/commands/delete-message.ts
  • packages/cli/src/commands/description.ts
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/draft.ts
  • packages/cli/src/commands/edit.ts
  • packages/cli/src/commands/export.ts
  • packages/cli/src/commands/focus.ts
  • packages/cli/src/commands/inbox.ts
  • packages/cli/src/commands/llm.ts
  • packages/cli/src/commands/login.tsx
  • packages/cli/src/commands/logout.ts
  • packages/cli/src/commands/low-priority.ts
  • packages/cli/src/commands/message-expiry.ts
  • packages/cli/src/commands/message.ts
  • packages/cli/src/commands/messages/index.ts
  • packages/cli/src/commands/messages/search.ts
  • packages/cli/src/commands/mute.ts
  • packages/cli/src/commands/notify-anyway.ts
  • packages/cli/src/commands/pin.ts
  • packages/cli/src/commands/react.ts
  • packages/cli/src/commands/read.ts
  • packages/cli/src/commands/remind.ts
  • packages/cli/src/commands/rpc.ts
  • packages/cli/src/commands/search.ts
  • packages/cli/src/commands/send/file.ts
  • packages/cli/src/commands/send/text.ts
  • packages/cli/src/commands/shell.ts
  • packages/cli/src/commands/start-chat.ts
  • packages/cli/src/commands/status.ts
  • packages/cli/src/commands/title.ts
  • packages/cli/src/commands/unarchive.ts
  • packages/cli/src/commands/unmute.ts
  • packages/cli/src/commands/unpin.ts
  • packages/cli/src/commands/unreact.ts
  • packages/cli/src/commands/unread.ts
  • packages/cli/src/commands/unremind.ts
  • packages/cli/src/commands/watch.ts
  • packages/cli/src/lib/account-login.ts
  • packages/cli/src/lib/app-api.ts
  • packages/cli/src/lib/argv.ts
  • packages/cli/src/lib/client.ts
  • packages/cli/src/lib/command.ts
  • packages/cli/src/lib/config.ts
  • packages/cli/src/lib/copy.ts
  • packages/cli/src/lib/desktop-auth.ts
  • packages/cli/src/lib/export/index.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/manifest.ts
  • packages/cli/src/lib/oauth.ts
  • packages/cli/src/lib/output.ts
  • packages/cli/src/lib/pkce.ts
  • packages/cli/src/lib/resolve.ts
  • packages/cli/src/lib/runner.ts
  • packages/cli/src/lib/send-message.ts
  • packages/cli/src/lib/wait.ts
  • packages/cli/src/plugin-sdk.ts
  • packages/cli/test/cli-smoke.mjs
  • packages/cli/test/e2e-staging.mjs
  • packages/cli/tsconfig.json
  • pkg/cmd/account.go
  • pkg/cmd/account_test.go
  • pkg/cmd/accountcontact.go
  • pkg/cmd/accountcontact_test.go
  • pkg/cmd/asset.go
  • pkg/cmd/asset_test.go
  • pkg/cmd/beeperdesktopapi.go
  • pkg/cmd/beeperdesktopapi_test.go
  • pkg/cmd/chat.go
  • pkg/cmd/chat_test.go
  • pkg/cmd/chatmessagereaction.go
  • pkg/cmd/chatmessagereaction_test.go
  • pkg/cmd/chatreminder.go
  • pkg/cmd/chatreminder_test.go
  • pkg/cmd/cmd.go
  • pkg/cmd/cmdutil.go
  • pkg/cmd/cmdutil_test.go
  • pkg/cmd/cmdutil_unix.go
  • pkg/cmd/cmdutil_windows.go
  • pkg/cmd/flagoptions.go
  • pkg/cmd/flagoptions_test.go
  • pkg/cmd/info.go
  • pkg/cmd/info_test.go
  • pkg/cmd/message.go
  • pkg/cmd/message_test.go
  • pkg/cmd/suggest.go
  • pkg/cmd/version.go
  • pnpm-workspace.yaml
  • scripts/build
  • scripts/link
  • scripts/lint
  • scripts/run
  • scripts/unlink
  • scripts/utils/upload-artifact.sh
  • tsconfig.base.json
💤 Files with no reviewable changes (51)
  • internal/binaryparam/binary_param_test.go
  • pkg/cmd/info_test.go
  • internal/debugmiddleware/debug_middleware.go
  • internal/apiform/encoder.go
  • .github/actions/setup-go/action.yml
  • pkg/cmd/flagoptions.go
  • go.mod
  • cmd/beeper-desktop-cli/main.go
  • internal/autocomplete/shellscripts/bash_autocomplete.bash
  • internal/autocomplete/shellscripts/fish_autocomplete.fish
  • pkg/cmd/cmdutil_test.go
  • internal/apiquery/query.go
  • pkg/cmd/accountcontact.go
  • pkg/cmd/account_test.go
  • internal/debugmiddleware/debug_middleware_test.go
  • pkg/cmd/chat_test.go
  • internal/apiform/form_test.go
  • internal/autocomplete/autocomplete_test.go
  • pkg/cmd/flagoptions_test.go
  • internal/autocomplete/shellscripts/pwsh_autocomplete.ps1
  • pkg/cmd/cmdutil_unix.go
  • internal/apiquery/query_test.go
  • internal/mocktest/mocktest.go
  • pkg/cmd/asset_test.go
  • internal/requestflag/requestflag_test.go
  • pkg/cmd/chatmessagereaction.go
  • internal/binaryparam/binary_param.go
  • pkg/cmd/beeperdesktopapi_test.go
  • pkg/cmd/account.go
  • pkg/cmd/chatreminder_test.go
  • internal/apiquery/encoder.go
  • pkg/cmd/cmdutil_windows.go
  • pkg/cmd/chatreminder.go
  • pkg/cmd/chatmessagereaction_test.go
  • pkg/cmd/info.go
  • internal/jsonview/staticdisplay.go
  • pkg/cmd/beeperdesktopapi.go
  • pkg/cmd/cmdutil.go
  • internal/requestflag/innerflag_test.go
  • pkg/cmd/message.go
  • pkg/cmd/cmd.go
  • internal/autocomplete/shellscripts/zsh_autocomplete.zsh
  • README.md
  • internal/autocomplete/autocomplete.go
  • pkg/cmd/accountcontact_test.go
  • pkg/cmd/asset.go
  • internal/jsonview/explorer.go
  • internal/requestflag/requestflag.go
  • internal/apiform/form.go
  • internal/requestflag/innerflag.go
  • pkg/cmd/chat.go
✅ Files skipped from review due to trivial changes (8)
  • .changeset/README.md
  • packages/cli/scripts/run
  • packages/cli/scripts/link
  • .changeset/config.json
  • packages/cli/tsconfig.json
  • packages/cli/src/commands/config/reset.ts
  • packages/cli/bin/check-release-environment
  • packages/cli/src/lib/ink/theme.ts
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.42.2)
packages/cli/src/lib/export/index.ts

[warning] 532-535: Manual HTML sanitization detected using string replacement methods. Manual sanitization is error-prone and can be bypassed. Use dedicated HTML sanitization libraries like 'sanitize-html' or 'DOMPurify' instead.
Context: value
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- https://cwe.mitre.org/data/definitions/79.html

(manual-html-sanitization)


[warning] 532-534: Manual HTML sanitization detected using string replacement methods. Manual sanitization is error-prone and can be bypassed. Use dedicated HTML sanitization libraries like 'sanitize-html' or 'DOMPurify' instead.
Context: value
.replaceAll('&', '&')
.replaceAll('<', '<')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- https://cwe.mitre.org/data/definitions/79.html

(manual-html-sanitization)


[warning] 532-535: Manual HTML entity encoding detected. Avoid manually sanitizing HTML input as it can be error-prone and lead to XSS vulnerabilities. Use dedicated libraries like sanitize-html or DOMPurify for proper HTML sanitization.
Context: value
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://cwe.mitre.org/data/definitions/79.html
- https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html

(manual-html-entity-encoding)


[warning] 532-534: Manual HTML entity encoding detected. Avoid manually sanitizing HTML input as it can be error-prone and lead to XSS vulnerabilities. Use dedicated libraries like sanitize-html or DOMPurify for proper HTML sanitization.
Context: value
.replaceAll('&', '&')
.replaceAll('<', '<')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://cwe.mitre.org/data/definitions/79.html
- https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html

(manual-html-entity-encoding)

🪛 LanguageTool
packages/cli/README.md

[uncategorized] ~49-~49: The official name of this software platform is spelled with a capital “H”.
Context: ... should use namespaced commands such as beeper github issue create and must treat installed ...

(GITHUB)


[uncategorized] ~2226-~2226: The official name of this software platform is spelled with a capital “H”.
Context: ...GitHub Releases. Push a v* tag to run .github/workflows/publish-release.yml. The re...

(GITHUB)

🔇 Additional comments (94)
packages/cli/src/commands/config/path.ts (1)

1-17: LGTM!

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

1-14: LGTM!

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

1-36: LGTM!

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

1-23: LGTM!

packages/cli/src/commands/app/e2ee/verification/start.ts (1)

1-22: LGTM!

packages/cli/src/commands/app/e2ee/recovery-code/verify.ts (1)

1-22: LGTM!

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

1-30: LGTM!

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

1-62: LGTM!

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

1-4: LGTM!

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

6-25: LGTM!

packages/cli/src/commands/app/e2ee/verification/cancel.ts (1)

7-25: LGTM!

packages/cli/src/commands/app/e2ee/recovery-code/mark-backed-up.ts (1)

6-16: LGTM!

packages/cli/src/commands/delete-message.ts (1)

8-33: LGTM!

packages/cli/src/commands/app/e2ee/recovery-code/reset/confirm.ts (1)

7-21: LGTM!

packages/cli/src/commands/current-user.ts (1)

1-13: LGTM!

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

1-25: LGTM!

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

1-44: LGTM!

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

1-41: LGTM!

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

1-51: LGTM!

packages/cli/scripts/unlink (1)

1-8: LGTM!

packages/cli/src/commands/app/e2ee/verification/sas/start.ts (1)

1-22: LGTM!

packages/cli/src/commands/app/e2ee/verification/sas/confirm.ts (1)

1-22: LGTM!

packages/cli/src/commands/app/e2ee/verification/qr/scan.ts (1)

1-22: LGTM!

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

1-30: LGTM!

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

1-27: LGTM!

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

1-75: LGTM!

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

1-32: LGTM!

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

1-25: LGTM!

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

1-26: LGTM!

packages/cli/scripts/build (1)

1-8: LGTM!

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

1-25: LGTM!

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

1-66: LGTM!

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

1-25: LGTM!

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

1-20: LGTM!

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

1-42: LGTM!

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

1-18: LGTM!

packages/cli/package.json (1)

1-115: LGTM!

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

1-32: LGTM!

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

1-47: LGTM!

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

1-16: LGTM!

packages/cli/src/commands/notify-anyway.ts (1)

1-25: LGTM!

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

1-25: LGTM!

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

1-22: LGTM!

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

1-38: LGTM!

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

8-24: LGTM!

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

8-24: LGTM!

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

1-4: LGTM!

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

3-38: LGTM!

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

5-12: LGTM!

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

20-28: LGTM!

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

1-28: LGTM!

packages/cli/src/commands/clear-draft.ts (1)

1-24: LGTM!

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

1-121: LGTM!

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

1-30: LGTM!

Also applies to: 35-40


31-33: ⚡ Quick win

The concern about this.error() with { exit: false } terminating the session is incorrect.

According to oclif documentation, this.error(message, { exit: false }) does not throw or cause process termination—it displays the error message and continues execution. The process-terminating logic is explicitly bypassed when exit: false is set. The current code in the loop is already safe and will not break the interactive session.

Additionally, the proposed fix references this.logToStderr(), which does not exist in this codebase.

			> Likely an incorrect or invalid review comment.
packages/cli/src/plugin-sdk.ts (1)

1-38: LGTM!

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

1-67: LGTM!

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

1-44: LGTM!

packages/cli/release-please-config.json (1)

64-65: LGTM!

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

1-26: LGTM!

packages/cli/src/commands/app/e2ee/verification/qr/confirm-scanned.ts (1)

1-22: LGTM!

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

1-26: LGTM!

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

1-54: LGTM!

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

1-25: LGTM!

packages/cli/src/lib/send-message.ts (1)

1-44: LGTM!

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

1-48: LGTM!

packages/cli/src/commands/low-priority.ts (1)

8-25: LGTM!

packages/cli/scripts/test (1)

56-56: LGTM!

packages/cli/scripts/bootstrap (1)

21-25: LGTM!

packages/cli/src/commands/app/e2ee/verification/accept.ts (1)

7-22: LGTM!

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

7-24: LGTM!

packages/cli/src/commands/create-chat.ts (1)

8-32: LGTM!

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

9-62: LGTM!

packages/cli/src/commands/login.tsx (1)

40-173: LGTM!

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

16-96: LGTM!

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

9-58: LGTM!

packages/cli/scripts/check-api-copy.mjs (1)

1-77: LGTM!

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

8-29: LGTM!

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

45-62: LGTM!

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

14-47: LGTM!

.github/workflows/ci.yml (1)

5-43: LGTM!

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

5-23: LGTM!

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

5-60: LGTM!

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

6-20: LGTM!

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

29-168: LGTM!

packages/cli/scripts/generate-readme.mjs (1)

1-253: LGTM!

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

1-24: LGTM!

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

1-30: LGTM!

packages/cli/scripts/build-homebrew-archive.mjs (1)

1-116: LGTM!

packages/cli/src/commands/start-chat.ts (1)

1-58: LGTM!

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

1-106: LGTM!

packages/cli/src/lib/ink/format.ts (1)

1-121: LGTM!

package.json (1)

1-25: LGTM!

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

1-121: LGTM!

Comment thread packages/cli/README.md
Comment on lines +154 to +155
| `accounts add.d` | |
| `accounts.d` | |
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

Generated command reference includes non-user .d commands.

Entries like accounts.d, api get.d, etc. should not be in user-facing docs; this indicates command-manifest generation is pulling declaration artifacts/noise.

Also applies to: 334-349

🤖 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` around lines 154 - 155, The generated CLI command
reference is including non-user-facing declaration artifacts like "accounts.d",
"accounts add.d", and "api get.d"; update the command-manifest generation logic
(the routine that builds the command reference/manifest) to filter out entries
whose command names end with ".d" or that are marked as declaration/generated
before writing docs. Locate the function that collects commands for the README
(e.g., the manifest or reference builder) and add a predicate to exclude names
matching /\.d$/ or a generated/declaration flag so only real user commands
appear in the docs.

Comment on lines +2 to +8
import {existsSync} from 'node:fs';
import {mkdir, mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {spawn} from 'node:child_process';

const root = new URL('..', import.meta.url).pathname;
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:

cat -n packages/cli/scripts/publish-homebrew-formula.mjs | head -20

Repository: beeper/desktop-api-cli

Length of output: 1287


🌐 Web query:

fileURLToPath vs pathname Node.js filesystem paths Windows

💡 Result:

In Node.js, you should use url.fileURLToPath instead of the URL.pathname property when converting a file URL to a filesystem path [1][2]. Using pathname directly is unreliable and often produces incorrect results, especially on Windows [3][2]. Key differences and reasons to use fileURLToPath: 1. Platform-Specific Formatting: On Windows, fileURLToPath correctly converts a file URL (e.g., file:///C:/path/) into a valid Windows-style path with backslashes (C:\path) [3][2]. In contrast, accessing.pathname on a Windows file URL typically returns a string with a leading slash and forward slashes (e.g., /C:/path/), which is not a valid path for most Node.js filesystem APIs [3][4]. 2. Percent-Encoding Handling: fileURLToPath automatically decodes percent-encoded characters (such as %20 for spaces or %E4%BD%A0 for non-ASCII characters) [3][2]. The.pathname property does not perform this decoding, leaving the string in its URL-encoded format [3][1]. 3. UNC Path Support: fileURLToPath correctly handles Windows UNC paths (e.g., file://nas/foo.txt), converting them to the appropriate format (\nas\foo.txt) [3][2]. The.pathname property would incorrectly return /foo.txt [3][2]. In summary, while.pathname is a raw component of the URL object, fileURLToPath is a utility specifically designed to resolve these URLs into platform-appropriate, usable filesystem paths [3][2]. Always use fileURLToPath to ensure cross-platform compatibility and correct path formatting [3][1].

Citations:


🏁 Script executed:

cat -n packages/cli/scripts/publish-homebrew-formula.mjs | grep -E "new URL|\.pathname"

Repository: beeper/desktop-api-cli

Length of output: 354


🏁 Script executed:

rg "new URL.*import.meta.url" --type js --type mjs -A 1 -B 1

Repository: beeper/desktop-api-cli

Length of output: 94


🏁 Script executed:

rg "new URL.*import.meta.url" --type js -A 1 -B 1

Repository: beeper/desktop-api-cli

Length of output: 48


Use fileURLToPath for filesystem path conversion.

Line 8 uses .pathname, which produces incorrect filesystem paths on Windows (returns /C:/path/ instead of C:\path\) and fails to decode URL-encoded characters (spaces as %20, non-ASCII characters, etc.). Node.js documentation recommends using fileURLToPath for cross-platform compatibility.

Proposed fix
 import {existsSync} from 'node:fs';
 import {mkdir, mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
 import {tmpdir} from 'node:os';
 import {join} from 'node:path';
+import {fileURLToPath} from 'node:url';
 import {spawn} from 'node:child_process';
 
-const root = new URL('..', import.meta.url).pathname;
+const root = fileURLToPath(new URL('..', import.meta.url));
📝 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 {existsSync} from 'node:fs';
import {mkdir, mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {spawn} from 'node:child_process';
const root = new URL('..', import.meta.url).pathname;
import {existsSync} from 'node:fs';
import {mkdir, mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {fileURLToPath} from 'node:url';
import {spawn} from 'node:child_process';
const root = fileURLToPath(new URL('..', import.meta.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/scripts/publish-homebrew-formula.mjs` around lines 2 - 8, The
root path conversion is using .pathname which breaks on Windows and with
URL-encoded chars; import fileURLToPath from 'node:url' and replace the current
root assignment (root = new URL('..', import.meta.url).pathname) with a call
that uses fileURLToPath(new URL('..', import.meta.url)) so the variable root is
a correct cross-platform filesystem path.

Comment on lines +31 to +36
execFileSync('stty', ['-echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
return (await rl.question(label)).trim()
} finally {
rl.close()
execFileSync('stty', ['echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
output.write('\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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n packages/cli/src/commands/app/e2ee/recovery-code/reset/begin.ts

Repository: beeper/desktop-api-cli

Length of output: 2010


🏁 Script executed:

rg 'stty' --type ts --type js

Repository: beeper/desktop-api-cli

Length of output: 707


🏁 Script executed:

fd package.json | head -5 | xargs cat

Repository: beeper/desktop-api-cli

Length of output: 3932


🏁 Script executed:

rg -B 5 -A 5 "stty" packages/cli/src/lib/account-login.ts

Repository: beeper/desktop-api-cli

Length of output: 421


🏁 Script executed:

rg -i 'windows|win32|platform|darwin' packages/cli/src --type ts --type js | head -20

Repository: beeper/desktop-api-cli

Length of output: 637


🏁 Script executed:

rg -B 3 -A 8 'promptSecret' packages/cli/src/lib/account-login.ts | head -30

Repository: beeper/desktop-api-cli

Length of output: 886


Wrap stty calls in error handling for platform portability.

Lines 31 and 35 call stty without catching errors. On platforms where stty is unavailable (notably Windows with modern TTY support), this causes a hard crash before input is captured. The function should gracefully fall back to standard input without echo suppression when stty is unavailable.

Suggested fix
 async function promptSecret(label: string): Promise<string> {
   const rl = createInterface({ input, output })
+  let echoDisabled = false
   try {
-    execFileSync('stty', ['-echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
+    try {
+      execFileSync('stty', ['-echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
+      echoDisabled = true
+    } catch {
+      // Fallback: continue without echo suppression when stty is unavailable.
+    }
     return (await rl.question(label)).trim()
   } finally {
     rl.close()
-    execFileSync('stty', ['echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
-    output.write('\n')
+    if (echoDisabled) {
+      execFileSync('stty', ['echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
+      output.write('\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
execFileSync('stty', ['-echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
return (await rl.question(label)).trim()
} finally {
rl.close()
execFileSync('stty', ['echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
output.write('\n')
async function promptSecret(label: string): Promise<string> {
const rl = createInterface({ input, output })
let echoDisabled = false
try {
try {
execFileSync('stty', ['-echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
echoDisabled = true
} catch {
// Fallback: continue without echo suppression when stty is unavailable.
}
return (await rl.question(label)).trim()
} finally {
rl.close()
if (echoDisabled) {
execFileSync('stty', ['echo'], { stdio: ['inherit', 'ignore', 'ignore'] })
output.write('\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/commands/app/e2ee/recovery-code/reset/begin.ts` around lines
31 - 36, The stty invocations (execFileSync('stty', ['-echo'], ...) and
execFileSync('stty', ['echo'], ...)) can throw on platforms without stty; wrap
both calls in try/catch and silently ignore errors so input still proceeds
(i.e., attempt to disable echo before calling rl.question and re-enable in the
finally block, but if execFileSync throws just continue), ensuring rl.question,
rl.close and output.write behavior is unchanged; reference execFileSync,
rl.question, rl.close and output.write when making the change.

Comment on lines +25 to +30
try {
const result = await client.accounts.contacts.search(accountID, { query: args.query })
collected.push(...result.items.map((item: unknown) => ({ ...(item as Record<string, unknown>), accountID })))
} catch {
// Some networks reject exact lookups for some identifiers; keep trying the rest.
}
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

Don’t silently swallow all account-level failures.

If every account request fails, this currently returns an empty result and looks like “no matches” instead of an operational failure.

Suggested patch
     const load = async (): Promise<Array<Record<string, unknown>>> => {
       const collected: Array<Record<string, unknown>> = []
+      let successCount = 0
       for (const accountID of accountIDs) {
         try {
           const result = await client.accounts.contacts.search(accountID, { query: args.query })
+          successCount += 1
           collected.push(...result.items.map((item: unknown) => ({ ...(item as Record<string, unknown>), accountID })))
         } catch {
           // Some networks reject exact lookups for some identifiers; keep trying the rest.
         }
       }
+      if (accountIDs.length > 0 && successCount === 0) {
+        throw new Error('Contact search failed for all selected accounts')
+      }
       return collected
     }
📝 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
try {
const result = await client.accounts.contacts.search(accountID, { query: args.query })
collected.push(...result.items.map((item: unknown) => ({ ...(item as Record<string, unknown>), accountID })))
} catch {
// Some networks reject exact lookups for some identifiers; keep trying the rest.
}
const load = async (): Promise<Array<Record<string, unknown>>> => {
const collected: Array<Record<string, unknown>> = []
let successCount = 0
for (const accountID of accountIDs) {
try {
const result = await client.accounts.contacts.search(accountID, { query: args.query })
successCount += 1
collected.push(...result.items.map((item: unknown) => ({ ...(item as Record<string, unknown>), accountID })))
} catch {
// Some networks reject exact lookups for some identifiers; keep trying the rest.
}
}
if (accountIDs.length > 0 && successCount === 0) {
throw new Error('Contact search failed for all selected accounts')
}
return collected
}
🤖 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` around lines 25 - 30, The
current catch block around client.accounts.contacts.search(accountID, ...)
silently swallows failures which can hide operational issues; modify the loop to
record per-account errors (e.g., push error + accountID into an errors array or
set a failedAccounts list) and only swallow individual errors while continuing,
then after the loop check if collected.length === 0 and errors.length > 0 and
surface an aggregated/meaningful error (or rethrow) so callers see an
operational failure instead of an empty “no matches”; use the existing symbols
client.accounts.contacts.search, collected, result.items and accountID to locate
and implement the change.

@@ -0,0 +1,50 @@
import { Flags } from '@oclif/core'
import { BeeperCommand } from '../lib/command.js'
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

Respect read-only mode before starting export.

This command writes local state (export files) but skips the write guard. Add ensureWritable(flags) before creating the client/exporting.

Proposed fix
-import { BeeperCommand } from '../lib/command.js'
+import { BeeperCommand, ensureWritable } from '../lib/command.js'
@@
   async run(): Promise<void> {
     const { flags } = await this.parse(Export)
+    ensureWritable(flags)
     const client = await createClient(flags)

Also applies to: 27-29

🤖 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/export.ts` at line 2, The export command must
respect read-only mode: call and await ensureWritable(flags) at the start of the
command handler (before instantiating the client and before any export/write
actions) so the write-guard is enforced; add the ensureWritable import if
missing and insert await ensureWritable(flags) in the BeeperCommand-derived
command's run/execute method immediately prior to creating the client/exporting
files.

Comment on lines +9 to +15
const OSC8_START = ']8;;'
const OSC8_END = ']8;;'
const BEL = ''
const Hyperlink: React.FC<{ url: string; children?: React.ReactNode }> = ({ url, children }) => {
if (!supportsHyperlinks) return <>{children ?? url}</>
return <Text>{OSC8_START}{url}{BEL}{children ?? url}{OSC8_END}</Text>
}
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

Fix OSC-8 hyperlink escape sequence generation.

The current sequence omits the ESC introducer and closing OSC-8 segment, so terminals won’t parse it as a hyperlink.

Suggested fix
-const OSC8_START = ';;'
-const OSC8_END = ''
+const ESC = '\u001B'
+const OSC8_START = `${ESC}]8;;`
+const OSC8_END = `${ESC}]8;;`
 const BEL = '�'
 const Hyperlink: React.FC<{ url: string; children?: React.ReactNode }> = ({ url, children }) => {
   if (!supportsHyperlinks) return <>{children ?? url}</>
-  return <Text>{OSC8_START}{url}{BEL}{children ?? url}{OSC8_END}</Text>
+  return <Text>{OSC8_START}{url}{BEL}{children ?? url}{OSC8_END}{BEL}</Text>
 }
🤖 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/ink/components.tsx` around lines 9 - 15, The OSC-8
sequences are malformed: update the constants so the ESC introducer is explicit
and the closing OSC-8 segment is emitted; set OSC8_START to '\x1b]8;;', BEL to
'\x07', and OSC8_END to '\x1b]8;;\x07' and ensure the Hyperlink component (and
its use of supportsHyperlinks and Text) concatenates OSC8_START + url + BEL +
(children ?? url) + OSC8_END so terminals can parse the hyperlink correctly.

Comment on lines +624 to +626
{/^(accesstoken|token|secret|key)$/i.test(k) && typeof v === 'string'
? <Text color={theme.subtle}>{maskToken(v)}</Text>
: <Text color={theme.text}>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</Text>}
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

Mask nested secret-like keys consistently.

Top-level masking uses /token|secret|key/i, but nested masking only matches exact keys (token|secret|key|accesstoken). Keys like refreshToken or clientSecret in nested objects can leak.

Suggested fix
-          {/^(accesstoken|token|secret|key)$/i.test(k) && typeof v === 'string'
+          {/token|secret|key/i.test(k) && typeof v === 'string'
             ? <Text color={theme.subtle}>{maskToken(v)}</Text>
             : <Text color={theme.text}>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</Text>}
📝 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
{/^(accesstoken|token|secret|key)$/i.test(k) && typeof v === 'string'
? <Text color={theme.subtle}>{maskToken(v)}</Text>
: <Text color={theme.text}>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</Text>}
{/token|secret|key/i.test(k) && typeof v === 'string'
? <Text color={theme.subtle}>{maskToken(v)}</Text>
: <Text color={theme.text}>{typeof v === 'object' ? JSON.stringify(v) : String(v)}</Text>}
🤖 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/ink/components.tsx` around lines 624 - 626, Top-level
masking uses a regex but nested objects only match exact keys, so update the
rendering in components.tsx to reuse a shared secret-key regex and mask nested
secret-like keys too: introduce a single secretKeyRegex (e.g.
/(token|secret|key)/i), use that instead of the current
/^(accesstoken|token|secret|key)$/i test, and when rendering object values
(where the code currently uses typeof v === 'object' ? JSON.stringify(v) :
String(v)) iterate the object's entries and apply secretKeyRegex.test(propName)
to decide whether to call maskToken(value) for each nested property so keys like
refreshToken or clientSecret are masked consistently.

Comment on lines +266 to +282
const interruptHandlers: Array<() => void> = []

const setState = (next: StreamState): void => {
current = next
api?.update(next)
}

const instance = inkRender(
<StreamView
initialState={initial}
baseURL={opts.baseURL}
subscribed={opts.subscribed}
bind={hooks => {
api = hooks
hooks.onInterrupt(() => {
for (const fn of interruptHandlers) fn()
})
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

Ctrl+C interrupt callback currently does nothing.

interruptHandlers is never populated, so the registered interrupt callback can no-op and leave stream mode running.

Suggested fix
       bind={hooks => {
         api = hooks
         hooks.onInterrupt(() => {
-          for (const fn of interruptHandlers) fn()
+          if (interruptHandlers.length === 0) {
+            hooks.exit()
+            return
+          }
+          for (const fn of interruptHandlers) fn()
         })
       }}
📝 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 interruptHandlers: Array<() => void> = []
const setState = (next: StreamState): void => {
current = next
api?.update(next)
}
const instance = inkRender(
<StreamView
initialState={initial}
baseURL={opts.baseURL}
subscribed={opts.subscribed}
bind={hooks => {
api = hooks
hooks.onInterrupt(() => {
for (const fn of interruptHandlers) fn()
})
const interruptHandlers: Array<() => void> = []
const setState = (next: StreamState): void => {
current = next
api?.update(next)
}
const instance = inkRender(
<StreamView
initialState={initial}
baseURL={opts.baseURL}
subscribed={opts.subscribed}
bind={hooks => {
api = hooks
hooks.onInterrupt(() => {
if (interruptHandlers.length === 0) {
hooks.exit()
return
}
for (const fn of interruptHandlers) fn()
})
🤖 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/ink/render.tsx` around lines 266 - 282, The
interruptHandlers array is never populated so the onInterrupt callback ends up
no-op; fix this by exposing a way to register handlers into that array when
binding the StreamView: after setting api = hooks in the bind block, add a
registration method (e.g., hooks.registerInterrupt or api.registerInterrupt)
that pushes a provided callback into interruptHandlers
(interruptHandlers.push(fn)), and ensure callers use that registerInterrupt to
add their Ctrl+C handlers; keep the existing hooks.onInterrupt invocation that
iterates interruptHandlers to invoke them.

Comment on lines +289 to +291
push(event) {
setState({ ...current, events: [...current.events, event] })
},
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

Cap in-memory stream history to avoid unbounded growth.

push() appends forever, so long-running watch sessions can grow memory without bound.

Suggested fix
+  const MAX_EVENTS = 2000
...
     push(event) {
-      setState({ ...current, events: [...current.events, event] })
+      const nextEvents = [...current.events, event]
+      setState({ ...current, events: nextEvents.slice(-MAX_EVENTS) })
     },
🤖 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/ink/render.tsx` around lines 289 - 291, The push method
currently appends events unboundedly; modify push (in the same object where
push(event) calls setState with current and events) to cap the events array to a
fixed max (e.g., MAX_EVENTS = 1000) by creating a new array that appends the
incoming event and then takes only the last MAX_EVENTS entries (use array slice
or similar) before calling setState; ensure you reference the existing symbols
push, setState, current, and events so the change preserves order while
preventing unbounded memory growth.

Comment on lines +96 to +106
const missing = manifest.map(item => item.command).filter(command => !coveredCommands.has(command))
report.missingCommands = missing
if (report.missingOpenAPIOperations?.length) {
throw new Error(`E2E did not cover ${report.missingOpenAPIOperations.length} OpenAPI operation(s): ${report.missingOpenAPIOperations.join(', ')}`)
}
report.finishedAt = new Date().toISOString()
await writeReport()

if (missing.length) {
throw new Error(`E2E did not exercise ${missing.length} command(s): ${missing.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 | 🟠 Major | ⚡ Quick win

Filter pseudo-commands before enforcing full command coverage.

The manifest currently includes non-user .d entries (also visible in generated docs), so this strict check can fail even when real CLI commands are covered.

Suggested fix
-  const missing = manifest.map(item => item.command).filter(command => !coveredCommands.has(command))
+  const manifestCommands = manifest
+    .map(item => item.command)
+    .filter(command => !command.endsWith('.d'))
+  const missing = manifestCommands.filter(command => !coveredCommands.has(command))
📝 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 missing = manifest.map(item => item.command).filter(command => !coveredCommands.has(command))
report.missingCommands = missing
if (report.missingOpenAPIOperations?.length) {
throw new Error(`E2E did not cover ${report.missingOpenAPIOperations.length} OpenAPI operation(s): ${report.missingOpenAPIOperations.join(', ')}`)
}
report.finishedAt = new Date().toISOString()
await writeReport()
if (missing.length) {
throw new Error(`E2E did not exercise ${missing.length} command(s): ${missing.join(', ')}`)
}
const manifestCommands = manifest
.map(item => item.command)
.filter(command => !command.endsWith('.d'))
const missing = manifestCommands.filter(command => !coveredCommands.has(command))
report.missingCommands = missing
if (report.missingOpenAPIOperations?.length) {
throw new Error(`E2E did not cover ${report.missingOpenAPIOperations.length} OpenAPI operation(s): ${report.missingOpenAPIOperations.join(', ')}`)
}
report.finishedAt = new Date().toISOString()
await writeReport()
if (missing.length) {
throw new Error(`E2E did not exercise ${missing.length} command(s): ${missing.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/test/e2e-staging.mjs` around lines 96 - 106, The failure arises
because pseudo-commands (the `.d` entries in manifest) are counted as real
commands; update the missing calculation to ignore such pseudo-commands by first
filtering manifest items whose command endsWith('.d') (or otherwise identifies
them as non-user) before mapping and checking against coveredCommands so that
report.missingCommands and the subsequent throw only consider real CLI commands
(adjust the expression that defines missing and ensure writeReport()/error logic
uses this filtered missing).

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