feat: add ElevenLabs Scribe support, fix noise-injected transcriptions, improve retry UX - #167
Conversation
…s, improve retry UX - Add ElevenLabs STT provider (scribe_v1) with tag_audio_events: false to prevent sound descriptions like "(Clicking noise)" from appearing in text - Keep Groq as fallback provider via STT_PROVIDER env var - Add sanitizeTranscription() regex as provider-agnostic fallback for remaining parenthetical noise labels - Return "" instead of throwing on no-speech, enabling clean retry flow - Show persistent "No speech detected — press voice key to retry" message (no longer auto-clears after 3s) - Dismiss footer message on any typed character - Switch primary voice key from F2 to Ctrl+V (more ergonomic), keep F2 as fallback - Remove console.error stack traces from voice failure paths - Clean up abortCapture → stopCapture naming, remove unused user-abort flag
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 49 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?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 reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. 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, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
WalkthroughThis PR adds multi-provider voice speech-to-text support (ElevenLabs/Groq) to the CLI chat with a new Ctrl+V toggle, refactors capture stop logic, and introduces a new ChangesVoice Capture and STT Provider Support
Estimated code review effort: 3 (Moderate) | ~30 minutes Infisical Secrets Package
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ChatCLI
participant Speech as speech.ts
participant STTProvider as ElevenLabs/Groq
User->>ChatCLI: Press Ctrl+V
ChatCLI->>Speech: startVoiceCapture()
Speech->>Speech: canVoiceCapture()
Speech->>Speech: captureAudio() via ffmpeg
User->>ChatCLI: Press Ctrl+V / Enter / Escape
ChatCLI->>Speech: stopCapture()
Speech->>STTProvider: transcribeAudio(filePath)
STTProvider-->>Speech: transcription text
Speech->>Speech: sanitizeTranscription()
Speech-->>ChatCLI: cleaned text
ChatCLI-->>User: insert text into input
sequenceDiagram
participant Caller
participant Index as secrets/src/index.ts
participant Client as client.ts
participant Infisical as Infisical API
participant Apply as apply.ts
Caller->>Index: loadSecrets(options)
Index->>Client: getClient(app)
Client->>Client: resolveToken(app)
Client->>Infisical: create InfisicalClient
Index->>Infisical: listSecrets(app, env, projectId)
Infisical-->>Index: secrets list
Index->>Apply: applySecrets(flat, app, env, schema)
Apply-->>Caller: process.env populated
Related Issues: Not specified in the provided information. Related PRs: Not specified in the provided information. Suggested labels: feature, documentation, cli Suggested reviewers: yashdev9274 Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/supercode-cli/server/src/voice/speech.ts (1)
91-127: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUnify the short-recording no-speech path
captureAudio()still rejects recordings under 100 bytes, so the manual-stop/no-speech flow falls into the genericVoice failedbranch instead of the intended retry message. Return an empty result there and treat it as no speech invoiceCaptureFlow().🤖 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 `@apps/supercode-cli/server/src/voice/speech.ts` around lines 91 - 127, The short-recording path in captureAudio() still rejects when data.length is under 100 bytes, which prevents the manual-stop/no-speech flow from reaching the intended retry handling. Change that branch to return an empty result instead of rejecting, then update voiceCaptureFlow() to recognize the empty/no-speech result and route it through the no speech retry message rather than the generic Voice failed path.
🧹 Nitpick comments (4)
packages/secrets/src/index.ts (1)
17-17: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDefault project ID captured at module-load time.
DEFAULT_PROJECT_IDis resolved once when the module is first imported. IfINFISICAL_PROJECT_IDis set later (e.g., by a lazy.envload or another init step beforeloadSecretsis actually called), this stale value (undefined) will be used for every call.♻️ Proposed fix: resolve lazily inside `loadSecrets`
-const DEFAULT_PROJECT_ID = process.env.INFISICAL_PROJECT_ID - export async function loadSecrets(options: LoadSecretsOptions): Promise<void> { - const { app, env, schema, projectId = DEFAULT_PROJECT_ID } = options + const { app, env, schema, projectId = process.env.INFISICAL_PROJECT_ID } = options🤖 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/secrets/src/index.ts` at line 17, The module-level DEFAULT_PROJECT_ID in the secrets index is captured too early and can stay stale if INFISICAL_PROJECT_ID is populated later. Move the project ID lookup into loadSecrets so it is resolved at call time, and use that live value wherever DEFAULT_PROJECT_ID is currently referenced. Keep the change localized around loadSecrets and any helper logic that consumes the project ID.packages/secrets/src/apply.ts (2)
3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueType-only circular import between
apply.tsandindex.ts.
apply.tsimportsAppId/EnvNamefrom./index, whileindex.tsimportsapplySecretsfrom./apply. Type-only imports are erased so this is safe at runtime, but consider hoistingAppId/EnvNameintoresolve-token.tsor a dedicatedtypes.tsto avoid the circular reference entirely.🤖 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/secrets/src/apply.ts` at line 3, The `apply.ts` and `index.ts` modules currently form a type-only circular reference through `AppId` and `EnvName`, so move those shared types out of `./index` into `resolve-token.ts` or a new dedicated `types.ts`. Update `apply.ts` to import the types from the new location, and adjust any related imports in `index.ts` so `applySecrets` no longer depends on `index` for type definitions. Keep the runtime behavior unchanged while removing the circular module dependency.
21-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider surfacing skipped/overridden secrets for observability.
Secrets whose keys already exist in
process.envare silently skipped. This can be intentional (let explicit env override secrets), but with no logging, it makes debugging "my secret didn't apply" hard.🤖 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/secrets/src/apply.ts` around lines 21 - 25, The applySecrets logic silently skips secrets whose keys already exist in process.env, so add observability in the apply.ts loop to surface when a secret is ignored because an env var is already set. Update the existing iteration over Object.entries(secrets) to log or otherwise record the skipped key in a clear way, while preserving the current override behavior for process.env and keeping the applySecrets function as the central place for this decision.apps/supercode-cli/server/src/voice/__tests__/speech.test.ts (1)
1-84: 📐 Maintainability & Code Quality | 🔵 TrivialGood coverage for
canVoiceCapture/getSttProvider; missing coverage for the new transcription/sanitization behavior.Tests thoroughly cover provider-gating and default-provider selection, matching the implementation. Consider adding coverage for
sanitizeTranscription()(regex stripping) and the no-speech-detected empty-string path invoiceCaptureFlow()/captureAudio(), since those are core new behaviors introduced by this PR and are currently untested.🤖 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 `@apps/supercode-cli/server/src/voice/__tests__/speech.test.ts` around lines 1 - 84, Add tests covering the new transcription sanitization and empty-result handling in the voice flow. Extend the existing speech tests to verify `sanitizeTranscription()` strips the regex-marked text correctly, and add a case for the no-speech-detected path in `voiceCaptureFlow()` or `captureAudio()` that returns an empty string without failing. Use the existing `speech.ts` exports and the `canVoiceCapture`/`getSttProvider` test file as the place to locate and mirror the new 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 `@AGENTS.md`:
- Line 113: The Voice/STT environment guidance is missing the Groq credential
even though speech.ts supports the groq path via STT_PROVIDER and requires
GROQ_API_KEY. Update the env list in AGENTS.md alongside ELEVENLABS_API_KEY,
STT_PROVIDER, ELEVENLABS_MODEL, and STT_LANGUAGE to include GROQ_API_KEY so the
fallback provider is clearly documented.
In `@apps/supercode-cli/server/README.md`:
- Line 57: The README shortcut documentation is out of sync with the actual chat
key binding in chat.ts. Update the voice capture shortcut text to match the CLI
binding used by the chat trigger logic, which uses Ctrl+V as the primary
shortcut with F2 as the fallback, so users are instructed to press the correct
keys.
In `@apps/supercode-cli/server/src/cli/ai/chat/chat.ts`:
- Around line 1052-1059: The voice capture success path in chat.ts leaves the
“Recording...” footer stale after transcription completes. In the voice capture
handling around voiceCaptureFlow(), clear or reset activeFooter’s status message
when text is returned, before or after updating stdinInput/stdinCursor, so the
footer no longer suggests recording is still active; keep the existing “No
speech detected” message only for the empty-text branch.
In `@apps/supercode-cli/server/src/voice/speech.ts`:
- Around line 11-13: `STT_LANGUAGE` is defined in `speech.ts` but never used, so
both transcription paths ignore the configured language. Update the ElevenLabs
and Groq request builders in `speech.ts` to forward this value: pass
`language_code` in the ElevenLabs transcription call and `language` in the Groq
transcription call, using `STT_LANGUAGE` as the source. Keep the changes
localized to the transcription request logic so the existing `ELEVENLABS_URL`,
`ELEVENLABS_MODEL`, and related helpers continue to work unchanged.
In `@packages/secrets/src/client.ts`:
- Around line 5-16: The module-level singleton in getClient currently caches a
single InfisicalClient in shared, so later calls with a different app reuse the
first app’s token. Update the caching logic in getClient and the shared state to
be keyed by AppId instead of one global instance, so each app gets its own
client from resolveToken(app) while still reusing clients per app. Keep the
lookup/creation logic localized to getClient and the shared cache declaration.
- Line 1: Update the Infisical client initialization in client.ts to use the
supported machine-identity auth shape for `@infisical/sdk` v0.0.30. The issue is
that the current client setup is using the legacy token-based shape, which does
not match the SDK’s expected credentials. Fix the InfisicalClient construction
to pass clientId and clientSecret instead of token, and make sure the auth
object aligns with the new SDK API.
In `@packages/secrets/src/index.ts`:
- Around line 28-33: The async Infisical call in getClient/listSecrets is
missing defensive handling, so SDK failures can escape as raw rejections and a
hung request can block indefinitely. Wrap the client.listSecrets(...) await in
try-catch, handle/log the error in this function path, and add a timeout or
abort mechanism around the external call so secrets loading fails fast and
predictably.
In `@packages/secrets/src/resolve-token.ts`:
- Around line 12-22: The local session lookup in readLocalSessionToken is
reading ~/.infisical/infisical-config.json, but the CLI stores login state in
the system keyring with an encrypted file vault fallback, so this path will not
return a valid session token. Update readLocalSessionToken in resolve-token.ts
to query the CLI’s वास्तविक credential store instead of parsing
config.token/config.accessToken from that JSON file, and make sure the returned
value matches the token source used by the logged-in Infisical CLI session so
MissingInfisicalTokenError is not triggered for normal users.
---
Outside diff comments:
In `@apps/supercode-cli/server/src/voice/speech.ts`:
- Around line 91-127: The short-recording path in captureAudio() still rejects
when data.length is under 100 bytes, which prevents the manual-stop/no-speech
flow from reaching the intended retry handling. Change that branch to return an
empty result instead of rejecting, then update voiceCaptureFlow() to recognize
the empty/no-speech result and route it through the no speech retry message
rather than the generic Voice failed path.
---
Nitpick comments:
In `@apps/supercode-cli/server/src/voice/__tests__/speech.test.ts`:
- Around line 1-84: Add tests covering the new transcription sanitization and
empty-result handling in the voice flow. Extend the existing speech tests to
verify `sanitizeTranscription()` strips the regex-marked text correctly, and add
a case for the no-speech-detected path in `voiceCaptureFlow()` or
`captureAudio()` that returns an empty string without failing. Use the existing
`speech.ts` exports and the `canVoiceCapture`/`getSttProvider` test file as the
place to locate and mirror the new assertions.
In `@packages/secrets/src/apply.ts`:
- Line 3: The `apply.ts` and `index.ts` modules currently form a type-only
circular reference through `AppId` and `EnvName`, so move those shared types out
of `./index` into `resolve-token.ts` or a new dedicated `types.ts`. Update
`apply.ts` to import the types from the new location, and adjust any related
imports in `index.ts` so `applySecrets` no longer depends on `index` for type
definitions. Keep the runtime behavior unchanged while removing the circular
module dependency.
- Around line 21-25: The applySecrets logic silently skips secrets whose keys
already exist in process.env, so add observability in the apply.ts loop to
surface when a secret is ignored because an env var is already set. Update the
existing iteration over Object.entries(secrets) to log or otherwise record the
skipped key in a clear way, while preserving the current override behavior for
process.env and keeping the applySecrets function as the central place for this
decision.
In `@packages/secrets/src/index.ts`:
- Line 17: The module-level DEFAULT_PROJECT_ID in the secrets index is captured
too early and can stay stale if INFISICAL_PROJECT_ID is populated later. Move
the project ID lookup into loadSecrets so it is resolved at call time, and use
that live value wherever DEFAULT_PROJECT_ID is currently referenced. Keep the
change localized around loadSecrets and any helper logic that consumes the
project ID.
🪄 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: c32684f2-7c13-4b57-b762-56783da7d494
📒 Files selected for processing (15)
AGENTS.mdapps/supercode-cli/server/README.mdapps/supercode-cli/server/package.jsonapps/supercode-cli/server/src/cli/ai/chat/chat.tsapps/supercode-cli/server/src/voice/__tests__/speech.test.tsapps/supercode-cli/server/src/voice/speech.tsapps/web/.env.examplepackages/secrets/index.tspackages/secrets/package.jsonpackages/secrets/src/apply.tspackages/secrets/src/client.tspackages/secrets/src/errors.tspackages/secrets/src/index.tspackages/secrets/src/resolve-token.tspackages/secrets/tsconfig.json
| - Web app: `apps/web/.env` or `apps/.env.local` | ||
| - Required for auth: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `NEXT_PUBLIC_BETTER_AUTH_URL` | ||
| - Required for DB: `DATABASE_URL` | ||
| - Voice/STT: `ELEVENLABS_API_KEY` (default provider), `STT_PROVIDER` (`elevenlabs`|`groq`), `ELEVENLABS_MODEL`, `STT_LANGUAGE` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add the Groq key to the Voice/STT env list.
STT_PROVIDER=groq is supported, and speech.ts requires GROQ_API_KEY on that path, so leaving it out of the env guidance makes the fallback provider look unsupported.
🤖 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.md` at line 113, The Voice/STT environment guidance is missing the
Groq credential even though speech.ts supports the groq path via STT_PROVIDER
and requires GROQ_API_KEY. Update the env list in AGENTS.md alongside
ELEVENLABS_API_KEY, STT_PROVIDER, ELEVENLABS_MODEL, and STT_LANGUAGE to include
GROQ_API_KEY so the fallback provider is clearly documented.
| | `GROQ_API_KEY` | Groq API key (required when `STT_PROVIDER=groq`) | — | | ||
| | `STT_LANGUAGE` | Transcription language | `en` | | ||
|
|
||
| Press **Ctrl+Shift+V** during a chat session to start voice capture. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Match the documented shortcut to the CLI binding.
chat.ts now uses Ctrl+V as the primary trigger, with F2 as fallback, so documenting Ctrl+Shift+V points users at the wrong key combo.
🤖 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 `@apps/supercode-cli/server/README.md` at line 57, The README shortcut
documentation is out of sync with the actual chat key binding in chat.ts. Update
the voice capture shortcut text to match the CLI binding used by the chat
trigger logic, which uses Ctrl+V as the primary shortcut with F2 as the
fallback, so users are instructed to press the correct keys.
| const text = await voiceCaptureFlow() | ||
| if (text) { | ||
| stdinInput = | ||
| stdinInput.slice(0, stdinCursor) + text + " " + stdinInput.slice(stdinCursor) | ||
| stdinCursor += text.length + 1 | ||
| // Don't call renderInput here — the chat loop will call chatInput() next, | ||
| // which preserves stdinInput (via voiceJustCaptured) and renders it once. | ||
| } else { | ||
| activeFooter?.setStatusMessage("🎤 No speech detected — press voice key to retry") | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
"Recording..." footer message never cleared on successful transcription.
When voiceCaptureFlow() returns non-empty text, the code inserts it into stdinInput but never clears the earlier "🎤 Recording... (voice key or Enter to stop)" footer message. It lingers until the user types a character (line 1030) or another status update overwrites it, which can confuse the user into thinking recording is still active right after a successful capture.
🧹 Proposed fix
const text = await voiceCaptureFlow()
if (text) {
+ activeFooter?.setStatusMessage("")
stdinInput =
stdinInput.slice(0, stdinCursor) + text + " " + stdinInput.slice(stdinCursor)
stdinCursor += text.length + 1
} else {📝 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.
| const text = await voiceCaptureFlow() | |
| if (text) { | |
| stdinInput = | |
| stdinInput.slice(0, stdinCursor) + text + " " + stdinInput.slice(stdinCursor) | |
| stdinCursor += text.length + 1 | |
| // Don't call renderInput here — the chat loop will call chatInput() next, | |
| // which preserves stdinInput (via voiceJustCaptured) and renders it once. | |
| } else { | |
| activeFooter?.setStatusMessage("🎤 No speech detected — press voice key to retry") | |
| } | |
| const text = await voiceCaptureFlow() | |
| if (text) { | |
| activeFooter?.setStatusMessage("") | |
| stdinInput = | |
| stdinInput.slice(0, stdinCursor) + text + " " + stdinInput.slice(stdinCursor) | |
| stdinCursor += text.length + 1 | |
| } else { | |
| activeFooter?.setStatusMessage("🎤 No speech detected — press voice key to retry") | |
| } |
🤖 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 `@apps/supercode-cli/server/src/cli/ai/chat/chat.ts` around lines 1052 - 1059,
The voice capture success path in chat.ts leaves the “Recording...” footer stale
after transcription completes. In the voice capture handling around
voiceCaptureFlow(), clear or reset activeFooter’s status message when text is
returned, before or after updating stdinInput/stdinCursor, so the footer no
longer suggests recording is still active; keep the existing “No speech
detected” message only for the empty-text branch.
| const ELEVENLABS_URL = "https://api.elevenlabs.io/v1/speech-to-text" | ||
| const ELEVENLABS_MODEL = process.env.ELEVENLABS_MODEL || "scribe_v1" | ||
| const STT_LANGUAGE = process.env.STT_LANGUAGE || "en" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '1,240p' apps/supercode-cli/server/src/voice/speech.ts | cat -nRepository: yashdev9274/supercli
Length of output: 8360
🌐 Web query:
ElevenLabs speech-to-text API language_code form field official docs Groq audio transcriptions language field official docs
💡 Result:
The language parameter fields for ElevenLabs and Groq speech-to-text APIs are documented as follows: ElevenLabs Speech-to-Text API The ElevenLabs API uses the parameter name language_code [1][2][3]. This field is optional and accepts an ISO-639-1 or ISO-639-3 language code (e.g., "en", "eng", "es", "fr", "de") [1][4][5]. Providing this code acts as a language hint, which can improve transcription performance if the language is known beforehand [1][4][5]. If the field is not provided, the API automatically detects the language [4][3]. Groq Speech-to-Text API The Groq API (which uses OpenAI-compatible endpoints for Whisper models) uses the parameter name language [6][7]. This field is optional and accepts a language code representing the language of the audio file [6][7]. It is recommended to provide this parameter explicitly for multilingual audio to avoid ambiguity, although language detection functions automatically if it is omitted [7]. The parameter is typically passed within the client.audio.transcriptions.create method [8][7].
Citations:
- 1: https://elevenlabs.io/docs/api-reference/speech-to-text/convert
- 2: https://github.com/elevenlabs/skills/blob/main/speech-to-text/references/transcription-options.md
- 3: https://elevenlabs.io/docs/eleven-api/guides/cookbooks/speech-to-text
- 4: https://elevenlabs-elevenlabs-python.mintlify.app/api-reference/audio/speech-to-text
- 5: https://github.com/elevenlabs/skills/blob/HEAD/speech-to-text/SKILL.md
- 6: https://docs.rs/groq-api-rust/latest/groq_api_rust/struct.SpeechToTextRequest.html
- 7: https://theneuralbase.com/groq/learn/intermediate/whisper-on-groq-for-stt-fastest-path/
- 8: https://github.com/groq/groq-python/blob/main/api.md
Pass STT_LANGUAGE into both transcription requests
STT_LANGUAGE is currently ignored, so setting it has no effect. ElevenLabs expects language_code and Groq expects language; forwarding it to both requests makes the config work as intended.
🧰 Tools
🪛 ast-grep (0.44.0)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawnSync, spawn, type ChildProcess } from "child_process"
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 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 `@apps/supercode-cli/server/src/voice/speech.ts` around lines 11 - 13,
`STT_LANGUAGE` is defined in `speech.ts` but never used, so both transcription
paths ignore the configured language. Update the ElevenLabs and Groq request
builders in `speech.ts` to forward this value: pass `language_code` in the
ElevenLabs transcription call and `language` in the Groq transcription call,
using `STT_LANGUAGE` as the source. Keep the changes localized to the
transcription request logic so the existing `ELEVENLABS_URL`,
`ELEVENLABS_MODEL`, and related helpers continue to work unchanged.
| @@ -0,0 +1,20 @@ | |||
| import { InfisicalClient } from "@infisical/sdk" | |||
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
What constructor options did @infisical/sdk version 0.0.30 InfisicalClient accept, and did it support a plain "token" field?
💡 Result:
The @infisical/sdk package, which is the current Infisical Node.js SDK, does not use an InfisicalClient constructor that accepts a plain token field [1][2]. Instead, authentication in modern versions of @infisical/sdk (including v0.0.30 and later) typically requires using machine identity credentials (clientId and clientSecret) or specific authentication methods provided by the SDK [3][1]. It appears the user may be confusing @infisical/sdk with the legacy SDK, infisical-node [4][5]. The legacy infisical-node package did feature an InfisicalClient constructor that explicitly accepted a plain token field (e.g., { token: "YOUR_INFISICAL_TOKEN" }) [4][5]. For the @infisical/sdk package, standard constructor options typically include fields such as: - clientId: The machine identity client ID [1]. - clientSecret: The machine identity client secret [1]. - logLevel: The level of logs to output [1]. - cacheTtl: The time-to-live for cached secrets [1]. - siteUrl: The URL for a self-hosted Infisical instance [1].
Citations:
- 1: https://infisical-groups-phase-3.mintlify.app/sdks/languages/node
- 2: https://www.npmjs.com/package/@infisical/sdk
- 3: https://infisical.com/docs/documentation/guides/node
- 4: https://registry.npmjs.org/infisical-node
- 5: https://www.npmjs.com/package/infisical-node
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## files\n'
git ls-files packages/secrets | sed 's#^`#-` #'
printf '\n## package.json excerpt\n'
sed -n '1,220p' packages/secrets/package.json
printf '\n## client.ts excerpt\n'
cat -n packages/secrets/src/client.ts
printf '\n## search InfisicalClient usages\n'
rg -n "InfisicalClient|`@infisical/sdk`|infisical-node|siteUrl|token|clientId|clientSecret" packages/secrets -SRepository: yashdev9274/supercli
Length of output: 2988
🌐 Web query:
@infisical/sdk v0.0.30 InfisicalClient constructor token siteUrl clientId clientSecret source definition README
💡 Result:
In the current @infisical/sdk (Node.js SDK), the InfisicalClient constructor typically takes an options object to initialize the client, but the implementation relies on Machine Identity (using clientId and clientSecret) rather than the legacy token approach [1]. The constructor for the current InfisicalClient is defined as follows [1]: new InfisicalClient({ clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", logLevel?: LogLevel // Optional }); Key details regarding this implementation: 1. Authentication: The modern SDK uses Machine Identity (Universal Auth) [1]. You provide your clientId and clientSecret directly to the constructor to authenticate [1]. 2. Site URL: Unlike the legacy infisical-node package, the current @infisical/sdk handles the connection to Infisical Cloud by default [1]. If you are using a self-hosted instance, configuration patterns may vary, but current documentation emphasizes the use of Machine Identities [1][2]. 3. Legacy vs. Current: The package infisical-node is a legacy SDK [3]. That legacy version used an Infisical Token and allowed for a siteURL parameter in its constructor [3]. The current @infisical/sdk, often referred to in v2+ contexts, has shifted away from the direct token constructor in favor of the Machine Identity login flow [1][4]. If you are following tutorials that reference siteUrl, clientId, or clientSecret in a constructor, ensure you are not mixing the legacy infisical-node API with the modern @infisical/sdk [3]. In the modern SDK, authentication is handled via the credentials provided at initialization [1].
Citations:
- 1: https://infisical-groups-phase-3.mintlify.app/sdks/languages/node
- 2: https://infisical.com/docs/documentation/guides/node
- 3: https://www.npmjs.com/package/infisical-node
- 4: https://github.com/infisical/infisical-node-sdk/
Use the supported Infisical auth shape here. @infisical/sdk v0.0.30 expects machine-identity credentials (clientId/clientSecret), so passing token here looks like the legacy infisical-node API and will prevent the client from authenticating.
🤖 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/secrets/src/client.ts` at line 1, Update the Infisical client
initialization in client.ts to use the supported machine-identity auth shape for
`@infisical/sdk` v0.0.30. The issue is that the current client setup is using the
legacy token-based shape, which does not match the SDK’s expected credentials.
Fix the InfisicalClient construction to pass clientId and clientSecret instead
of token, and make sure the auth object aligns with the new SDK API.
| let shared: InfisicalClient | null = null | ||
|
|
||
| export function getClient(app: AppId): InfisicalClient { | ||
| if (!shared) { | ||
| const { token, source } = resolveToken(app) | ||
| shared = new InfisicalClient({ | ||
| token, | ||
| siteUrl: process.env.INFISICAL_SITE_URL ?? "https://app.infisical.com", | ||
| }) | ||
| } | ||
| return shared | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Singleton shared client ignores app on subsequent calls.
shared is a single module-level instance keyed by nothing. Once created for one app, every later getClient(otherApp) returns the same client authenticated with the first app's token — secrets for the second app would be fetched (or fail) using the wrong identity.
🐛 Proposed fix: key the singleton cache by app
-let shared: InfisicalClient | null = null
+const clients = new Map<AppId, InfisicalClient>()
export function getClient(app: AppId): InfisicalClient {
- if (!shared) {
- const { token, source } = resolveToken(app)
- shared = new InfisicalClient({
- token,
- siteUrl: process.env.INFISICAL_SITE_URL ?? "https://app.infisical.com",
- })
- }
- return shared
+ if (!clients.has(app)) {
+ const { token } = resolveToken(app)
+ clients.set(
+ app,
+ new InfisicalClient({
+ token,
+ siteUrl: process.env.INFISICAL_SITE_URL ?? "https://app.infisical.com",
+ }),
+ )
+ }
+ return clients.get(app)!
}
export function resetClient(): void {
- shared = null
+ clients.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/secrets/src/client.ts` around lines 5 - 16, The module-level
singleton in getClient currently caches a single InfisicalClient in shared, so
later calls with a different app reuse the first app’s token. Update the caching
logic in getClient and the shared state to be keyed by AppId instead of one
global instance, so each app gets its own client from resolveToken(app) while
still reusing clients per app. Keep the lookup/creation logic localized to
getClient and the shared cache declaration.
| const client = getClient(app) | ||
| const secrets = await client.listSecrets({ | ||
| environment: env, | ||
| projectId, | ||
| attachToProcessEnv: false, | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
No error handling or timeout around the external Infisical call.
client.listSecrets(...) is awaited without try/catch, so any SDK failure (auth error, network issue, rate limit) propagates as an unhandled/raw rejection, and there's no timeout to bound a hung request. As per coding guidelines, **/*.{ts,tsx} files should "Use try-catch for async operations."
🤖 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/secrets/src/index.ts` around lines 28 - 33, The async Infisical call
in getClient/listSecrets is missing defensive handling, so SDK failures can
escape as raw rejections and a hung request can block indefinitely. Wrap the
client.listSecrets(...) await in try-catch, handle/log the error in this
function path, and add a timeout or abort mechanism around the external call so
secrets loading fails fast and predictably.
Source: Coding guidelines
| function readLocalSessionToken(): string | null { | ||
| const configPath = join(homedir(), ".infisical", "infisical-config.json") | ||
| if (!existsSync(configPath)) return null | ||
| try { | ||
| const raw = readFileSync(configPath, "utf-8") | ||
| const config = JSON.parse(raw) | ||
| return config?.token ?? config?.accessToken ?? null | ||
| } catch { | ||
| return null | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Where does the Infisical CLI store login/session tokens on disk when using the default credential store, and what is the JSON schema if using the file vault?
💡 Result:
The Infisical CLI uses an auto-detect mechanism by default to determine the best credential store for your environment [1][2]. When the default auto-detect mode is unable to locate a suitable system keyring (such as Keychain on macOS, Credential Manager on Windows, or Secret Service/KWallet on Linux), it falls back to a file-based storage method [1][3]. If you explicitly configure the CLI to use the file vault, or if the auto-detect mechanism falls back to it, the credentials are stored in a local configuration file [1][2]. While the exact file path can vary by operating system according to standard configuration directory conventions (typically within the user's home directory, such as ~/.config/infisical/ on Linux/macOS), the file vault is explicitly described as an encrypted file store [1]. There is no public, static JSON schema for the file vault's contents, as the file is intended to be managed directly by the CLI's internal vault implementation, which performs the necessary encryption to safeguard your sensitive tokens [1]. Users are generally expected to interact with this data exclusively through Infisical CLI commands (e.g., infisical login, infisical vault) rather than manual file manipulation [4][2]. If you are scripting or require access to the token for programmatic use, you should rely on the CLI's output mechanisms—such as the --plain and --silent flags—rather than attempting to parse the vault file directly [4].
Citations:
- 1: https://infisical.mintlify.dev/docs/cli/commands/vault
- 2: https://infisical.mintlify.dev/docs/cli/faq
- 3: [CLI] StoreUserCredsInKeyRing: unable to store user credentials Infisical/infisical#936
- 4: https://infisical.mintlify.dev/docs/cli/commands/login
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## repo files\n'
git ls-files 'packages/secrets/src/resolve-token.ts' 'packages/secrets/src/*' | sed -n '1,80p'
printf '\n## outline resolve-token.ts\n'
ast-grep outline packages/secrets/src/resolve-token.ts --view expanded || true
printf '\n## nearby contents\n'
cat -n packages/secrets/src/resolve-token.ts | sed -n '1,220p'
printf '\n## search for config path/schema references\n'
rg -n '"\.infisical"|infisical-config|accessToken|readLocalSessionToken|resolveToken|MissingInfisicalTokenError' packages/secrets/src packages -g '!**/dist/**' -g '!**/build/**' || trueRepository: yashdev9274/supercli
Length of output: 4989
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## resolve-token.ts\n'
cat -n packages/secrets/src/resolve-token.ts | sed -n '1,220p'
printf '\n## references\n'
rg -n 'infisical-config|readLocalSessionToken|MissingInfisicalTokenError|accessToken|token' packages/secrets/src -g '!**/dist/**' -g '!**/build/**' || trueRepository: yashdev9274/supercli
Length of output: 3329
🌐 Web query:
Infisical CLI vault docs keyring file vault encrypted file schema token accessToken ~/.infisical/infisical-config.json
💡 Result:
The Infisical CLI manages user authentication and credentials primarily through a system keyring or an encrypted file [1]. By default, the CLI uses an 'auto' vault type, which attempts to select the most appropriate native system keyring [1]. If a system keyring is unavailable or fails, users can explicitly configure the CLI to use a 'file' vault [1][2]. The configuration of this storage behavior is handled via the infisical vault command [1]: - To view or change the current vault type, use: infisical vault set (where is 'auto' or 'file') [1][2]. It is important to distinguish between these storage mechanisms and the.infisical.json file: - System Keyring / Encrypted File: Used to store sensitive user login credentials (like the access token obtained after running infisical login) securely on the local machine [1][3]. -.infisical.json: This is a configuration file created by infisical init in the root directory of a project [4][5]. It stores non-sensitive project-specific settings, such as the workspace ID, default environment, and custom API domain (e.g., {"workspaceId": "...", "defaultEnvironment": "dev", "domain": "..."}) [4][6][7]. It is typically committed to version control and does not contain sensitive authentication tokens [6]. When using machine identities or service tokens, credentials are often handled via the INFISICAL_TOKEN environment variable or the --token flag rather than being stored in the local keyring [8][9].
Citations:
- 1: https://infisical.com/docs/cli/commands/vault
- 2: [CLI] StoreUserCredsInKeyRing: unable to store user credentials Infisical/infisical#936
- 3: https://infisical.com/docs/cli/commands/login
- 4: https://infisical.com/docs/cli/project-config
- 5: https://infisical.com/docs/cli/usage
- 6: https://infisical.mintlify.dev/docs/cli/project-config
- 7: [Feat] Multi-Path Auto-Loading in .infisical.json Infisical/infisical#5412
- 8: https://infisical.com/docs/cli/commands/run
- 9: https://infisical.com/docs/cli/commands/secrets
Use the CLI’s actual credential store here instead of ~/.infisical/infisical-config.json. The Infisical CLI keeps login state in the system keyring by default and falls back to an encrypted file vault, so config.token/config.accessToken won’t resolve a normal logged-in session and this will still throw MissingInfisicalTokenError.
🤖 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/secrets/src/resolve-token.ts` around lines 12 - 22, The local
session lookup in readLocalSessionToken is reading
~/.infisical/infisical-config.json, but the CLI stores login state in the system
keyring with an encrypted file vault fallback, so this path will not return a
valid session token. Update readLocalSessionToken in resolve-token.ts to query
the CLI’s वास्तविक credential store instead of parsing
config.token/config.accessToken from that JSON file, and make sure the returned
value matches the token source used by the logged-in Infisical CLI session so
MissingInfisicalTokenError is not triggered for normal users.
- Bump @infisical/sdk to ^5.0.2 (v0.0.30 was never published to npm)
- Replace InfisicalClient with InfisicalSDK, authenticate via
separate .authenticate(token) call per v5 API
- Destructure .secrets from listSecrets response (v5 returns
{ secrets: Secret[] } instead of a bare array)
- Fixes "No version matching ^0.0.30 found" on Vercel deployment
Description
Type of change
How Has This Been Tested?
Please describe the tests that you ran to verify your changes.
bun testpassesbun run typecheckpassesbun run lintpasses (if applicable)Checklist:
Summary by CodeRabbit
New Features
Bug Fixes
Documentation