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
315 changes: 197 additions & 118 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/cortex-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.turbo
27 changes: 27 additions & 0 deletions packages/cortex-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# cortex-sdk

Unified SDK for building AI agents — 8 gateway providers, 3 web search providers, 6 MCP platforms, and 3 voice providers.

## Install

```bash
bun add cortex-sdk
```

Requires `ai` as a peer dependency (`^4.0.0 || ^5.0.0 || ^6.0.0`).

## Modules

| Import path | Purpose |
| -------------------------- | ----------------------------------------------- |
| `cortex-sdk` | Root exports + `SupercodeAgent` orchestration |
| `cortex-sdk/gateway` | `createGateway` — model access via 8 providers |
| `cortex-sdk/agent-handler` | `createAgentHandler` — MergeDev tool packs |
| `cortex-sdk/composio` | `createComposio` — 150+ app integrations |
| `cortex-sdk/web-search` | `createWebSearch` — Exa, Firecrawl, Context.dev |
| `cortex-sdk/mcp` | `createMcpManager` — direct MCP connections |
| `cortex-sdk/voice` | `createVoice` — STT + TTS |

## Status

Package scaffolded (Phase 1). Module factories are stubs that throw `SdkError("NOT_IMPLEMENTED")` until their implementation phases land.
220 changes: 220 additions & 0 deletions packages/cortex-sdk/cli/src/catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
export type ModuleId =
| "gateway"
| "web-search"
| "voice"
| "composio"
| "agent-handler"
| "mcp"

export interface Provider {
id: string
name: string
description: string
envKey?: string
defaultModel?: string
defaultBaseURL?: string
needsKey: boolean
}

export interface Module {
id: ModuleId
name: string
description: string
providers: Provider[]
}

export const MODULES: Module[] = [
{
id: "gateway",
name: "Gateway",
description: "LLM gateway — chat, streaming, and tool calls over one OpenAI-compatible API",
providers: [
{
id: "concentrateai",
name: "Concentrate AI",
description: "Aggregated model router",
envKey: "CONCENTRATEAI_API_KEY",
defaultModel: "deepseek/deepseek-v4-flash",
defaultBaseURL: "https://api.concentrate.ai/v1",
needsKey: true,
},
{
id: "mergedev",
name: "Merge Dev",
description: "Multi-provider API gateway",
envKey: "MERGEDEV_API_KEY",
defaultModel: "anthropic/claude-opus-4-8",
defaultBaseURL: "https://api-gateway.merge.dev/v1/openai",
needsKey: true,
},
{
id: "openrouter",
name: "OpenRouter",
description: "Unified access to 300+ models",
envKey: "OPENROUTER_API_KEY",
defaultModel: "moonshotai/kimi-k2.6",
defaultBaseURL: "https://openrouter.ai/api/v1",
needsKey: true,
},
{
id: "gemini",
name: "Google Gemini",
description: "Gemini models via Generative Language API",
envKey: "GOOGLE_GENERATIVE_AI_API_KEY",
defaultModel: "gemini-2.5-flash",
needsKey: true,
},
{
id: "minimax",
name: "MiniMax",
description: "MiniMax models",
envKey: "MINIMAX_API_KEY",
defaultModel: "MiniMax-M1",
needsKey: true,
},
{
id: "nim",
name: "NVIDIA NIM",
description: "NVIDIA-hosted open models",
envKey: "NVIDIA_API_KEY",
defaultModel: "minimaxai/minimax-m3",
defaultBaseURL: "https://integrate.api.nvidia.com/v1",
needsKey: true,
},
{
id: "orcarouter",
name: "Orca Router",
description: "Budget LLM router",
envKey: "ORCAROUTER_API_KEY",
defaultModel: "openai/gpt-4o-mini",
defaultBaseURL: "https://api.orcarouter.ai/v1",
needsKey: true,
},
{
id: "supercode-cloud",
name: "SuperCode Cloud",
description: "Hosted SuperCode gateway (no key required)",
defaultModel: "deepseek-v4-flash",
defaultBaseURL: "https://supercode-8w7e.onrender.com",
needsKey: false,
},
],
},
{
id: "web-search",
name: "Web Search",
description: "Search tool providers — exa, firecrawl, context.dev",
providers: [
{
id: "exa",
name: "Exa",
description: "Neural web search API",
envKey: "EXA_API_KEY",
needsKey: true,
},
{
id: "firecrawl",
name: "Firecrawl",
description: "Crawl, scrape, and search the web",
envKey: "FIRECRAWL_API_KEY",
needsKey: true,
},
{
id: "contextdev",
name: "context.dev",
description: "Context search engine",
envKey: "CONTEXTDEV_API_KEY",
needsKey: true,
},
],
},
{
id: "voice",
name: "Voice",
description: "Speech-to-text and text-to-speech providers",
providers: [
{
id: "smallest",
name: "Smallest.ai",
description: "Pulse STT + TTS (primary STT provider)",
envKey: "SMALLEST_API_KEY",
needsKey: true,
},
{
id: "elevenlabs",
name: "ElevenLabs",
description: "High-quality TTS + Scribe STT",
envKey: "ELEVENLABS_API_KEY",
needsKey: true,
},
{
id: "groq",
name: "Groq",
description: "Whisper STT via Groq",
envKey: "GROQ_API_KEY",
needsKey: true,
},
],
},
{
id: "composio",
name: "Composio",
description: "Tool integrations for agents (GitHub, Gmail, Slack, ...)",
providers: [
{
id: "composio",
name: "Composio",
description: "Tool integration platform",
envKey: "COMPOSIO_API_KEY",
needsKey: true,
},
],
},
{
id: "agent-handler",
name: "Agent Handler",
description: "Backend agent handler client (SuperCode or custom)",
providers: [
{
id: "agent-handler",
name: "Agent Handler",
description: "HTTP/SSE agent client",
envKey: "AGENT_HANDLER_API_KEY",
needsKey: true,
},
],
},
{
id: "mcp",
name: "MCP",
description: "Model Context Protocol servers (coming soon)",
providers: [],
},
]

