Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
23 changes: 23 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@
},
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
"presentation": { "hidden": false, "group": "tasks", "order": 1 }
},
{
"name": "Run Extension [Local Backend]",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}/src", "--disable-extensions"],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}",
"env": {
"NODE_ENV": "development",
"VSCODE_DEBUG_MODE": "true",
"KILOCODE_BACKEND_BASE_URL": "${input:kilocodeBackendBaseUrl}"
},
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
"presentation": { "hidden": false, "group": "tasks", "order": 2 }
}
],
"inputs": [
Expand All @@ -52,6 +69,12 @@
"description": "Directory the dev extension will open in",
"default": "${workspaceFolder}/launch",
"type": "promptString"
},
{
"id": "kilocodeBackendBaseUrl",
"description": "Override the kilocode backend base URL",
"default": "http://localhost:3000",
"type": "promptString"
}
]
}
12 changes: 12 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,18 @@ These hooks help maintain code quality and consistency. If you encounter issues
- Check the Output panel in VSCode (View > Output) and select "Kilo Code" from the dropdown
- For webview issues, use the browser developer tools in the webview (right-click > "Inspect Element")

### Testing with Local Backend

To test the extension against a local Kilo Code backend:

1. **Set up your local backend** at `http://localhost:3000`
2. **Use the "Run Extension [Local Backend]" launch configuration**:
- Go to Run and Debug (Ctrl+Shift+D)
- Select "Run Extension [Local Backend]" from the dropdown
- Press F5 to start debugging

This automatically sets the `KILOCODE_BACKEND_BASE_URL` environment variable, making all sign-in/sign-up buttons point to your local backend instead of production.

## Contributing

We welcome contributions to Kilo Code! Here's how you can help:
Expand Down
3 changes: 2 additions & 1 deletion cli/src/services/telemetry/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as crypto from "crypto"
import * as os from "os"
import { KiloCodePaths } from "../../utils/paths.js"
import { logs } from "../logs.js"
import { getApiUrl } from "@roo-code/types"

