Add Ring Camera Recorder (initial codebase)#1
Conversation
Self-hosted tool that records the live view from Ring cameras you own to local disk, without a Ring Protect subscription. Includes: - Interactive 2FA auth with automatic refresh-token rotation/persistence - Motion/ding-triggered recording, plus manual one-shot capture - Fragmented-MP4 output (interrupted clips stay playable) - Configurable retention, layered config, CLI (list/record/verify) - Hermetic tests (filename, retention, trigger state machine) - systemd + pm2 service units, CI workflow, LICENSE/NOTICE/SECURITY docs
Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request introduces a self-hosted local recorder for Ring cameras, featuring motion and doorbell-triggered recording, automatic token rotation, and configurable clip retention. The reviewer provided constructive feedback focused on improving robustness and error handling. Key recommendations include preserving original error details during authentication failures, recursively creating directories before writing the token file, wrapping token parsing and logger serialization in try-catch blocks to prevent crashes, validating retention sweep intervals, and unsubscribing from event streams immediately in the verification harness to prevent concurrent overlapping recordings.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| } catch { | ||
| // Expected when 2FA is enabled: the first getAuth triggers the code being | ||
| // sent and sets promptFor2fa with a human-readable instruction. | ||
| if (!client.promptFor2fa) { | ||
| throw new Error('Login failed (check email/password). Ring did not request a 2FA code.'); | ||
| } |
There was a problem hiding this comment.
The catch block masks the original error when client.getAuth() fails for reasons other than a 2FA prompt (e.g., network errors, DNS issues, or rate limits). This makes troubleshooting difficult. Preserving the original error message in the thrown error will greatly improve debuggability.
| } catch { | |
| // Expected when 2FA is enabled: the first getAuth triggers the code being | |
| // sent and sets promptFor2fa with a human-readable instruction. | |
| if (!client.promptFor2fa) { | |
| throw new Error('Login failed (check email/password). Ring did not request a 2FA code.'); | |
| } | |
| } catch (err) { | |
| // Expected when 2FA is enabled: the first getAuth triggers the code being | |
| // sent and sets promptFor2fa with a human-readable instruction. | |
| if (!client.promptFor2fa) { | |
| throw new Error('Login failed: ' + (err as Error).message); | |
| } |
| @@ -0,0 +1,79 @@ | |||
| import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs'; | |||
There was a problem hiding this comment.
Import mkdirSync from node:fs and dirname from node:path to support safe directory creation for the token file.
| import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs'; | |
| import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync } from 'node:fs'; | |
| import { dirname } from 'node:path'; |
| const raw = JSON.parse(readFileSync(cfg.tokenPath, 'utf8')) as TokenFile; | ||
| if (!raw.refreshToken) throw new Error(`Token file ${cfg.tokenPath} is missing "refreshToken".`); | ||
| return raw.refreshToken; |
There was a problem hiding this comment.
Wrap readFileSync and JSON.parse in a try-catch block to handle cases where the token file is corrupted, unreadable, or contains invalid JSON, providing a clear and helpful error message.
| const raw = JSON.parse(readFileSync(cfg.tokenPath, 'utf8')) as TokenFile; | |
| if (!raw.refreshToken) throw new Error(`Token file ${cfg.tokenPath} is missing "refreshToken".`); | |
| return raw.refreshToken; | |
| try { | |
| const raw = JSON.parse(readFileSync(cfg.tokenPath, 'utf8')) as TokenFile; | |
| if (!raw || !raw.refreshToken) { | |
| throw new Error('File is missing the "refreshToken" property.'); | |
| } | |
| return raw.refreshToken; | |
| } catch (err) { | |
| throw new Error('Failed to read or parse token file at ' + cfg.tokenPath + ': ' + (err as Error).message); | |
| } |
| export function writeToken(cfg: AppConfig, refreshToken: string): void { | ||
| const body: TokenFile = { refreshToken, updatedAt: new Date().toISOString() }; | ||
| writeFileSync(cfg.tokenPath, JSON.stringify(body, null, 2), { mode: 0o600 }); |
There was a problem hiding this comment.
If the directory containing cfg.tokenPath does not exist (e.g., if configured to a nested directory like secrets/ring-token.json), writeFileSync will throw an ENOENT error and crash the application. This is especially critical during background token rotation. Ensure the parent directory is created recursively before writing.
export function writeToken(cfg: AppConfig, refreshToken: string): void {
const body: TokenFile = { refreshToken, updatedAt: new Date().toISOString() };
const dir = dirname(cfg.tokenPath);
if (dir && !existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(cfg.tokenPath, JSON.stringify(body, null, 2), { mode: 0o600 });| function validate(c: AppConfig): void { | ||
| if (c.clipLengthSeconds <= 0) throw new Error('clipLengthSeconds must be > 0'); | ||
| if (c.motionCooldownSeconds < 0) throw new Error('motionCooldownSeconds must be >= 0'); | ||
| if (c.retentionDays !== null && c.retentionDays < 0) throw new Error('retentionDays must be >= 0 or null'); | ||
| if (c.cameras !== 'all' && !Array.isArray(c.cameras)) { | ||
| throw new Error('cameras must be "all" or an array of names/ids'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Add validation for retentionSweepMinutes to ensure it is strictly greater than 0. If configured to 0 or a negative value, setInterval in src/retention.ts could execute continuously, leading to high CPU usage or unexpected runtime errors.
| function validate(c: AppConfig): void { | |
| if (c.clipLengthSeconds <= 0) throw new Error('clipLengthSeconds must be > 0'); | |
| if (c.motionCooldownSeconds < 0) throw new Error('motionCooldownSeconds must be >= 0'); | |
| if (c.retentionDays !== null && c.retentionDays < 0) throw new Error('retentionDays must be >= 0 or null'); | |
| if (c.cameras !== 'all' && !Array.isArray(c.cameras)) { | |
| throw new Error('cameras must be "all" or an array of names/ids'); | |
| } | |
| } | |
| function validate(c: AppConfig): void { | |
| if (c.clipLengthSeconds <= 0) throw new Error('clipLengthSeconds must be > 0'); | |
| if (c.motionCooldownSeconds < 0) throw new Error('motionCooldownSeconds must be >= 0'); | |
| if (c.retentionDays !== null && c.retentionDays < 0) throw new Error('retentionDays must be >= 0 or null'); | |
| if (c.retentionSweepMinutes <= 0) throw new Error('retentionSweepMinutes must be > 0'); | |
| if (c.cameras !== 'all' && !Array.isArray(c.cameras)) { | |
| throw new Error('cameras must be "all" or an array of names/ids'); | |
| } | |
| } |
| const onTrigger = (reason: string) => { | ||
| log.info(` → ${reason} detected; recording a clip to prove the trigger path…`); | ||
| recordClip(camera, cfg, Math.min(cfg.clipLengthSeconds, 10)) | ||
| .then(() => finish(true)) | ||
| .catch((e) => { | ||
| log.error(` recording after trigger failed: ${(e as Error).message}`); | ||
| finish(false); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
In waitForTriggeredRecording, if multiple events (e.g., rapid motion events or a doorbell press) fire before the first recordClip finishes, multiple concurrent recordings will be triggered on the same camera. This can cause overlapping ffmpeg writes and API throttling. Unsubscribe from the event streams immediately upon receiving the first trigger.
| const onTrigger = (reason: string) => { | |
| log.info(` → ${reason} detected; recording a clip to prove the trigger path…`); | |
| recordClip(camera, cfg, Math.min(cfg.clipLengthSeconds, 10)) | |
| .then(() => finish(true)) | |
| .catch((e) => { | |
| log.error(` recording after trigger failed: ${(e as Error).message}`); | |
| finish(false); | |
| }); | |
| }; | |
| const onTrigger = (reason: string) => { | |
| subs.forEach((s) => s.unsubscribe()); | |
| subs.length = 0; | |
| log.info(' → ' + reason + ' detected; recording a clip to prove the trigger path…'); | |
| recordClip(camera, cfg, Math.min(cfg.clipLengthSeconds, 10)) | |
| .then(() => finish(true)) | |
| .catch((e) => { | |
| log.error(' recording after trigger failed: ' + (e as Error).message); | |
| finish(false); | |
| }); | |
| }; |
| if (extra !== undefined) { | ||
| stream.write(`${line} ${typeof extra === 'string' ? extra : JSON.stringify(extra)}\n`); | ||
| } else { |
There was a problem hiding this comment.
If extra is a complex object containing circular references (such as certain Error objects or API responses), JSON.stringify(extra) will throw a TypeError and crash the logger. Since logging is frequently used inside catch blocks, a logger crash will mask the original error and crash the application. Wrap the serialization in a try-catch block for safety.
if (extra !== undefined) {
let extraStr = '';
try {
extraStr = typeof extra === 'string' ? extra : JSON.stringify(extra);
} catch {
extraStr = '[Unserializable Object]';
}
stream.write(line + ' ' + extraStr + '\n');
} else {|
Warning Review limit reached
More reviews will be available in 41 minutes and 42 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
📝 WalkthroughWalkthroughThis PR introduces the complete initial implementation of ChangesRing Camera Recorder — full initial implementation
Sequence Diagram(s)sequenceDiagram
participant User
participant auth as src/auth.ts
participant RingRestClient
participant ring as src/ring.ts
participant index as src/index.ts
participant watchCamera as src/events.ts
participant recordClip as src/recorder.ts
participant RingCamera
User->>auth: npm run auth (email/password/2FA)
auth->>RingRestClient: authenticate credentials
RingRestClient-->>auth: refreshToken
auth->>ring: writeToken(.ring-token.json, 0600)
User->>index: npm start (long-running service)
index->>ring: createRingApi (readToken)
ring->>ring: subscribe onRefreshTokenUpdated → writeToken
index->>ring: getSelectedCameras
ring-->>index: filtered RingCamera list
index->>watchCamera: watchCamera(camera, cfg, recordClip)
RingCamera-->>watchCamera: motion active (rising edge) or ding
watchCamera->>watchCamera: shouldTrigger? (no overlap, cooldown passed)
watchCamera->>recordClip: recordClip(camera, cfg, clipLengthSeconds)
recordClip->>RingCamera: streamVideo(fragmented MP4 args)
RingCamera-->>recordClip: onCallEnded
recordClip-->>watchCamera: RecordResult {path, bytes, seconds}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| }; | ||
|
|
||
| sweep(); // run immediately on startup | ||
| const handle = setInterval(sweep, cfg.retentionSweepMinutes * 60 * 1000); |
There was a problem hiding this comment.
Suggestion: The sweep interval is scheduled from cfg.retentionSweepMinutes without validating it is a positive finite number. If config/env sets it to 0, negative, or NaN, Node timers effectively run almost continuously, causing a tight retention loop (high CPU + repeated filesystem churn). Reject invalid values before calling setInterval (or clamp to a safe minimum). [performance]
Severity Level: Major ⚠️
- ⚠️ Long-running service in `src/index.ts` can spike CPU.
- ⚠️ Excessive filesystem scans in `pruneOldClips` under misconfig.
- ⚠️ Retention logs may flood due to rapid sweep scheduling.Steps of Reproduction ✅
1. Configure a positive retention window in the config layer: set `retentionDays` to a
value > 0 and `retentionSweepMinutes` to `0`, a negative number, or a non-numeric string
in `config.json` / `config.local.json` (these files are merged in `src/config.ts:60-73`).
2. Start the long-running service by executing the main entrypoint in `src/index.ts:12-45`
(e.g., the script that runs `main()`), which calls `loadConfig()` (`src/index.ts:13`) and
then `startRetention(cfg)` at `src/index.ts:30`.
3. Inside `startRetention` (`src/retention.ts:9-27`), the `cfg.retentionDays` guard at
lines 10-13 passes (since it is > 0), then `setInterval(sweep, cfg.retentionSweepMinutes *
60 * 1000)` at line 23 schedules the sweep with an interval derived from the misconfigured
`retentionSweepMinutes`.
4. Because Node.js coerces invalid/zero/negative timer values to a very short delay, the
`sweep` callback at `src/retention.ts:15-20` repeatedly calls `pruneOldClips()`
(`src/files.ts:39-64`) in a near-tight loop, causing continuous directory scans and
`statSync`/`unlinkSync` operations against `cfg.outputDir` and driving high CPU and
filesystem churn.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/retention.ts
**Line:** 23:23
**Comment:**
*Performance: The sweep interval is scheduled from `cfg.retentionSweepMinutes` without validating it is a positive finite number. If config/env sets it to `0`, negative, or `NaN`, Node timers effectively run almost continuously, causing a tight retention loop (high CPU + repeated filesystem churn). Reject invalid values before calling `setInterval` (or clamp to a safe minimum).
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| /** Write the token with 0600 perms (it grants full account access). */ | ||
| export function writeToken(cfg: AppConfig, refreshToken: string): void { | ||
| const body: TokenFile = { refreshToken, updatedAt: new Date().toISOString() }; | ||
| writeFileSync(cfg.tokenPath, JSON.stringify(body, null, 2), { mode: 0o600 }); |
There was a problem hiding this comment.
Suggestion: Writing the refresh token directly to the destination file is non-atomic; interruption or concurrent writes can leave a truncated/invalid JSON token file and break future startups. Persist to a temporary file and rename atomically to avoid partial-file corruption. [race condition]
Severity Level: Major ⚠️
- ❌ Token rotation persistence can leave a corrupted credential file.
- ❌ Main service in `src/index.ts` may fail to start afterward.
- ❌ CLI and verify scripts also break on corrupted token JSON.Steps of Reproduction ✅
1. Run the long-lived recorder service via `main()` in `src/index.ts:12-45`, which creates
a `RingApi` instance by calling `createRingApi(cfg)` (`src/index.ts:17` →
`src/ring.ts:47-65`), wiring `api.onRefreshTokenUpdated.subscribe(...)` at
`src/ring.ts:59-63`.
2. Allow the service to run until `RingApi` rotates the refresh token (per the documented
behavior in the comment at `src/ring.ts:40-45`), causing the `onRefreshTokenUpdated`
subscription to invoke `writeToken(cfg, newRefreshToken)` at `src/ring.ts:62`.
3. During `writeToken` (`src/ring.ts:29-37`), the call to `writeFileSync(cfg.tokenPath,
JSON.stringify(body, null, 2), { mode: 0o600 });` at line 31 writes JSON directly to the
final token path; if the process is terminated abruptly (e.g., `kill -9`, crash, power
loss) or a second process (e.g., `npm run verify` in `src/verify.ts:23-29`) accesses the
same `cfg.tokenPath` concurrently, the file can be left truncated or partially written.
4. On the next startup of any entrypoint that uses `createRingApi(cfg)` (service in
`src/index.ts`, CLI in `src/cli.ts:19-20`, or verify harness in `src/verify.ts:27-29`),
`readToken(cfg)` at `src/ring.ts:17-25` attempts `JSON.parse(readFileSync(cfg.tokenPath,
'utf8'))`; if the token file is corrupted by the non-atomic write, `JSON.parse` throws a
`SyntaxError`, preventing the recorder from authenticating until the operator reruns
`src/auth.ts:14-53` to regenerate a fresh token.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/ring.ts
**Line:** 31:31
**Comment:**
*Race Condition: Writing the refresh token directly to the destination file is non-atomic; interruption or concurrent writes can leave a truncated/invalid JSON token file and break future startups. Persist to a temporary file and rename atomically to avoid partial-file corruption.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| if (process.env.RING_TOKEN_PATH) merged.tokenPath = process.env.RING_TOKEN_PATH; | ||
| if (process.env.RING_OUTPUT_DIR) merged.outputDir = process.env.RING_OUTPUT_DIR; | ||
| if (process.env.RING_CLIP_SECONDS) merged.clipLengthSeconds = num(process.env.RING_CLIP_SECONDS, merged.clipLengthSeconds); | ||
| if (process.env.RING_RETENTION_DAYS) merged.retentionDays = num(process.env.RING_RETENTION_DAYS, 0) || null; |
There was a problem hiding this comment.
Suggestion: RING_RETENTION_DAYS uses num(..., 0) || null, so any non-numeric env value (for example a typo like RING_RETENTION_DAYS=abc) is silently converted to null and disables retention instead of keeping the previous configured value. This can unexpectedly stop pruning and grow disk usage indefinitely; use the current merged.retentionDays as fallback or reject invalid env input. [logic error]
Severity Level: Major ⚠️
- ❌ Retention disabled by typo in RING_RETENTION_DAYS env.
- ❌ Long-running service accumulates clips without any pruning.
- ⚠️ Log only shows generic "Retention disabled" message.Steps of Reproduction ✅
1. Create a `config.json` at the project root
(`/workspace/ring-camera-recorder/config.json`) with a positive retention value, e.g. `{
"retentionDays": 7 }`, which loadConfig reads and merges at `src/config.ts:63-68`.
2. Start the long-running service entrypoint at `src/index.ts:12-33` (e.g. `node
dist/index.js`) with an invalid environment override, such as `RING_RETENTION_DAYS=abc`,
so `process.env.RING_RETENTION_DAYS` is a non-numeric string.
3. In `loadConfig()` at `src/config.ts:60-87`, the environment override block at line 79
executes: `num('abc', 0)` returns `0` (fallback), and `0 || null` sets
`merged.retentionDays` to `null`, overwriting the `7` from `config.json` without error or
warning.
4. Still in `index.ts`, `startRetention(cfg)` is called at line 30; inside
`startRetention` at `src/retention.ts:9-13`, the condition `if (!cfg.retentionDays ||
cfg.retentionDays <= 0)` treats `null` as falsy, logs `Retention disabled (keeping all
clips).` and returns a no-op, so `pruneOldClips()` at `src/files.ts:39-64` is never
invoked and old recordings are never pruned, allowing unbounded disk growth.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/config.ts
**Line:** 79:79
**Comment:**
*Logic Error: `RING_RETENTION_DAYS` uses `num(..., 0) || null`, so any non-numeric env value (for example a typo like `RING_RETENTION_DAYS=abc`) is silently converted to `null` and disables retention instead of keeping the previous configured value. This can unexpectedly stop pruning and grow disk usage indefinitely; use the current `merged.retentionDays` as fallback or reject invalid env input.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| } | ||
|
|
||
| function validate(c: AppConfig): void { | ||
| if (c.clipLengthSeconds <= 0) throw new Error('clipLengthSeconds must be > 0'); |
There was a problem hiding this comment.
Suggestion: The clip length validation only checks <= 0 and does not verify the value is actually numeric/finite, so a malformed config value like "clipLengthSeconds": "abc" passes validation and later propagates as NaN into recording logic (-t NaN, invalid timeout math). Enforce Number.isFinite (and numeric type) during validation. [type error]
Severity Level: Major ⚠️
- ❌ Motion-triggered recordings fail when clip length misconfigured.
- ❌ Verify harness can pass NaN/invalid duration to ffmpeg.
- ⚠️ Error messages include NaN timeouts, confusing diagnosis.Steps of Reproduction ✅
1. Create `config.json` at the project root with a non-numeric clip length, e.g. `{
"clipLengthSeconds": "abc", "recordOnMotion": true }`, which is merged into the config in
`loadConfig()` at `src/config.ts:63-68`.
2. When `loadConfig()` calls `validate(merged)` at `src/config.ts:85`, the only check for
this field is `if (c.clipLengthSeconds <= 0)` at line 95; the comparison `"abc" <= 0`
coerces to `NaN <= 0` which is `false`, so validation passes and `clipLengthSeconds`
remains the string `"abc"`.
3. Start the main service at `src/index.ts:12-33` so it loads this config and begins
watching cameras; on a motion or ding event, `watchCamera()` at `src/events.ts:39-52`
calls `recordFn(camera, cfg, cfg.clipLengthSeconds)` at line 52, passing `"abc"` as the
`seconds` argument.
4. In `recordClip()` at `src/recorder.ts:42-58`, `seconds` is treated as numeric:
`clipOutputArgs(seconds, outPath)` builds an ffmpeg command with `'-t', String(seconds)`
producing `-t abc`, and `const hardTimeoutMs = (seconds + 30) * 1000` at line 57 yields
`NaN`, causing `withTimeout()` to use an immediate timeout; the recording either fails
quickly due to the invalid `-t` value or times out immediately, logging a failure in
`watchCamera()` at `src/events.ts:52-56` and producing no usable clip.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/config.ts
**Line:** 95:95
**Comment:**
*Type Error: The clip length validation only checks `<= 0` and does not verify the value is actually numeric/finite, so a malformed config value like `"clipLengthSeconds": "abc"` passes validation and later propagates as `NaN` into recording logic (`-t NaN`, invalid timeout math). Enforce `Number.isFinite` (and numeric type) during validation.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| const ts = when.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '-'); | ||
| return `${slug}_${ts}.mp4`; |
There was a problem hiding this comment.
Suggestion: The filename timestamp strips milliseconds, so two recordings from the same camera started within the same second will generate the exact same path and one clip can overwrite the other. Keep sub-second precision (or add a unique suffix) to prevent silent data loss on near-simultaneous recordings. [logic error]
Severity Level: Major ⚠️
- ❌ Concurrent recordings may overwrite earlier clip for same camera.
- ⚠️ Verify harness parallel runs can drop diagnostic clips.Steps of Reproduction ✅
1. Invoke the manual recording CLI twice in parallel for the same camera by running
`ring-recorder record --camera "Front Door" --seconds 30` from two terminals; both
executions enter `main()` in `src/cli.ts:11-54`, which loads config and calls
`recordClip()` at `src/cli.ts:51`.
2. Each CLI process calls `recordClip()` in `src/recorder.ts:42-85`, which computes
`startedAt = new Date()` (line 48) and then `const filename = clipFilename(camera.name,
startedAt);` (line 49).
3. `clipFilename()` in `src/files.ts:12-15` builds the name by calling
`when.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '-')` (line 14), explicitly
stripping milliseconds, so both processes started within the same second compute an
identical timestamp string and thus the same `${slug}_${ts}.mp4` filename (line 15).
4. Both ffmpeg processes in `recordClip()` write to the same `outPath` (joined in
`src/recorder.ts:50`), so the second completion silently overwrites or corrupts the first
recording file for that camera; this cannot occur for auto-triggered recordings (debounced
by `watchCamera()` in `src/events.ts:39-56`), but is reproducible with concurrent
CLI/verify processes.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/files.ts
**Line:** 14:15
**Comment:**
*Logic Error: The filename timestamp strips milliseconds, so two recordings from the same camera started within the same second will generate the exact same path and one clip can overwrite the other. Keep sub-second precision (or add a unique suffix) to prevent silent data loss on near-simultaneous recordings.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| try { | ||
| entries = readdirSync(dir); | ||
| } catch { | ||
| return []; // dir not created yet — nothing to prune |
There was a problem hiding this comment.
Suggestion: This catch block treats every directory read failure as "nothing to prune", which silently hides real runtime failures like permission errors or I/O faults and disables retention without warning. Only ignore expected not-found cases and surface other errors. [possible bug]
Severity Level: Critical 🚨
- ❌ Retention sweep silently disabled on directory permission failures.
- ⚠️ Old clips accumulate despite retentionDays configured.Steps of Reproduction ✅
1. Enable retention by setting a positive `retentionDays` in config (validated in
`src/config.ts:94-101`) so that `startRetention()` in `src/retention.ts:9-27` is used
instead of exiting early at lines 10-13.
2. Misconfigure `outputDir` (e.g., point it at a directory without read permission for the
service user) in `config.json`, which is loaded and merged in `src/config.ts:60-84`, then
passed into `startRetention(cfg)` at `src/index.ts:30`.
3. On service startup, `startRetention()` defines `sweep()` at `src/retention.ts:15-20`
and immediately calls it at line 22; `sweep()` calls `pruneOldClips(cfg.outputDir,
cfg.retentionDays, new Date())` in `src/files.ts:39`.
4. Inside `pruneOldClips()` (`src/files.ts:39-65`), `readdirSync(dir)` at line 46 throws
(e.g., `EACCES` or other I/O error), the broad `catch { return []; }` at lines 45-48
treats this as "dir not created yet — nothing to prune", causing the sweep to silently do
nothing; subsequent sweeps behave the same, so configured retention is effectively
disabled without any log indication from `startRetention()` beyond the initial "Retention:
deleting clips older than…" message.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/files.ts
**Line:** 45:48
**Comment:**
*Possible Bug: This catch block treats every directory read failure as "nothing to prune", which silently hides real runtime failures like permission errors or I/O faults and disables retention without warning. Only ignore expected not-found cases and surface other errors.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| } catch { | ||
| // File vanished mid-sweep (e.g. a concurrent record finished/rotated). Skip. | ||
| } |
There was a problem hiding this comment.
Suggestion: The inner catch suppresses all stat/unlink failures even though the comment only describes racey file disappearance; permission or filesystem errors will be silently ignored and old files will remain indefinitely. Restrict this to expected transient errors and report the rest. [possible bug]
Severity Level: Critical 🚨
- ❌ Some expired clips never deleted under filesystem errors.
- ⚠️ Disk usage grows silently when deletions repeatedly fail.Steps of Reproduction ✅
1. Configure retention with `retentionDays > 0` (as in `src/config.ts:31-36` and enforced
in `src/retention.ts:9-13`) so that `startRetention()` schedules periodic calls to
`pruneOldClips()` via the `sweep` function at `src/retention.ts:15-24`.
2. Create an output directory with a mix of `.mp4` clip files older than the cutoff and at
least one file that the process cannot fully stat or delete (e.g., change its permissions
or ownership after creation); `pruneOldClips()` will be invoked with this directory from
`startRetention()` at `src/retention.ts:16`.
3. During `pruneOldClips()` (`src/files.ts:51-63`), for each `.mp4` entry it runs
`statSync(full)` (line 55) and, if old enough, `unlinkSync(full)` (line 57); any error
from either call (permission denied, transient filesystem error, I/O fault) is caught by
the blanket `catch { ... }` at lines 60-62, which is documented only for "File vanished
mid-sweep".
4. Because the catch block is empty, failures to delete old clips are silently ignored and
the offending files remain on disk indefinitely while `startRetention()` still logs only
successful deletions (via the `deleted` array) at `src/retention.ts:17-19`, masking
retention failures and allowing disk usage to grow unexpectedly.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/files.ts
**Line:** 60:62
**Comment:**
*Possible Bug: The inner catch suppresses all stat/unlink failures even though the comment only describes racey file disappearance; permission or filesystem errors will be silently ignored and old files will remain indefinitely. Restrict this to expected transient errors and report the rest.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix- Atomic token write + create parent dir on rotation (no corrupt token file) - Clear error when the token file is unreadable or invalid JSON - Validate numeric config is finite; require retentionSweepMinutes > 0 - Ignore non-numeric RING_RETENTION_DAYS instead of silently disabling retention - Preserve the original auth error when the failure is not a 2FA prompt - Logger tolerates circular-reference objects (no crash from catch blocks) - verify: record once and unsubscribe on the first trigger - Filenames keep milliseconds to avoid same-second overwrite - Retention sweep surfaces real I/O errors instead of swallowing them
|
Thanks to the review bots. Addressed the actionable findings in f4c1bca:
Hermetic tests pass locally (15/15); CI is re-running on the new commit. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/index.ts (1)
33-42: ⚡ Quick winShutdown handler can be invoked twice if signals arrive in quick succession.
If both SIGINT and SIGTERM are received within the 1500ms delay window,
shutdown()runs twice—callingapi.disconnect()multiple times, which may throw or behave unexpectedly depending on internal state.🛡️ Proposed guard to prevent reentrancy
+ let shuttingDown = false; const shutdown = (signal: string) => { + if (shuttingDown) return; + shuttingDown = true; log.info(`Received ${signal}, shutting down.`); for (const s of subs) s.unsubscribe(); stopRetention(); api.disconnect(); // Give in-flight ffmpeg writes a moment to flush before exit. setTimeout(() => process.exit(0), 1500); };🤖 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/index.ts` around lines 33 - 42, The shutdown function can execute multiple times if SIGINT and SIGTERM signals arrive in quick succession before the process exits. Add a guard flag at the module level that tracks whether shutdown has already been initiated. In the shutdown function, check this flag at the beginning and return early if shutdown has already started. Set the flag to true at the start of the shutdown function to prevent subsequent calls from re-executing the cleanup logic (unsubscribe, stopRetention, api.disconnect, etc.).src/verify.ts (1)
98-126: 💤 Low valuePotential for overlapping recordings if events fire in quick succession.
onTriggerstarts an asyncrecordClipwithout immediately settingdone = true. If motion and doorbell events fire within milliseconds, two recordings may start before the first completes and setsdone. This is minor for a verification harness but could be guarded by settingdone = trueat the start ofonTrigger.♻️ Optional guard to prevent duplicate recordings
const onTrigger = (reason: string) => { + if (done) return; + done = true; // Prevent a second event from starting another recording log.info(` → ${reason} detected; recording a clip to prove the trigger path…`); recordClip(camera, cfg, Math.min(cfg.clipLengthSeconds, 10)) .then(() => finish(true)) .catch((e) => { log.error(` recording after trigger failed: ${(e as Error).message}`); finish(false); }); };🤖 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/verify.ts` around lines 98 - 126, In the `waitForTriggeredRecording` function, the `onTrigger` callback can be invoked multiple times (from both motion and doorbell events) before the first async `recordClip` call completes, causing duplicate recordings to start. Fix this by setting `done = true` at the very beginning of the `onTrigger` function, before calling `recordClip`, so that subsequent trigger events are blocked by the guard check in the `finish` function.
🤖 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/ci.yml:
- Line 12: The GitHub Actions workflow is using version tag references (such as
`@v4`) for the actions/checkout action, which creates a supply-chain security
vulnerability by allowing GitHub to auto-update to any version. Replace the `@v4`
tag reference with the full commit SHA for actions/checkout. Visit the release
page for the version being used
(https://github.com/actions/checkout/releases/tag/v4) and copy the commit hash
from the "Use this version" section, then update the uses clause to use the
format actions/checkout@<commit-sha>. Apply this same pinning pattern to any
other GitHub Actions referenced in the workflow file that use version tags.
- Line 12: The actions/checkout@v4 action is not explicitly disabling credential
persistence, which poses a security risk if the build environment is
compromised. Add the `persist-credentials: false` parameter to the
actions/checkout@v4 step in the workflow to explicitly prevent GitHub
credentials from being persisted in the runner environment.
In `@src/events.ts`:
- Around line 40-64: The motion detection subscription is triggering on every
true emission from onMotionDetected instead of only on rising-edge transitions
(false to true). To fix this, add state tracking for the previous motion state
(either as a new property in the CameraState object or as a local variable in
the setupCamera function). Then modify the onMotionDetected subscription handler
to check if the current active state is true AND the previous motion state was
false before calling trigger('motion'), and always update the tracked motion
state after each emission so future comparisons are correct.
In `@src/verify.ts`:
- Around line 23-31: The main function uses Number() to convert CLI arguments
for seconds and watch-motion, but this can result in NaN if the user provides
non-numeric values like --seconds foo. Add validation after the Number()
conversions to check that the parsed values are valid numbers (not NaN), and
either throw an error with a helpful message or use a default fallback value.
Apply this validation to both the seconds assignment and the watchMotion
assignment to ensure these critical timeout parameters are always valid numbers.
---
Nitpick comments:
In `@src/index.ts`:
- Around line 33-42: The shutdown function can execute multiple times if SIGINT
and SIGTERM signals arrive in quick succession before the process exits. Add a
guard flag at the module level that tracks whether shutdown has already been
initiated. In the shutdown function, check this flag at the beginning and return
early if shutdown has already started. Set the flag to true at the start of the
shutdown function to prevent subsequent calls from re-executing the cleanup
logic (unsubscribe, stopRetention, api.disconnect, etc.).
In `@src/verify.ts`:
- Around line 98-126: In the `waitForTriggeredRecording` function, the
`onTrigger` callback can be invoked multiple times (from both motion and
doorbell events) before the first async `recordClip` call completes, causing
duplicate recordings to start. Fix this by setting `done = true` at the very
beginning of the `onTrigger` function, before calling `recordClip`, so that
subsequent trigger events are blocked by the guard check in the `finish`
function.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e0d3e700-948f-4f69-8eed-4aa4b8d59d20
⛔ Files ignored due to path filters (2)
assets/hero.jpgis excluded by!**/*.jpgpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (23)
.github/workflows/ci.yml.gitignoreLICENSENOTICEREADME.mdSECURITY.mdconfig.example.jsondeploy/ecosystem.config.cjsdeploy/ring-camera-recorder.servicepackage.jsonsrc/auth.tssrc/cli.tssrc/config.tssrc/events.tssrc/files.tssrc/index.tssrc/log.tssrc/recorder.tssrc/retention.tssrc/ring.tssrc/verify.tstest/logic.test.mjstsconfig.json
|
@gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a self-hosted local recorder for Ring cameras, featuring automated event-triggered recording, configuration layering, and retention pruning. The code review highlights several important improvement opportunities: adding a connection timeout to camera.streamVideo to prevent indefinite hangs, wrapping the trigger execution in a resolved promise to safely catch synchronous errors, refactoring the synchronous file-pruning operations to asynchronous APIs to avoid blocking the event loop, and updating the TypeScript module resolution to nodenext to align with Node.js ESM runtime behavior.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const hardTimeoutMs = (seconds + 30) * 1000; | ||
| const session = await camera.streamVideo({ output: clipOutputArgs(seconds, outPath) }); | ||
| try { | ||
| await withTimeout( | ||
| firstValueFrom(session.onCallEnded), | ||
| hardTimeoutMs, | ||
| `live call for "${camera.name}" exceeded ${hardTimeoutMs / 1000}s`, | ||
| ); | ||
| } catch (err) { | ||
| session.stop(); // ensure the WebRTC session + ffmpeg are torn down on timeout | ||
| throw err; | ||
| } |
There was a problem hiding this comment.
Issue: The call to camera.streamVideo is executed without a timeout. If the WebRTC connection or signaling hangs indefinitely (a common issue with flaky networks and unofficial APIs), the entire recording process will hang. This keeps the trigger lock (state.recording = true) active forever, permanently preventing any future recordings until the service is restarted.\n\nRecommendation: Wrap the camera.streamVideo call in withTimeout to ensure connection establishment is capped (e.g., at 30 seconds).
const hardTimeoutMs = (seconds + 30) * 1000;\n const connectionTimeoutMs = 30000;\n const session = await withTimeout(\n camera.streamVideo({ output: clipOutputArgs(seconds, outPath) }),\n connectionTimeoutMs,\n `Establishing live stream session for "${camera.name}" timed out after 30s`,\n );\n\n try {\n await withTimeout(\n firstValueFrom(session.onCallEnded),\n hardTimeoutMs,\n `live call for "${camera.name}" exceeded ${hardTimeoutMs / 1000}s`,\n );\n } catch (err) {\n session.stop(); // ensure the WebRTC session + ffmpeg are torn down on timeout\n throw err;\n }| recordFn(camera, cfg, cfg.clipLengthSeconds) | ||
| .catch((err) => log.error(`Recording failed for "${camera.name}": ${(err as Error).message}`)) | ||
| .finally(() => { | ||
| state.recording = false; | ||
| }); |
There was a problem hiding this comment.
Issue: If recordFn throws an error synchronously (before returning a Promise), the synchronous exception will bypass the .catch and .finally blocks. This will leave state.recording as true forever, permanently blocking any future recordings for that camera.\n\nRecommendation: Wrap the call in Promise.resolve().then(...) to guarantee that any synchronous throw is safely caught and handled as a promise rejection.
Promise.resolve()\n .then(() => recordFn(camera, cfg, cfg.clipLengthSeconds))\n .catch((err) => log.error(`Recording failed for "${camera.name}": ${(err as Error).message}`))\n .finally(() => {\n state.recording = false;\n });| import { mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'; | ||
| import { join } from 'node:path'; |
| export function pruneOldClips(dir: string, retentionDays: number | null, now: Date): string[] { | ||
| if (!retentionDays || retentionDays <= 0) return []; | ||
| const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1000; | ||
| const deleted: string[] = []; | ||
|
|
||
| let entries: string[]; | ||
| try { | ||
| entries = readdirSync(dir); | ||
| } catch (err) { | ||
| // Output dir not created yet is fine; surface real failures (permissions, | ||
| // I/O) instead of silently disabling retention. | ||
| if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []; | ||
| throw err; | ||
| } | ||
|
|
||
| for (const entry of entries) { | ||
| if (!entry.endsWith('.mp4')) continue; | ||
| const full = join(dir, entry); | ||
| try { | ||
| const st = statSync(full); | ||
| if (st.isFile() && st.mtimeMs < cutoff) { | ||
| unlinkSync(full); | ||
| deleted.push(full); | ||
| } | ||
| } catch { | ||
| // File vanished mid-sweep (e.g. a concurrent record finished/rotated). Skip. | ||
| } | ||
| } | ||
| return deleted; | ||
| } |
There was a problem hiding this comment.
Issue: pruneOldClips uses synchronous file system methods (readdirSync, statSync, unlinkSync) inside a setInterval loop. Since this is a real-time service that must respond to push notifications and events immediately, blocking the event loop with synchronous I/O can cause the service to miss incoming events or drop connections.\n\nRecommendation: Refactor pruneOldClips to use asynchronous file system APIs (fs.promises).
export async function pruneOldClips(dir: string, retentionDays: number | null, now: Date): Promise<string[]> {\n if (!retentionDays || retentionDays <= 0) return [];\n const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;\n const deleted: string[] = [];\n\n let entries: string[];\n try {\n entries = await fs.readdir(dir);\n } catch (err) {\n // Output dir not created yet is fine; surface real failures (permissions,\n // I/O) instead of silently disabling retention.\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];\n throw err;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith('.mp4')) continue;\n const full = join(dir, entry);\n try {\n const st = await fs.stat(full);\n if (st.isFile() && st.mtimeMs < cutoff) {\n await fs.unlink(full);\n deleted.push(full);\n }\n } catch {\n // File vanished mid-sweep (e.g. a concurrent record finished/rotated). Skip.\n }\n }\n return deleted;\n}| const sweep = () => { | ||
| try { | ||
| const deleted = pruneOldClips(cfg.outputDir, cfg.retentionDays, new Date()); | ||
| if (deleted.length) { | ||
| log.info(`Retention: deleted ${deleted.length} clip(s) older than ${cfg.retentionDays}d.`); | ||
| } | ||
| } catch (err) { | ||
| log.warn(`Retention sweep failed: ${(err as Error).message}`); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Recommendation: Update the sweep function to await the asynchronous pruneOldClips call.
const sweep = async () => {\n try {\n const deleted = await pruneOldClips(cfg.outputDir, cfg.retentionDays, new Date());\n if (deleted.length) {\n log.info(`Retention: deleted ${deleted.length} clip(s) older than ${cfg.retentionDays}d.`);\n }\n } catch (err) {\n log.warn(`Retention sweep failed: ${(err as Error).message}`);\n }\n };| writeFileSync(join(dir, 'notes.txt'), 'x'); | ||
| const tenDaysAgo = Date.now() / 1000 - 10 * 86400; | ||
| utimesSync(old, tenDaysAgo, tenDaysAgo); | ||
| const deleted = pruneOldClips(dir, 7, new Date()); |
| "module": "ES2022", | ||
| "moduleResolution": "bundler", |
There was a problem hiding this comment.
Issue: "moduleResolution": "bundler" is used for a project that runs directly in Node.js (via node dist/index.js). For Node.js ESM projects, "moduleResolution": "nodenext" (or "node16") is the correct and standard setting to ensure TypeScript's module resolution matches Node's runtime behavior.\n\nRecommendation: Change "module" and "moduleResolution" to "nodenext".
| "module": "ES2022", | |
| "moduleResolution": "bundler", | |
| "module": "nodenext",\n "moduleResolution": "nodenext", |
- events: gate motion on the false->true rising edge (matches documented intent; sustained 'true' emissions no longer queue extra clips). CodeRabbit. - retention: per-file sweep surfaces non-ENOENT errors instead of swallowing all. CodeAnt (critical). - ci: pin actions/checkout and actions/setup-node to commit SHAs. CodeRabbit. - verify: validate numeric CLI args (--seconds / --watch-motion) to reject NaN. CodeRabbit. - tests updated for rising-edge semantics (15/15 pass).
|
Round 2 addressed in the latest commit:
Note: the re-posted comments on |
- recorder: add a start timeout around camera.streamVideo() so a hang during WebRTC signaling can't block forever (the onCallEnded timeout only covered the call after the session existed). gemini (high). - events: dispatch recordFn via Promise.resolve().then() so a synchronous throw becomes a rejection and never leaves state.recording stuck true. gemini.
|
Round 3: Fixed:
Intentionally not changing:
Stale re-posts (already fixed in earlier commits, re-surfaced by full-file re-scan): |
tsconfig used moduleResolution 'bundler' (intended for bundlers). This project is Node ESM run via `node dist/`, so 'nodenext' matches the runtime. Relative imports already use explicit .js extensions, so the switch builds cleanly. gemini.
|
Applied the one new actionable item from this pass:
Everything else on the latest commit is either already fixed in an earlier commit (re-surfaced by the bots' full-file re-scans) or intentionally declined:
This should be the steady state: remaining bot comments are re-scans of resolved items, not new findings. |
User description
Summary
Initial codebase for Ring Camera Recorder: a self-hosted tool that records the live view from Ring cameras you own to local disk, without a Ring Protect subscription. It captures the freely available live stream (not Ring's paid cloud storage), either on demand or automatically when a motion/doorbell event fires.
What's included
list/record/verify)Verification
Verified against a real account during development: authenticated, listed the camera, captured playable clips (video + audio confirmed via ffprobe), and confirmed a motion event auto-records end to end.
npm testpasses 15/15 hermetic checks.Notes for reviewers
ring-client-api@14.3.0. It uses Ring's unofficial API, which can change without notice (see SECURITY.md).npm auditreports a transitive WebRTC advisory (ipviawerift) that cannot be resolved without downgrading to a broken version. It is documented in SECURITY.md; do not runnpm audit fix --force.Test plan
npm run buildnpm test🤖 Generated with Claude Code
CodeAnt-AI Description
Add a self-hosted Ring camera recorder with login, clip capture, and auto-triggered recording
What Changed
Impact
✅ Shorter setup for local Ring recording✅ Fewer missed motion events✅ Playable clips after interrupted recordings💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.
Summary by CodeRabbit
New Features
Documentation
Tests
Chores