export function getModule(id: ModuleId): Module | undefined {
return MODULES.find((m) => m.id === id)
}

export function configKey(moduleId: ModuleId): string {
return moduleId.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())
}

export function moduleFromKey(key: string): Module | undefined {
return MODULES.find((m) => configKey(m.id) === key || m.id === key)
}

export function getProvider(moduleId: ModuleId, providerId: string): Provider | undefined {
return getModule(moduleId)?.providers.find((p) => p.id === providerId)
}

export function findUnit(term: string): { module: Module; provider: Provider } | undefined {
for (const module of MODULES) {
for (const provider of module.providers) {
if (provider.id === term || `${module.id}.${provider.id}` === term) {
return { module, provider }
}
}
}
return undefined
}
28 changes: 28 additions & 0 deletions packages/cortex-sdk/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { TuiContext } from "../tui/renderer"
import { destroyTui } from "../tui/renderer"
import { findUnit, type ModuleId } from "../catalog"
import { collectConfig } from "./flow"
import { writeConfig, readConfig } from "../config"

export async function runAdd(tui: TuiContext, term: string): Promise<number> {
const unit = findUnit(term)
if (!unit) {
await destroyTui(tui)
console.log(`Unknown unit: ${term}`)
console.log("Run `cortex-sdk list` to see available units.")
return 1
}

const { module, provider } = unit
const entry = await collectConfig(tui, module.id, provider.id, provider.needsKey, provider.defaultModel, provider.envKey)
await destroyTui(tui)

if (!entry) {
console.log("Cancelled.")
return 0
}

const result = await writeConfig(process.cwd(), [entry], { append: true })
console.log(`\nAdded ${module.id}.${provider.id} → ${result.path}`)
return 0
}
84 changes: 84 additions & 0 deletions packages/cortex-sdk/cli/src/commands/find.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { TuiContext } from "../tui/renderer"
import { destroyTui } from "../tui/renderer"
import { MODULES, type ModuleId } from "../catalog"
import { promptSelect, type SelectOption } from "../tui/select"
import { promptMultiSelect } from "../tui/multiselect"
import { resolveModule, selectModules, collectConfig } from "./flow"
import { writeConfig, readConfig } from "../config"

export async function runFind(tui: TuiContext): Promise<number> {
// 1. pick a unit (provider or module)
const unitOptions: SelectOption[] = MODULES.flatMap((module) =>
module.providers.map((p) => ({
name: p.name,
description: `${module.name} — ${p.description}`,
value: `${module.id}.${p.id}`,
})),
)

const pick = await promptSelect(tui, {
title: "Find a provider",
subtitle: "Search or scroll to choose what to install",
options: unitOptions,
})
if (!pick) {
await destroyTui(tui)
console.log("Cancelled.")
return 0
}

const [moduleId, providerId] = pick.split(".") as [ModuleId, string]

// 2. ask which SDK modules to enable
const modules = await selectModules(tui)
if (!modules || modules.length === 0) {
await destroyTui(tui)
console.log("No modules selected — no changes made.")
return 0
}

// 3. resolve the picked provider first, then the rest
const entries = []
const pickedEntry = await resolveProviderDirect(tui, moduleId, providerId)
if (pickedEntry) entries.push(pickedEntry)

for (const m of modules) {
if (m === moduleId) continue
const entry = await resolveModule(tui, m)
if (entry) entries.push(entry)
}

await destroyTui(tui)

if (entries.length === 0) {
console.log("No modules configured — no changes made.")
return 0
}

const result = await writeConfig(process.cwd(), entries, { append: true })
const existing = await readConfig(process.cwd())
console.log(`\nWrote ${result.path}`)
console.log("")
for (const [name, config] of Object.entries(existing.modules)) {
console.log(` export const ${name} = ${JSON.stringify(config)}`)
}
return 0
}

async function resolveProviderDirect(
tui: TuiContext,
moduleId: ModuleId,
providerId: string,
): Promise<{ name: string; config: Record<string, unknown> } | undefined> {
const module = MODULES.find((m) => m.id === moduleId)
if (!module) return undefined
const provider = module.providers.find((p) => p.id === providerId)
if (!provider) return undefined

if (moduleId === "voice") {
// keep the voice flow (stt + tts) but pre-pick the chosen provider as stt
return resolveModule(tui, moduleId)
}

return collectConfig(tui, moduleId, providerId, provider.needsKey, provider.defaultModel, provider.envKey)
}
Loading
Loading