/**
* User identity structure
Expand Down Expand Up @@ -107,7 +108,7 @@ export class IdentityManager {

try {
// Fetch user profile from Kilocode API
const response = await fetch("https://api.kilocode.ai/api/profile", {
const response = await fetch(getApiUrl("/profile"), {
headers: {
Authorization: `Bearer ${kilocodeToken}`,
"Content-Type": "application/json",
Expand Down
205 changes: 204 additions & 1 deletion packages/types/src/__tests__/kilocode.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// npx vitest run src/__tests__/kilocode.test.ts

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { ghostServiceSettingsSchema, checkKilocodeBalance } from "../kilocode/kilocode.js"
import {
ghostServiceSettingsSchema,
checkKilocodeBalance,
getApiUrl,
getAppUrl,
getKiloUrlFromToken,
getExtensionConfigUrl,
} from "../kilocode/kilocode.js"

describe("ghostServiceSettingsSchema", () => {
describe("autoTriggerDelay", () => {
Expand Down Expand Up @@ -205,3 +212,199 @@ describe("checkKilocodeBalance", () => {
expect(result).toBe(false)
})
})

describe("URL functions", () => {
const originalEnv = process.env.KILOCODE_BACKEND_BASE_URL

// Helper functions to create properly formatted test tokens
const createDevToken = () => {
const payload = { env: "development" }
return `header.${btoa(JSON.stringify(payload))}.signature`
}

const createProdToken = () => {
const payload = {}
return `header.${btoa(JSON.stringify(payload))}.signature`
}

afterEach(() => {
// Reset environment variable after each test
if (originalEnv) {
process.env.KILOCODE_BACKEND_BASE_URL = originalEnv
} else {
delete process.env.KILOCODE_BACKEND_BASE_URL
}
})

describe("getExtensionConfigUrl", () => {
it("should use path structure for development", () => {
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
expect(getExtensionConfigUrl()).toBe("http://localhost:3000/extension-config.json")
})
it("should use subdomain structure for production", () => {
expect(getExtensionConfigUrl()).toBe("https://api.kilocode.ai/extension-config.json")
})
})

describe("getApiUrl", () => {
it("should handle production URLs correctly", () => {
// API URLs using /api path structure
expect(getApiUrl("/extension-config.json")).toBe("https://kilocode.ai/api/extension-config.json")
expect(getApiUrl("/marketplace/modes")).toBe("https://kilocode.ai/api/marketplace/modes")
expect(getApiUrl("/marketplace/mcps")).toBe("https://kilocode.ai/api/marketplace/mcps")
expect(getApiUrl("/profile/balance")).toBe("https://kilocode.ai/api/profile/balance")
expect(getApiUrl()).toBe("https://kilocode.ai/api")
})

it("should handle development environment", () => {
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"

expect(getApiUrl("/extension-config.json")).toBe("http://localhost:3000/api/extension-config.json")
expect(getApiUrl("/marketplace/modes")).toBe("http://localhost:3000/api/marketplace/modes")
expect(getApiUrl("/marketplace/mcps")).toBe("http://localhost:3000/api/marketplace/mcps")
expect(getApiUrl()).toBe("http://localhost:3000/api")
})

it("should handle paths without leading slash", () => {
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
expect(getApiUrl("extension-config.json")).toBe("http://localhost:3000/api/extension-config.json")
})

it("should handle empty and root paths", () => {
expect(getApiUrl("")).toBe("https://kilocode.ai/api")
expect(getApiUrl("/")).toBe("https://kilocode.ai/api/")
})
})

describe("getAppUrl", () => {
it("should handle production URLs correctly", () => {
expect(getAppUrl()).toBe("https://kilocode.ai")
expect(getAppUrl("/profile")).toBe("https://kilocode.ai/profile")
expect(getAppUrl("/support")).toBe("https://kilocode.ai/support")
expect(getAppUrl("/sign-in-to-editor")).toBe("https://kilocode.ai/sign-in-to-editor")
})

it("should handle development environment", () => {
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"

expect(getAppUrl()).toBe("http://localhost:3000")
expect(getAppUrl("/profile")).toBe("http://localhost:3000/profile")
expect(getAppUrl("/support")).toBe("http://localhost:3000/support")
})

it("should handle paths without leading slash", () => {
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:3000"
expect(getAppUrl("profile")).toBe("http://localhost:3000/profile")
})

it("should handle empty and root paths", () => {
expect(getAppUrl("")).toBe("https://kilocode.ai")
expect(getAppUrl("/")).toBe("https://kilocode.ai")
})
})

describe("getKiloUrlFromToken", () => {
it("should handle production token URLs correctly", () => {
const prodToken = createProdToken()

// Token-based URLs using api.kilocode.ai subdomain
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile", prodToken)).toBe(
"https://api.kilocode.ai/api/profile",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile/balance", prodToken)).toBe(
"https://api.kilocode.ai/api/profile/balance",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/organizations/123/defaults", prodToken)).toBe(
"https://api.kilocode.ai/api/organizations/123/defaults",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/openrouter/", prodToken)).toBe(
"https://api.kilocode.ai/api/openrouter/",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/users/notifications", prodToken)).toBe(
"https://api.kilocode.ai/api/users/notifications",
)
})

it("should map development tokens to localhost correctly", () => {
const devToken = createDevToken()

// Development token should map to localhost:3000
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile", devToken)).toBe(
"http://localhost:3000/api/profile",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile/balance", devToken)).toBe(
"http://localhost:3000/api/profile/balance",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/organizations/456/defaults", devToken)).toBe(
"http://localhost:3000/api/organizations/456/defaults",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/openrouter/", devToken)).toBe(
"http://localhost:3000/api/openrouter/",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/users/notifications", devToken)).toBe(
"http://localhost:3000/api/users/notifications",
)
})

it("should handle invalid tokens gracefully", () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
// Use a token that looks like JWT but has invalid JSON payload
const result = getKiloUrlFromToken("https://api.kilocode.ai/api/test", "header.invalid-json.signature")
expect(result).toBe("https://api.kilocode.ai/api/test")
expect(consoleSpy).toHaveBeenCalledWith("Failed to get base URL from Kilo Code token")
consoleSpy.mockRestore()
})
})

describe("Real-world URL patterns from application", () => {
it("should correctly handle marketplace endpoints", () => {
// These are the actual endpoints used in RemoteConfigLoader
expect(getApiUrl("/marketplace/modes")).toBe("https://kilocode.ai/api/marketplace/modes")
expect(getApiUrl("/marketplace/mcps")).toBe("https://kilocode.ai/api/marketplace/mcps")
})

it("should correctly handle app navigation URLs", () => {
// These are the actual URLs used in Task.ts and webviewMessageHandler.ts
expect(getAppUrl("/profile")).toBe("https://kilocode.ai/profile")
expect(getAppUrl("/support")).toBe("https://kilocode.ai/support")
})

it("should correctly handle token-based API calls", () => {
// These are the actual API endpoints used throughout the application
const prodToken = createProdToken()
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile", prodToken)).toBe(
"https://api.kilocode.ai/api/profile",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/profile/balance", prodToken)).toBe(
"https://api.kilocode.ai/api/profile/balance",
)
expect(getKiloUrlFromToken("https://api.kilocode.ai/api/users/notifications", prodToken)).toBe(
"https://api.kilocode.ai/api/users/notifications",
)
})

it("should maintain backwards compatibility for legacy endpoints", () => {
expect(getExtensionConfigUrl()).toBe("https://api.kilocode.ai/extension-config.json")
expect(getApiUrl("/extension-config.json")).toBe("https://kilocode.ai/api/extension-config.json")
expect(getApiUrl("/extension-config.json")).not.toBe(getExtensionConfigUrl())
})
})

describe("Edge cases and error handling", () => {
it("should handle various localhost configurations", () => {
process.env.KILOCODE_BACKEND_BASE_URL = "http://localhost:8080"
expect(getApiUrl("/test")).toBe("http://localhost:8080/api/test")

process.env.KILOCODE_BACKEND_BASE_URL = "http://127.0.0.1:3000"
expect(getApiUrl("/test")).toBe("http://127.0.0.1:3000/api/test")
})

it("should handle custom backend URLs", () => {
process.env.KILOCODE_BACKEND_BASE_URL = "https://staging.example.com"

expect(getAppUrl()).toBe("https://staging.example.com")
expect(getApiUrl("/test")).toBe("https://staging.example.com/api/test")
expect(getAppUrl("/dashboard")).toBe("https://staging.example.com/dashboard")
})
})
})
90 changes: 90 additions & 0 deletions packages/types/src/kilocode/kilocode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const fastApplyModelSchema = z.enum([

export type FastApplyModel = z.infer<typeof fastApplyModelSchema>

export const DEFAULT_KILOCODE_BACKEND_URL = "https://kilocode.ai"

export function getKiloBaseUriFromToken(kilocodeToken?: string) {
if (kilocodeToken) {
try {
Expand All @@ -56,6 +58,94 @@ export function getKiloBaseUriFromToken(kilocodeToken?: string) {
return "https://api.kilocode.ai"
}

/**
* Helper function that combines token-based base URL resolution with URL construction.
* Takes a token and a full URL, uses the token to get the appropriate base URL,
* then constructs the final URL by replacing the domain in the target URL.
*
* @param targetUrl The target URL to transform
* @param kilocodeToken The KiloCode authentication token
* @returns Fully constructed KiloCode URL with proper backend mapping based on token
*/
export function getKiloUrlFromToken(targetUrl: string, kilocodeToken?: string): string {
const baseUrl = getKiloBaseUriFromToken(kilocodeToken)
const target = new URL(targetUrl)

const { protocol, hostname, port } = new URL(baseUrl)
Object.assign(target, { protocol, hostname, port })
return target.toString()
}

