Skip to content
Open
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
1 change: 1 addition & 0 deletions agent-support/kilo-code/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
54 changes: 54 additions & 0 deletions agent-support/kilo-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# git-ai Plugin for Kilo Code

A plugin that integrates [git-ai](https://github.com/git-ai-project/git-ai) with [Kilo Code](https://kilo.ai) to automatically track AI-generated code.

## Overview

This plugin hooks into Kilo Code's tool execution lifecycle to create checkpoints that mark code changes as either human or AI-authored. It uses the `tool.execute.before` and `tool.execute.after` events to:

1. Create a human checkpoint before AI edits (marking any intermediate changes as human-authored)
2. Create an AI checkpoint after AI edits (marking the changes as AI-authored with model information)

## Installation

The plugin is automatically installed by `git-ai install-hooks`.

Build `git-ai` (`cargo build`) and then run the `git-ai install-hooks` or `cargo run -- install-hooks` command to test the entire flow of installing and using the plugin.

## Requirements

- [git-ai](https://github.com/git-ai-project/git-ai) must be installed and available in PATH
- [Kilo Code](https://kilo.ai) with plugin support

## How It Works

The plugin intercepts file editing operations (`edit` and `write` tools) and:

1. **Before AI edit**: Creates a human checkpoint to mark any changes since the last checkpoint as human-authored
2. **After AI edit**: Creates an AI checkpoint with:
- Model information (provider/model ID)
- Session/conversation ID
- List of edited file paths

If `git-ai` is not installed or the file is not in a git repository, the plugin gracefully skips checkpoint creation without breaking Kilo Code functionality.

## Development

### Type Checking

Run type checking:
```bash
yarn type-check
```

### Dependencies

Install dependencies:
```bash
yarn install
```

## See Also

- [git-ai Documentation](https://github.com/git-ai-project/git-ai)
- [Kilo Code](https://kilo.ai)
136 changes: 136 additions & 0 deletions agent-support/kilo-code/git-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* git-ai plugin for Kilo Code
*
* This plugin integrates git-ai with Kilo Code to track AI-generated code.
* It uses the tool.execute.before and tool.execute.after events to create
* checkpoints that mark code changes as human or AI-authored.
*
* Installation:
* - Automatically installed by `git-ai install-hooks`
* - Or manually copy to ~/.config/kilo/plugins/git-ai.ts (global)
* - Or to .kilo/plugins/git-ai.ts (project-local)
*
* Requirements:
* - git-ai must be installed (path is injected at install time)
*
* @see https://github.com/git-ai-project/git-ai
* @see https://kilo.ai
*/

import type { Plugin } from "@kilocode/plugin"
import { dirname } from "path"

// Absolute path to git-ai binary, replaced at install time by `git-ai install-hooks`
const GIT_AI_BIN = "__GIT_AI_BINARY_PATH__"

// Tools that modify files and should be tracked
const FILE_EDIT_TOOLS = ["edit", "write", "patch", "multiedit"]

export const GitAiPlugin: Plugin = async (ctx) => {
const { $ } = ctx

// Check if git-ai is installed
let gitAiInstalled = false
try {
await $`${GIT_AI_BIN} --version`.quiet()
gitAiInstalled = true
} catch {
// git-ai not installed, plugin will be a no-op
}

if (!gitAiInstalled) {
return {}
}

// Track pending edits by callID so we can reference them in the after hook
// Stores { filePath, repoDir, sessionID } for each pending edit
const pendingEdits = new Map<string, { filePath: string; repoDir: string; sessionID: string }>()

// Helper to find git repo root from a file path
const findGitRepo = async (filePath: string): Promise<string | null> => {
try {
const dir = dirname(filePath)
const result = await $`git -C ${dir} rev-parse --show-toplevel`.quiet()
const repoRoot = result.stdout.toString().trim()
return repoRoot || null
} catch {
// Not a git repo or git not available
return null
}
}

return {
"tool.execute.before": async (input, output) => {
// Only intercept file editing tools
if (!FILE_EDIT_TOOLS.includes(input.tool)) {
return
}

// Extract file path from tool arguments (args are in output, not input)
const filePath = output.args?.filePath as string | undefined
if (!filePath) {
return
}

// Find the git repo for this file
const repoDir = await findGitRepo(filePath)
if (!repoDir) {
// File is not in a git repo, skip silently
return
}

// Store filePath, repoDir, and sessionID for the after hook
pendingEdits.set(input.callID, { filePath, repoDir, sessionID: input.sessionID })

try {
// Create human checkpoint before AI edit
// This marks any changes since the last checkpoint as human-authored
const hookInput = JSON.stringify({
hook_event_name: "PreToolUse",
session_id: input.sessionID,
cwd: repoDir,
tool_input: { filePath },
})

await $`echo ${hookInput} | ${GIT_AI_BIN} checkpoint kilo-code --hook-input stdin`.quiet()
} catch (error) {
// Log to stderr for debugging, but don't throw - git-ai errors shouldn't break the agent
console.error("[git-ai] Failed to create human checkpoint:", String(error))
}
},

"tool.execute.after": async (input, _output) => {
// Only intercept file editing tools
if (!FILE_EDIT_TOOLS.includes(input.tool)) {
return
}

// Get the filePath and repoDir we stored in the before hook
const editInfo = pendingEdits.get(input.callID)
pendingEdits.delete(input.callID)

if (!editInfo) {
return
}

const { filePath, repoDir, sessionID } = editInfo

try {
// Create AI checkpoint after edit
// This marks the changes made by this tool call as AI-authored
// Transcript is fetched from Kilo Code's local storage by the preset
const hookInput = JSON.stringify({
hook_event_name: "PostToolUse",
session_id: sessionID,
cwd: repoDir,
tool_input: { filePath },
})

await $`echo ${hookInput} | ${GIT_AI_BIN} checkpoint kilo-code --hook-input stdin`.quiet()
} catch (error) {
// Log to stderr for debugging, but don't throw - git-ai errors shouldn't break the agent
console.error("[git-ai] Failed to create AI checkpoint:", String(error))
}
},
}
}
17 changes: 17 additions & 0 deletions agent-support/kilo-code/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "git-ai-kilo-code-plugin",
"version": "0.1.0",
"description": "git-ai plugin for Kilo Code",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"type-check": "tsc --noEmit"
},
"dependencies": {
"@kilocode/plugin": "^7.0.50"
},
"devDependencies": {
"@types/node": "^25.x",
"typescript": "^5.8.3"
}
}
17 changes: 17 additions & 0 deletions agent-support/kilo-code/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["node"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"noEmit": true
},
"include": ["*.ts"],
"exclude": ["node_modules"]
}
45 changes: 45 additions & 0 deletions src/authorship/prompt_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::commands::checkpoint_agent::agent_presets::{
GithubCopilotPreset, WindsurfPreset,
};
use crate::commands::checkpoint_agent::amp_preset::AmpPreset;
use crate::commands::checkpoint_agent::kilo_code_preset::KiloCodePreset;
use crate::commands::checkpoint_agent::opencode_preset::OpenCodePreset;
use crate::error::GitAiError;
use crate::git::refs::{get_authorship, grep_ai_notes};
Expand Down Expand Up @@ -178,6 +179,7 @@ pub fn update_prompt_from_tool(
"droid" => update_droid_prompt(agent_metadata, current_model),
"amp" => update_amp_prompt(external_thread_id, agent_metadata, current_model),
"opencode" => update_opencode_prompt(external_thread_id, agent_metadata, current_model),
"kilo-code" => update_kilo_code_prompt(external_thread_id, agent_metadata, current_model),
"windsurf" => update_windsurf_prompt(agent_metadata, current_model),
_ => {
debug_log(&format!("Unknown tool: {}", tool));
Expand Down Expand Up @@ -580,6 +582,49 @@ fn update_opencode_prompt(
}
}

/// Update Kilo Code prompt by fetching latest transcript from storage
fn update_kilo_code_prompt(
session_id: &str,
metadata: Option<&HashMap<String, String>>,
current_model: &str,
) -> PromptUpdateResult {
// Check for test storage path override in metadata or env var
let storage_path = if let Ok(env_path) = std::env::var("GIT_AI_KILO_CODE_STORAGE_PATH") {
Some(std::path::PathBuf::from(env_path))
} else {
metadata
.and_then(|m| m.get("__test_storage_path"))
.map(std::path::PathBuf::from)
};

let result = if let Some(path) = storage_path {
KiloCodePreset::transcript_and_model_from_storage(&path, session_id)
} else {
KiloCodePreset::transcript_and_model_from_session(session_id)
};

match result {
Ok((transcript, model)) => PromptUpdateResult::Updated(
transcript,
model.unwrap_or_else(|| current_model.to_string()),
),
Err(e) => {
debug_log(&format!(
"Failed to fetch Kilo Code transcript for session {}: {}",
session_id, e
));
log_error(
&e,
Some(serde_json::json!({
"agent_tool": "kilo-code",
"operation": "transcript_and_model_from_storage"
})),
);
PromptUpdateResult::Failed(e)
}
}
}

/// Update Windsurf prompt from transcript JSONL file
fn update_windsurf_prompt(
metadata: Option<&HashMap<String, String>>,
Expand Down
Loading