Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 4 additions & 1 deletion apps/kilocode-docs/docs/advanced-usage/slackbot.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The Kilo Slackbot brings the power of Kilo Code directly into your Slack workspa
Before using the Kilo Slackbot:

- You must have a **Kilo Code account** with available credits
- Your **GitHub Integration must be configured** via the [Integrations tab](https://app.kilo.ai/integrations) so the Slackbot can access your repositories
- Your **GitHub Integration must be configured** via the [Integrations tab](https://app.kilo.ai/integrations) so the Slackbot can access your repositories

To install the Kilo Slackbot, simply go to the integrations menu in the sidebar on https://app.kilo.ai and set up the Slack integration.

Expand All @@ -40,6 +40,7 @@ You can message the Kilo Slackbot directly through Slack DMs for private convers
3. Ask your question or describe what you need

This is ideal for:

- Private questions about your code
- Sensitive debugging sessions
- Personal productivity tasks
Expand All @@ -53,6 +54,7 @@ Mention the bot in any channel where it's been added:
```

This is great for:

- Team discussions where AI assistance would help
- Collaborative debugging sessions
- Getting quick answers during code reviews
Expand Down Expand Up @@ -82,6 +84,7 @@ When your team identifies an issue or improvement in a Slack thread, ask the bot
```

The bot can:

- Read the context from the thread
- Understand the proposed solution
- Create a branch with the implementation
Expand Down
156 changes: 101 additions & 55 deletions src/core/kilocode/agent-manager/CliPathResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,97 @@ import { execSync } from "node:child_process"
import { fileExistsAtPath } from "../../../utils/fs"
import { getLocalCliPath } from "./CliInstaller"

/**
* Case-insensitive lookup for environment variables.
* Windows environment variables can have inconsistent casing (PATH, Path, path).
*/
function getCaseInsensitive(target: NodeJS.ProcessEnv, key: string): string | undefined {
const lowercaseKey = key.toLowerCase()
const equivalentKey = Object.keys(target).find((k) => k.toLowerCase() === lowercaseKey)
return equivalentKey ? target[equivalentKey] : target[key]
}

/**
* Check if a path exists and is a file (not a directory).
* Follows symlinks - a symlink to a file returns true, symlink to a directory returns false.
*/
async function pathExistsAsFile(filePath: string): Promise<boolean> {
try {
const stat = await fs.promises.stat(filePath)
return stat.isFile()
} catch (e: unknown) {
if (e instanceof Error && "code" in e && e.code === "EACCES") {
try {
const lstat = await fs.promises.lstat(filePath)
return lstat.isFile() || lstat.isSymbolicLink()
} catch {
return false
}
}
return false
}
}

/**
* Find an executable by name, resolving it against PATH and PATHEXT (on Windows).
*/
export async function findExecutable(
command: string,
cwd?: string,
paths?: string[],
env: NodeJS.ProcessEnv = process.env,
): Promise<string | undefined> {
if (path.isAbsolute(command)) {
return (await pathExistsAsFile(command)) ? command : undefined
}

if (cwd === undefined) {
cwd = process.cwd()
}

const dir = path.dirname(command)
if (dir !== ".") {
const fullPath = path.join(cwd, command)
return (await pathExistsAsFile(fullPath)) ? fullPath : undefined
}

const envPath = getCaseInsensitive(env, "PATH")
if (paths === undefined && typeof envPath === "string") {
paths = envPath.split(path.delimiter)
}

if (paths === undefined || paths.length === 0) {
const fullPath = path.join(cwd, command)
return (await pathExistsAsFile(fullPath)) ? fullPath : undefined
}

for (const pathEntry of paths) {
let fullPath: string
if (path.isAbsolute(pathEntry)) {
fullPath = path.join(pathEntry, command)
} else {
fullPath = path.join(cwd, pathEntry, command)
}

if (process.platform === "win32") {
const pathExt = getCaseInsensitive(env, "PATHEXT") || ".COM;.EXE;.BAT;.CMD"
for (const ext of pathExt.split(";")) {
const withExtension = fullPath + ext
if (await pathExistsAsFile(withExtension)) {
return withExtension
}
}
}

if (await pathExistsAsFile(fullPath)) {
return fullPath
}
}

const fullPath = path.join(cwd, command)
return (await pathExistsAsFile(fullPath)) ? fullPath : undefined
}

/**
* Find the kilocode CLI executable.
*
Expand All @@ -12,7 +103,7 @@ import { getLocalCliPath } from "./CliInstaller"
* 2. Workspace-local build at <workspace>/cli/dist/index.js
* 3. Local installation at ~/.kilocode/cli/pkg (for immutable systems like NixOS)
* 4. Login shell lookup (respects user's nvm, fnm, volta, asdf config)
* 5. Direct PATH lookup (fallback for system-wide installs)
* 5. Direct PATH lookup using findExecutable (handles PATHEXT on Windows)
* 6. Common npm installation paths (last resort)
*
* IMPORTANT: Login shell is checked BEFORE direct PATH because:
Expand Down Expand Up @@ -50,22 +141,23 @@ export async function findKilocodeCli(log?: (msg: string) => void): Promise<stri
}

// 3) Check local installation (for immutable systems like NixOS)
// This is checked early because it's a deliberate user choice for systems that can't use global install
const localCliPath = getLocalCliPath()
if (await fileExistsAtPath(localCliPath)) {
log?.(`Found local CLI installation: ${localCliPath}`)
return localCliPath
}

// 4) Try login shell FIRST to pick up user's shell environment (nvm, fnm, volta, asdf, etc.)
// This is preferred because it respects the user's actual node environment.
// When we run `npm install -g`, it installs to this environment, so we should find CLI here.
const loginShellResult = findViaLoginShell(log)
if (loginShellResult) return loginShellResult

// 5) Fall back to direct PATH lookup (for users without version managers)
const directPathResult = findInPath(log)
if (directPathResult) return directPathResult
// 5) Use findExecutable to resolve CLI path (handles PATHEXT on Windows)
const executablePath = await findExecutable("kilocode")
if (executablePath) {
log?.(`Found CLI via PATH: ${executablePath}`)
return executablePath
}
log?.("kilocode not found in PATH lookup")

// 6) Last resort: scan common npm installation paths
log?.("Falling back to scanning common installation paths...")
Expand All @@ -84,53 +176,28 @@ export async function findKilocodeCli(log?: (msg: string) => void): Promise<stri
return null
}

/**
* Try to find kilocode in the current process PATH.
* This works when CLI is installed in a system-wide location.
*/
function findInPath(log?: (msg: string) => void): string | null {
const cmd = process.platform === "win32" ? "where kilocode" : "which kilocode"
try {
const result = execSync(cmd, { encoding: "utf-8", timeout: 5000 }).split(/\r?\n/)[0]?.trim()
if (result) {
log?.(`Found CLI in PATH: ${result}`)
return result
}
} catch {
log?.("kilocode not found in direct PATH lookup")
}
return null
}

/**
* Try to find kilocode by running `which` in a login shell.
* This sources the user's shell profile (~/.zshrc, ~/.bashrc, etc.)
* which sets up version managers like nvm, fnm, volta, asdf, etc.
*
* This is the most reliable way to find CLI installed via version managers
* because VS Code's extension host doesn't inherit the user's shell environment.
*/
function findViaLoginShell(log?: (msg: string) => void): string | null {
if (process.platform === "win32") {
// Windows doesn't have the same shell environment concept
return null
}

// Detect user's shell from SHELL env var, default to bash
const userShell = process.env.SHELL || "/bin/bash"
const shellName = path.basename(userShell)

// Use login shell (-l) to source profile files, interactive (-i) for some shells
// that only source certain files in interactive mode
const shellFlags = shellName === "zsh" ? "-l -i" : "-l"
const cmd = `${userShell} ${shellFlags} -c 'which kilocode' 2>/dev/null`

try {
log?.(`Trying login shell lookup: ${cmd}`)
const result = execSync(cmd, {
encoding: "utf-8",
timeout: 10000, // 10s timeout - login shells can be slow
env: { ...process.env, HOME: process.env.HOME }, // Ensure HOME is set
timeout: 10000,
env: { ...process.env, HOME: process.env.HOME },
})
.split(/\r?\n/)[0]
?.trim()
Expand All @@ -140,7 +207,6 @@ function findViaLoginShell(log?: (msg: string) => void): string | null {
return result
}
} catch (error) {
// This is expected if CLI is not installed or shell init is slow/broken
log?.(`Login shell lookup failed (this is normal if CLI not installed via version manager): ${error}`)
}

Expand All @@ -149,7 +215,6 @@ function findViaLoginShell(log?: (msg: string) => void): string | null {

/**
* Get fallback paths to check for CLI installation.
* This is used when login shell lookup fails or on Windows.
*/
function getNpmPaths(log?: (msg: string) => void): string[] {
const home = process.env.HOME || process.env.USERPROFILE || ""
Expand All @@ -164,27 +229,16 @@ function getNpmPaths(log?: (msg: string) => void): string[] {
].filter(Boolean)
}

// macOS and Linux paths
const paths = [
// Local installation (for immutable systems like NixOS)
getLocalCliPath(),
// macOS Homebrew (Apple Silicon)
"/opt/homebrew/bin/kilocode",
// macOS Homebrew (Intel) and Linux standard
"/usr/local/bin/kilocode",
// Common user-local npm prefix
path.join(home, ".npm-global", "bin", "kilocode"),
// nvm: scan installed versions
...getNvmPaths(home, log),
// fnm
path.join(home, ".local", "share", "fnm", "aliases", "default", "bin", "kilocode"),
// volta
path.join(home, ".volta", "bin", "kilocode"),
// asdf nodejs plugin
path.join(home, ".asdf", "shims", "kilocode"),
// Linux snap
"/snap/bin/kilocode",
// Linux user local bin
path.join(home, ".local", "bin", "kilocode"),
]

Expand All @@ -193,35 +247,27 @@ function getNpmPaths(log?: (msg: string) => void): string[] {

/**
* Get potential nvm paths for the kilocode CLI.
* nvm installs node versions in ~/.nvm/versions/node/
*
* Note: This is a fallback - the login shell approach (findViaLoginShell)
* is preferred because it respects the user's shell configuration.
*/
function getNvmPaths(home: string, log?: (msg: string) => void): string[] {
const nvmDir = process.env.NVM_DIR || path.join(home, ".nvm")
const versionsDir = path.join(nvmDir, "versions", "node")

const paths: string[] = []

// Check NVM_BIN if set (current nvm version in the shell)
if (process.env.NVM_BIN) {
paths.push(path.join(process.env.NVM_BIN, "kilocode"))
}

// Scan the nvm versions directory for installed node versions
try {
if (fs.existsSync(versionsDir)) {
const versions = fs.readdirSync(versionsDir)
// Sort versions in reverse order to check newer versions first
versions.sort().reverse()
log?.(`Found ${versions.length} nvm node versions to check`)
for (const version of versions) {
paths.push(path.join(versionsDir, version, "bin", "kilocode"))
}
}
} catch (error) {
// This is normal if user doesn't have nvm installed
log?.(`Could not scan nvm versions directory: ${error}`)
}

Expand Down
17 changes: 17 additions & 0 deletions src/core/kilocode/agent-manager/CliProcessHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawn, ChildProcess } from "node:child_process"
import * as path from "node:path"
import {
CliOutputParser,
type StreamEvent,
Expand Down Expand Up @@ -37,6 +38,7 @@ interface PendingProcessInfo {
gitUrl?: string
stderrBuffer: string[] // Capture stderr for error detection
timeoutId?: NodeJS.Timeout // Timer for auto-failing stuck pending sessions
cliPath?: string // CLI path for error telemetry
provisionalSessionId?: string // Temporary session ID created when api_req_started arrives (before session_created)
}

Expand Down Expand Up @@ -211,6 +213,7 @@ export class CliProcessHandler {
gitUrl: options?.gitUrl,
stderrBuffer: [],
timeoutId: setTimeout(() => this.handlePendingTimeout(), PENDING_SESSION_TIMEOUT_MS),
cliPath,
}
}

Expand Down Expand Up @@ -666,10 +669,24 @@ export class CliProcessHandler {

private handleProcessError(proc: ChildProcess, error: Error): void {
if (this.pendingProcess && this.pendingProcess.process === proc) {
const cliPath = this.pendingProcess.cliPath
this.clearPendingTimeout()
this.registry.clearPendingSession()
this.callbacks.onPendingSessionChanged(null)
this.pendingProcess = null

// Capture spawn error telemetry with context for debugging
const { platform, shell } = getPlatformDiagnostics()
const cliPathExtension = cliPath ? path.extname(cliPath).slice(1).toLowerCase() || undefined : undefined
captureAgentManagerLoginIssue({
issueType: "cli_spawn_error",
platform,
shell,
errorMessage: error.message,
cliPath,
cliPathExtension,
})

this.callbacks.onStartSessionFailed({
type: "spawn_error",
message: error.message,
Expand Down
Loading
Loading