function getGlobalKilocodeBackendUrl(): string {
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(typeof window !== "undefined" ? (window as any).KILOCODE_BACKEND_BASE_URL : undefined) ||
process.env.KILOCODE_BACKEND_BASE_URL ||
DEFAULT_KILOCODE_BACKEND_URL
)
}

function removeTrailingSlash(url: string, pathname: string): string {
return url.endsWith("/") && (pathname === "/" || pathname === "") ? url.slice(0, -1) : url
}

function ensureLeadingSlash(path: string): string {
return path.startsWith("/") ? path : `/${path}`
}

/**
* Internal helper to build URLs for the current environment.
*/
function buildUrl(path: string = ""): string {
try {
const backend = new URL(getGlobalKilocodeBackendUrl())
const result = new URL(backend)
result.pathname = path ? ensureLeadingSlash(path) : ""

return removeTrailingSlash(result.toString(), result.pathname)
} catch (error) {
console.warn("Failed to build URL:", path, error)
return `https://kilocode.ai${path ? ensureLeadingSlash(path) : ""}`
}
}

/**
* Gets the app/web URL for the current environment.
* In development: http://localhost:3000
* In production: https://kilocode.ai
*/
export function getAppUrl(path: string = ""): string {
return buildUrl(path)
}

/**
* Gets the API base URL for the current environment.
* In development: http://localhost:3000/api
* In production: https://kilocode.ai/api
*/
export function getApiUrl(path: string = ""): string {
return buildUrl(`/api${path ? ensureLeadingSlash(path) : ""}`)
}

/**
* Gets the extension config URL, which uses a legacy subdomain structure.
* In development: http://localhost:3000/extension-config.json
* In production: https://api.kilocode.ai/extension-config.json
*/
export function getExtensionConfigUrl(): string {
try {
const backend = getGlobalKilocodeBackendUrl()
if (backend.includes("localhost")) {
return getAppUrl("/extension-config.json")
} else {
return "https://api.kilocode.ai/extension-config.json"
}
} catch (error) {
console.warn("Failed to build extension config URL:", error)
return "https://api.kilocode.ai/extension-config.json"
}
}

/**
* Check if the Kilocode account has a positive balance
* @param kilocodeToken - The Kilocode JWT token
Expand Down
Loading