Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,45 @@ jobs:
path: artifacts/*.vsix
if-no-files-found: error
retention-days: 14

jetbrains:
name: JetBrains build, test, verify
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
project: [jetbrains, jetbrains-apiclient]

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ matrix.project }}-${{ hashFiles(format('apps/{0}/**/*.gradle.kts', matrix.project), format('apps/{0}/gradle.properties', matrix.project), format('apps/{0}/gradle/wrapper/gradle-wrapper.properties', matrix.project)) }}
restore-keys: |
gradle-${{ runner.os }}-${{ matrix.project }}-
gradle-${{ runner.os }}-

- name: Compile, test, build, and verify plugin
working-directory: apps/${{ matrix.project }}
run: ./gradlew compileKotlin compileTestKotlin test buildPlugin verifyPlugin --no-daemon

- name: Upload plugin zip (smoke build)
uses: actions/upload-artifact@v4
with:
name: codesetu-${{ matrix.project }}-zip
path: apps/${{ matrix.project }}/build/distributions/*.zip
if-no-files-found: error
retention-days: 14
4 changes: 2 additions & 2 deletions .github/workflows/release-jetbrains.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ jobs:
echo "tag=jetbrains-v${VERSION}" >> "$GITHUB_OUTPUT"
echo "Computed version: ${VERSION}"

- name: Build plugin
- name: Build and verify plugin
working-directory: apps/jetbrains
run: ./gradlew -PpluginVersion=${{ steps.version.outputs.version }} buildPlugin --no-daemon
run: ./gradlew -PpluginVersion=${{ steps.version.outputs.version }} buildPlugin verifyPlugin --no-daemon

- name: Verify plugin zip exists
run: |
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/release-vscode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ jobs:
release:
name: Build and publish
runs-on: ubuntu-latest
# Job-level env so the Open VSX step's `if` can read it (step-level env is
# not visible in that same step's `if` condition).
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}

steps:
- name: Check out repository
Expand Down Expand Up @@ -72,8 +76,6 @@ jobs:

- name: Publish to Open VSX
if: ${{ !inputs.dry-run && env.OVSX_PAT != '' }}
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
VSIX=$(ls artifacts/codesetu-*.vsix | head -n1)
pnpm dlx ovsx publish "$VSIX" --pat "$OVSX_PAT"
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Discord](https://img.shields.io/badge/Discord-Join-5865F2?logo=discord&logoColor=white)](https://discord.gg/sjVKU8cpC6)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)

A **Copilot / Cursor alternative** designed for Indian developers, enterprises, public sector teams, and air-gapped deployments. Bring your own model — CodeSetu works with **Sarvam, OpenAI-compatible APIs (Ollama, vLLM, OpenRouter, SGLang)**, and local self-hosted deployments. AI chat, repo-aware context, selected-code actions, inline FIM completions, tool-calling, and an extensible plugin and skill system across VSCode and JetBrains.
A **Copilot / Cursor alternative** designed for Indian developers, enterprises, public sector teams, and air-gapped deployments. Bring your own model — CodeSetu works with **Sarvam, Hugging Face (any served chat model, dedicated endpoints, or self-hosted TGI), OpenAI-compatible APIs (Ollama, vLLM, OpenRouter, SGLang)**, and local self-hosted deployments. AI chat, repo-aware context, selected-code actions, inline FIM completions, tool-calling, and an extensible plugin and skill system across VSCode and JetBrains.

**Highlights**: AI chat in IDE · Repo-aware context · Selected-code actions · Inline (FIM) code completions · Provider setup and diagnostics · Workspace skills/checks · Air-gapped friendly · Hindi / Indic-aware · Plugin + skill SDK · 100% open-source (Apache 2.0)

Expand All @@ -27,7 +27,7 @@ This repository is a pnpm + Gradle monorepo organized as:

- Node.js 18+
- pnpm 9+
- A provider — Sarvam API key, OpenRouter key, or a local OpenAI-compatible endpoint (Ollama, vLLM, SGLang)
- A provider — Sarvam API key, a Hugging Face token (`hf_…`), OpenRouter key, or a local OpenAI-compatible endpoint (Ollama, vLLM, SGLang)

## Setup

Expand All @@ -50,10 +50,10 @@ Marketplace, Open VSX, and private VSIX hosting, see

The VSCode extension reads these settings:

- `codesetu.provider` - `sarvam` or `openai-compatible`
- `codesetu.apiKey` - optional provider API key
- `codesetu.baseUrl` - optional OpenAI-compatible base URL
- `codesetu.model` - optional model name
- `codesetu.provider` - `sarvam`, `openai-compatible`, or `huggingface`
- `codesetu.baseUrl` - optional base URL (e.g. `https://router.huggingface.co/v1`, a dedicated HF endpoint, or a local server)
- `codesetu.model` - optional model name (for Hugging Face, the model repo id, e.g. `meta-llama/Llama-3.3-70B-Instruct`)
- API key — set via the **CodeSetu: Setup Provider** command (stored in the OS secret store), or the `SARVAM_API_KEY` / `HF_TOKEN` / `CODESETU_API_KEY` environment variables
- `codesetu.inlineCompletions.enabled` - enable FIM inline completions
- `codesetu.chat.maxTokens` / `codesetu.chat.temperature`

Expand Down
Binary file not shown.
Binary file not shown.
Binary file modified apps/jetbrains-apiclient/.gradle/8.10/fileHashes/fileHashes.bin
Binary file not shown.
Binary file modified apps/jetbrains-apiclient/.gradle/8.10/fileHashes/fileHashes.lock
Binary file not shown.
Binary file not shown.
Binary file modified apps/jetbrains-apiclient/.gradle/file-system.probe
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2026-05-27
2026-05-28
12 changes: 12 additions & 0 deletions apps/jetbrains-apiclient/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType

plugins {
// Kotlin version must be >= the version bundled in the target IntelliJ Platform.
// IDEA 2025.2 (IC-252.x) ships with Kotlin 2.2.x.
Expand Down Expand Up @@ -36,6 +38,8 @@ dependencies {
intellijIdeaCommunity("2025.2.5")
}
instrumentationTools()
// CLI used by the `verifyPlugin` task (JetBrains Plugin Verifier).
pluginVerifier()
}
}

Expand All @@ -61,6 +65,14 @@ intellijPlatform {
}
}
buildSearchableOptions = false
// `verifyPlugin` runs the JetBrains Plugin Verifier. Pin it to the build
// target so it's deterministic and always resolvable (recommended() can pick
// an unreleased IDE that fails to download).
pluginVerification {
ides {
ide(IntelliJPlatformType.IntellijIdeaCommunity, "2025.2.5")
}
}
publishing {
token = providers.environmentVariable("JETBRAINS_MARKETPLACE_TOKEN")
}
Expand Down
12 changes: 12 additions & 0 deletions apps/jetbrains/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType

plugins {
// Kotlin version must be >= the version bundled in the target IntelliJ Platform.
// IDEA 2025.2 (IC-252.x) ships with Kotlin 2.2.x.
Expand Down Expand Up @@ -35,6 +37,8 @@ dependencies {
intellijIdeaCommunity("2025.2.5")
}
instrumentationTools()
// CLI used by the `verifyPlugin` task (JetBrains Plugin Verifier).
pluginVerifier()
}
}

Expand Down Expand Up @@ -65,6 +69,14 @@ intellijPlatform {
// compute a settings-search index; our scaffold has no settings panel, so
// it's pure overhead. Re-enable once we add settings UI.
buildSearchableOptions = false
// `verifyPlugin` runs the JetBrains Plugin Verifier. Pin it to the build
// target so it's deterministic and always resolvable (recommended() can pick
// an unreleased IDE that fails to download).
pluginVerification {
ides {
ide(IntelliJPlatformType.IntellijIdeaCommunity, "2025.2.5")
}
}
publishing {
token = providers.environmentVariable("JETBRAINS_MARKETPLACE_TOKEN")
}
Expand Down
2 changes: 1 addition & 1 deletion apps/jetbrains/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ org.gradle.parallel=true
# Plugin version baseline. CI overrides with -PpluginVersion=<major>.<minor>.<run_number>.
# Bump the major.minor here when you want a semantic version bump; CI keeps the
# patch incrementing automatically.
pluginVersion=0.1.0
pluginVersion=0.2.0

# Tell Gradle's toolchain detection where to find JDK 21 installed via Homebrew.
# Falls back to other auto-detected JDKs if this path doesn't exist on a contributor's machine.
Expand Down
2 changes: 1 addition & 1 deletion apps/jetbrains/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codesetu/jetbrains",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"description": "JetBrains plugin for CodeSetu. This Kotlin/Gradle project is integrated into the pnpm workspace via thin script delegations — actual build/test runs through Gradle.",
"license": "Apache-2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import kotlinx.serialization.Serializable

enum class ProviderKind(val id: String) {
SARVAM("sarvam"),
OPENAI_COMPATIBLE("openai-compatible");
OPENAI_COMPATIBLE("openai-compatible"),
HUGGING_FACE("huggingface");

companion object {
fun fromId(id: String): ProviderKind =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,27 @@ import ai.codesetu.model.IdeActionId
import ai.codesetu.model.IdeContextPayload
import ai.codesetu.model.WorkspaceInstruction

fun buildSystemMessage(instructions: List<WorkspaceInstruction>): String =
if (instructions.isEmpty()) {
"You are CodeSetu, an AI coding assistant for Indian developers. Be concise, correct, practical, and privacy-aware."
} else {
"You are CodeSetu, an AI coding assistant for Indian developers. Be concise, correct, practical, and privacy-aware.\n\nFollow applicable workspace guidance when it helps the user's request."
fun buildSystemMessage(instructions: List<WorkspaceInstruction>): String {
val parts = mutableListOf(
"You are CodeSetu, an AI coding assistant for Indian developers. Be concise, correct, practical, and privacy-aware.",
"Use the supplied IDE context as the source of truth. Ask for missing context when needed.",
)

if (instructions.isNotEmpty()) {
parts.add(formatWorkspaceInstructions(instructions))
}

return parts.joinToString("\n\n")
}

private fun formatWorkspaceInstructions(instructions: List<WorkspaceInstruction>): String {
val rendered = instructions.joinToString("\n\n") { instruction ->
listOf("${instruction.name} (${instruction.id})", instruction.description, instruction.body)
.joinToString("\n")
}
return "Workspace instructions\n\n$rendered"
}

fun buildActionInstruction(actionId: IdeActionId): String =
when (actionId) {
IdeActionId.EXPLAIN -> "Explain the selected code clearly and concisely. Include key control flow, inputs, outputs, and risks."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ai.codesetu.model.ChatCompletionRequest
import ai.codesetu.model.ChatCompletionResponse
import ai.codesetu.model.ChatCompletionChunk
import ai.codesetu.model.ChatMessage
import ai.codesetu.model.ProviderKind
import ai.codesetu.settings.CodeSetuSettingsState
import ai.codesetu.settings.resolveCodeSetuModel
import java.net.URI
Expand All @@ -24,12 +25,12 @@ class CodeSetuProviderClient(
messages = messages,
maxTokens = maxTokens,
temperature = temperature,
reasoningEffort = "low",
reasoningEffort = reasoningEffortFor(state.provider),
json = json,
)
val request = HttpRequest.newBuilder()
.uri(URI.create(state.baseUrl.trimEnd('/') + "/chat/completions"))
.header("Authorization", "Bearer ${state.apiKey}")
.header("Authorization", "Bearer ${CodeSetuSettingsState.getInstance().getApiKey()}")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build()
Expand All @@ -54,13 +55,13 @@ class CodeSetuProviderClient(
messages = messages,
maxTokens = maxTokens,
temperature = temperature,
reasoningEffort = "low",
reasoningEffort = reasoningEffortFor(state.provider),
stream = true,
json = json,
)
val request = HttpRequest.newBuilder()
.uri(URI.create(state.baseUrl.trimEnd('/') + "/chat/completions"))
.header("Authorization", "Bearer ${state.apiKey}")
.header("Authorization", "Bearer ${CodeSetuSettingsState.getInstance().getApiKey()}")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build()
Expand All @@ -87,7 +88,12 @@ class CodeSetuProviderClient(
break
}

val text = getAssistantChunkText(json.decodeFromString<ChatCompletionChunk>(data))
// Skip malformed/partial SSE payloads instead of aborting the whole stream.
val text = try {
getAssistantChunkText(json.decodeFromString<ChatCompletionChunk>(data))
} catch (error: Exception) {
""
}

if (text.isNotEmpty()) {
assistantText.append(text)
Expand All @@ -100,6 +106,11 @@ class CodeSetuProviderClient(
}
}

// Sarvam needs a low reasoning effort to avoid exhausting its token budget;
// other providers (OpenAI-compatible, Hugging Face) may reject an unknown field.
private fun reasoningEffortFor(providerId: String): String? =
if (ProviderKind.fromId(providerId) == ProviderKind.SARVAM) "low" else null

fun getAssistantText(response: ChatCompletionResponse): String {
val message = response.choices.firstOrNull()?.message
return message?.content ?: message?.refusal.orEmpty()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ai.codesetu.settings

import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.generateServiceName
import com.intellij.ide.passwordSafe.PasswordSafe

/**
* Stores the CodeSetu provider API key in the IDE's [PasswordSafe] (OS keychain
* / encrypted credential store) instead of the plaintext settings XML.
*/
object CodeSetuApiKeyStore {
private val attributes: CredentialAttributes =
CredentialAttributes(generateServiceName("CodeSetu", "apiKey"))

fun get(): String = PasswordSafe.instance.getPassword(attributes).orEmpty()

fun set(value: String) {
val trimmed = value.trim()
if (trimmed.isEmpty()) {
PasswordSafe.instance.set(attributes, null)
} else {
PasswordSafe.instance.set(attributes, Credentials(null, trimmed))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ai.codesetu.settings

import ai.codesetu.model.ProviderKind

/**
* Curated, reliably-served chat models offered in the model picker. Users can
* still type any other model id (Hub repo id, dedicated endpoint model, etc.).
*/
object CodeSetuModelCatalog {
val HUGGINGFACE_MODELS: List<String> = listOf(
"meta-llama/Llama-3.3-70B-Instruct",
"Qwen/Qwen2.5-72B-Instruct",
"Qwen/Qwen2.5-Coder-32B-Instruct",
"deepseek-ai/DeepSeek-V3-0324",
"meta-llama/Llama-3.1-8B-Instruct",
"google/gemma-2-27b-it",
"mistralai/Mistral-Small-24B-Instruct-2501",
)

fun suggestionsFor(providerId: String): List<String> =
if (ProviderKind.fromId(providerId) == ProviderKind.HUGGING_FACE) HUGGINGFACE_MODELS else emptyList()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
package ai.codesetu.settings

import ai.codesetu.model.ProviderKind

const val DEFAULT_CODESETU_BASE_URL = "https://api.sarvam.ai/v1"
const val DEFAULT_CODESETU_MODEL = "sarvam-30b"

fun resolveCodeSetuModel(model: String): String =
model.ifBlank { DEFAULT_CODESETU_MODEL }

data class ProviderDefaults(val baseUrl: String, val model: String)

fun providerDefaults(providerId: String): ProviderDefaults =
when (ProviderKind.fromId(providerId)) {
ProviderKind.SARVAM -> ProviderDefaults("https://api.sarvam.ai/v1", "sarvam-30b")
ProviderKind.OPENAI_COMPATIBLE ->
ProviderDefaults("http://localhost:11434/v1", "qwen2.5-coder:7b")
ProviderKind.HUGGING_FACE ->
ProviderDefaults("https://router.huggingface.co/v1", "meta-llama/Llama-3.3-70B-Instruct")
}
Loading
Loading