Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
83581d9d18
|
Generated
+70
-14
@@ -1238,7 +1238,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -1338,6 +1338,22 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
@@ -1347,7 +1363,7 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crossterm_winapi",
|
||||
"filedescriptor",
|
||||
"mio",
|
||||
"mio 1.1.1",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"serde",
|
||||
@@ -1366,7 +1382,7 @@ dependencies = [
|
||||
"crossterm_winapi",
|
||||
"derive_more 2.1.1",
|
||||
"document-features",
|
||||
"mio",
|
||||
"mio 1.1.1",
|
||||
"parking_lot",
|
||||
"rustix 1.1.3",
|
||||
"signal-hook",
|
||||
@@ -2148,7 +2164,7 @@ version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2822,16 +2838,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inquire"
|
||||
version = "0.9.4"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
|
||||
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crossterm 0.29.0",
|
||||
"crossterm 0.25.0",
|
||||
"dyn-clone",
|
||||
"fuzzy-matcher",
|
||||
"fxhash",
|
||||
"newline-converter",
|
||||
"once_cell",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3223,7 +3242,7 @@ dependencies = [
|
||||
"tokio-graceful",
|
||||
"tokio-stream",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
@@ -3438,6 +3457,18 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
@@ -3502,6 +3533,15 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "newline-converter"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
@@ -4515,7 +4555,7 @@ dependencies = [
|
||||
"strum_macros 0.26.4",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5337,7 +5377,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 0.8.11",
|
||||
"mio 1.1.1",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
@@ -5641,7 +5682,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memchr",
|
||||
"mio",
|
||||
"mio 1.1.1",
|
||||
"terminal-trx",
|
||||
"windows-sys 0.59.0",
|
||||
"xterm-color",
|
||||
@@ -5676,7 +5717,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5821,7 +5862,7 @@ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.1.1",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
@@ -6252,6 +6293,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
@@ -6919,6 +6966,15 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ bytes = "1.4.0"
|
||||
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
||||
dirs = "6.0.0"
|
||||
futures-util = "0.3.29"
|
||||
inquire = "0.9.4"
|
||||
inquire = "0.7.0"
|
||||
is-terminal = "0.4.9"
|
||||
reedline = "0.40.0"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
|
||||
@@ -402,6 +402,7 @@ invoke_agent_with_summary() {
|
||||
summary=$(echo "${output}" | sed -n '/^## Recommendation/,/^## /{/^## Recommendation/d;/^## /d;p}' | sed '/^$/d' | head -10)
|
||||
fi
|
||||
|
||||
# Failsafe: extract up to 5 meaningful lines if no markers found
|
||||
if [[ -z "${summary}" ]]; then
|
||||
summary=$(echo "${output}" | grep -v "^$" | grep -v "^#" | grep -v "^\-\-\-" | tail -10 | head -5)
|
||||
fi
|
||||
@@ -410,11 +411,7 @@ invoke_agent_with_summary() {
|
||||
append_context "${agent}" "${summary}"
|
||||
fi
|
||||
|
||||
if [[ -z "${output}" ]] || [[ -z "$(echo "${output}" | tr -d '[:space:]')" ]]; then
|
||||
echo "[${agent} agent completed but produced no text output. The agent may have performed work via tool calls (file writes, builds, etc.) that did not generate visible text. Check the project directory for changes.]"
|
||||
else
|
||||
echo "${output}"
|
||||
fi
|
||||
echo "${output}"
|
||||
}
|
||||
|
||||
###########################
|
||||
|
||||
@@ -33,7 +33,7 @@ instructions: |
|
||||
2. Study existing patterns (read 1-2 similar files)
|
||||
3. Write the code (using tools, NOT chat output)
|
||||
4. Verify it compiles/builds
|
||||
5. Signal completion with a summary
|
||||
5. Signal completion
|
||||
|
||||
## Todo System
|
||||
|
||||
@@ -82,13 +82,12 @@ instructions: |
|
||||
|
||||
## Completion Signal
|
||||
|
||||
When done, end your response with a summary so the parent agent knows what happened:
|
||||
|
||||
End with:
|
||||
```
|
||||
CODER_COMPLETE: [summary of what was implemented, which files were created/modified, and build status]
|
||||
CODER_COMPLETE: [summary of what was implemented]
|
||||
```
|
||||
|
||||
Or if something went wrong:
|
||||
Or if failed:
|
||||
```
|
||||
CODER_FAILED: [what went wrong]
|
||||
```
|
||||
@@ -105,3 +104,4 @@ instructions: |
|
||||
- Project: {{project_dir}}
|
||||
- CWD: {{__cwd__}}
|
||||
- Shell: {{__shell__}}
|
||||
|
||||
|
||||
@@ -14,28 +14,11 @@ _project_dir() {
|
||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||
}
|
||||
|
||||
# Normalize a path to be relative to project root.
|
||||
# Strips the project_dir prefix if the LLM passes an absolute path.
|
||||
# Usage: local rel_path; rel_path=$(_normalize_path "/abs/or/rel/path")
|
||||
_normalize_path() {
|
||||
local input_path="$1"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
if [[ "${input_path}" == /* ]]; then
|
||||
input_path="${input_path#"${project_dir}"/}"
|
||||
fi
|
||||
|
||||
input_path="${input_path#./}"
|
||||
echo "${input_path}"
|
||||
}
|
||||
|
||||
# @cmd Read a file's contents before modifying
|
||||
# @option --path! Path to the file (relative to project root)
|
||||
read_file() {
|
||||
local file_path
|
||||
# shellcheck disable=SC2154
|
||||
file_path=$(_normalize_path "${argc_path}")
|
||||
local file_path="${argc_path}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
local full_path="${project_dir}/${file_path}"
|
||||
@@ -56,8 +39,7 @@ read_file() {
|
||||
# @option --path! Path for the file (relative to project root)
|
||||
# @option --content! Complete file contents to write
|
||||
write_file() {
|
||||
local file_path
|
||||
file_path=$(_normalize_path "${argc_path}")
|
||||
local file_path="${argc_path}"
|
||||
# shellcheck disable=SC2154
|
||||
local content="${argc_content}"
|
||||
local project_dir
|
||||
@@ -65,7 +47,7 @@ write_file() {
|
||||
local full_path="${project_dir}/${file_path}"
|
||||
|
||||
mkdir -p "$(dirname "${full_path}")"
|
||||
printf '%s' "${content}" > "${full_path}"
|
||||
echo "${content}" > "${full_path}"
|
||||
|
||||
green "Wrote: ${file_path}" >> "$LLM_OUTPUT"
|
||||
}
|
||||
@@ -73,8 +55,7 @@ write_file() {
|
||||
# @cmd Find files similar to a given path (for pattern matching)
|
||||
# @option --path! Path to find similar files for
|
||||
find_similar_files() {
|
||||
local file_path
|
||||
file_path=$(_normalize_path "${argc_path}")
|
||||
local file_path="${argc_path}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
@@ -90,14 +71,14 @@ find_similar_files() {
|
||||
! -name "$(basename "${file_path}")" \
|
||||
! -name "*test*" \
|
||||
! -name "*spec*" \
|
||||
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
|
||||
2>/dev/null | head -3)
|
||||
|
||||
if [[ -z "${results}" ]]; then
|
||||
results=$(find "${project_dir}/src" -type f -name "*.${ext}" \
|
||||
! -name "*test*" \
|
||||
! -name "*spec*" \
|
||||
-not -path '*/target/*' \
|
||||
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
|
||||
2>/dev/null | head -3)
|
||||
fi
|
||||
|
||||
if [[ -n "${results}" ]]; then
|
||||
@@ -205,7 +186,6 @@ search_code() {
|
||||
grep -v '/target/' | \
|
||||
grep -v '/node_modules/' | \
|
||||
grep -v '/.git/' | \
|
||||
sed "s|^${project_dir}/||" | \
|
||||
head -20) || true
|
||||
|
||||
if [[ -n "${results}" ]]; then
|
||||
|
||||
@@ -8,13 +8,12 @@ variables:
|
||||
description: Project directory to explore
|
||||
default: '.'
|
||||
|
||||
mcp_servers:
|
||||
- ddg-search
|
||||
global_tools:
|
||||
- fs_read.sh
|
||||
- fs_grep.sh
|
||||
- fs_glob.sh
|
||||
- fs_ls.sh
|
||||
- web_search_loki.sh
|
||||
|
||||
instructions: |
|
||||
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
||||
|
||||
@@ -14,21 +14,6 @@ _project_dir() {
|
||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||
}
|
||||
|
||||
# Normalize a path to be relative to project root.
|
||||
# Strips the project_dir prefix if the LLM passes an absolute path.
|
||||
_normalize_path() {
|
||||
local input_path="$1"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
if [[ "${input_path}" == /* ]]; then
|
||||
input_path="${input_path#"${project_dir}"/}"
|
||||
fi
|
||||
|
||||
input_path="${input_path#./}"
|
||||
echo "${input_path}"
|
||||
}
|
||||
|
||||
# @cmd Get project structure and layout
|
||||
get_structure() {
|
||||
local project_dir
|
||||
@@ -93,7 +78,6 @@ search_content() {
|
||||
grep -v '/node_modules/' | \
|
||||
grep -v '/.git/' | \
|
||||
grep -v '/dist/' | \
|
||||
sed "s|^${project_dir}/||" | \
|
||||
head -30) || true
|
||||
|
||||
if [[ -n "${results}" ]]; then
|
||||
@@ -107,9 +91,8 @@ search_content() {
|
||||
# @option --path! Path to the file (relative to project root)
|
||||
# @option --lines Maximum lines to read (default: 200)
|
||||
read_file() {
|
||||
local file_path
|
||||
# shellcheck disable=SC2154
|
||||
file_path=$(_normalize_path "${argc_path}")
|
||||
local file_path="${argc_path}"
|
||||
local max_lines="${argc_lines:-200}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
@@ -139,8 +122,7 @@ read_file() {
|
||||
# @cmd Find similar files to a given file (for pattern matching)
|
||||
# @option --path! Path to the reference file
|
||||
find_similar() {
|
||||
local file_path
|
||||
file_path=$(_normalize_path "${argc_path}")
|
||||
local file_path="${argc_path}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
@@ -156,7 +138,7 @@ find_similar() {
|
||||
! -name "$(basename "${file_path}")" \
|
||||
! -name "*test*" \
|
||||
! -name "*spec*" \
|
||||
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
|
||||
2>/dev/null | head -5)
|
||||
|
||||
if [[ -n "${results}" ]]; then
|
||||
echo "${results}" >> "$LLM_OUTPUT"
|
||||
@@ -165,7 +147,7 @@ find_similar() {
|
||||
! -name "$(basename "${file_path}")" \
|
||||
! -name "*test*" \
|
||||
-not -path '*/target/*' \
|
||||
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
|
||||
2>/dev/null | head -5)
|
||||
if [[ -n "${results}" ]]; then
|
||||
echo "${results}" >> "$LLM_OUTPUT"
|
||||
else
|
||||
|
||||
@@ -8,13 +8,12 @@ variables:
|
||||
description: Project directory for context
|
||||
default: '.'
|
||||
|
||||
mcp_servers:
|
||||
- ddg-search
|
||||
global_tools:
|
||||
- fs_read.sh
|
||||
- fs_grep.sh
|
||||
- fs_glob.sh
|
||||
- fs_ls.sh
|
||||
- web_search_loki.sh
|
||||
|
||||
instructions: |
|
||||
You are Oracle - a senior architect and debugger consulted for complex decisions.
|
||||
|
||||
@@ -14,38 +14,21 @@ _project_dir() {
|
||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${dir}"
|
||||
}
|
||||
|
||||
# Normalize a path to be relative to project root.
|
||||
# Strips the project_dir prefix if the LLM passes an absolute path.
|
||||
_normalize_path() {
|
||||
local input_path="$1"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
|
||||
if [[ "${input_path}" == /* ]]; then
|
||||
input_path="${input_path#"${project_dir}"/}"
|
||||
fi
|
||||
|
||||
input_path="${input_path#./}"
|
||||
echo "${input_path}"
|
||||
}
|
||||
|
||||
# @cmd Read a file for analysis
|
||||
# @option --path! Path to the file (relative to project root)
|
||||
read_file() {
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
local file_path
|
||||
# shellcheck disable=SC2154
|
||||
file_path=$(_normalize_path "${argc_path}")
|
||||
local full_path="${project_dir}/${file_path}"
|
||||
local full_path="${project_dir}/${argc_path}"
|
||||
|
||||
if [[ ! -f "${full_path}" ]]; then
|
||||
error "File not found: ${file_path}" >> "$LLM_OUTPUT"
|
||||
error "File not found: ${argc_path}" >> "$LLM_OUTPUT"
|
||||
return 1
|
||||
fi
|
||||
|
||||
{
|
||||
info "Reading: ${file_path}"
|
||||
info "Reading: ${argc_path}"
|
||||
echo ""
|
||||
cat "${full_path}"
|
||||
} >> "$LLM_OUTPUT"
|
||||
@@ -97,7 +80,6 @@ search_code() {
|
||||
grep -v '/target/' | \
|
||||
grep -v '/node_modules/' | \
|
||||
grep -v '/.git/' | \
|
||||
sed "s|^${project_dir}/||" | \
|
||||
head -30) || true
|
||||
|
||||
if [[ -n "${results}" ]]; then
|
||||
@@ -131,8 +113,7 @@ analyze_with_command() {
|
||||
# @cmd List directory contents
|
||||
# @option --path Path to list (default: project root)
|
||||
list_directory() {
|
||||
local dir_path
|
||||
dir_path=$(_normalize_path "${argc_path:-.}")
|
||||
local dir_path="${argc_path:-.}"
|
||||
local project_dir
|
||||
project_dir=$(_project_dir)
|
||||
local full_path="${project_dir}/${dir_path}"
|
||||
|
||||
@@ -22,13 +22,12 @@ variables:
|
||||
description: Auto-confirm command execution
|
||||
default: '1'
|
||||
|
||||
mcp_servers:
|
||||
- ddg-search
|
||||
global_tools:
|
||||
- fs_read.sh
|
||||
- fs_grep.sh
|
||||
- fs_glob.sh
|
||||
- fs_ls.sh
|
||||
- web_search_loki.sh
|
||||
- execute_command.sh
|
||||
|
||||
instructions: |
|
||||
|
||||
@@ -16,15 +16,11 @@
|
||||
},
|
||||
"atlassian": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/mcp"]
|
||||
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/sse"]
|
||||
},
|
||||
"docker": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-docker"]
|
||||
},
|
||||
"ddg-search": {
|
||||
"command": "uvx",
|
||||
"args": ["duckduckgo-mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ max_concurrent_agents: 4 # Maximum number of agents that can run simulta
|
||||
max_agent_depth: 3 # Maximum nesting depth for sub-agents (prevents runaway spawning)
|
||||
inject_spawn_instructions: true # Inject the default agent spawning instructions into the agent's system prompt
|
||||
summarization_model: null # Model to use for summarizing sub-agent output (e.g. 'openai:gpt-4o-mini'); defaults to current model
|
||||
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
||||
summarization_threshold: 4000 # Character threshold above which sub-agent output is summarized before returning to parent
|
||||
escalation_timeout: 300 # Seconds a sub-agent waits for a user interaction response before timing out (default: 5 minutes)
|
||||
mcp_servers: # Optional list of MCP servers that the agent utilizes
|
||||
- github # Corresponds to the name of an MCP server in the `<loki-config-dir>/functions/mcp.json` file
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ visible_tools: # Which tools are visible to be compiled (and a
|
||||
mcp_server_support: true # Enables or disables MCP servers (globally).
|
||||
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
||||
git: github,gitmcp
|
||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack')
|
||||
|
||||
# ---- Session ----
|
||||
# See the [Session documentation](./docs/SESSIONS.md) for more information
|
||||
|
||||
@@ -714,7 +714,6 @@ Loki comes packaged with some useful built-in agents:
|
||||
* `code-reviewer`: A [CodeRabbit](https://coderabbit.ai)-style code reviewer that spawns per-file reviewers using the teammate messaging pattern
|
||||
* `demo`: An example agent to use for reference when learning to create your own agents
|
||||
* `explore`: An agent designed to help you explore and understand your codebase
|
||||
* `file-reviewer`: An agent designed to perform code-review on a single file (used by the `code-reviewer` agent)
|
||||
* `jira-helper`: An agent that assists you with all your Jira-related tasks
|
||||
* `oracle`: An agent for high-level architecture, design decisions, and complex debugging
|
||||
* `sisyphus`: A powerhouse orchestrator agent for writing complex code and acting as a natural language interface for your codebase (similar to ClaudeCode, Gemini CLI, Codex, or OpenCode). Uses sub-agent spawning to delegate to `explore`, `coder`, and `oracle`.
|
||||
|
||||
@@ -142,25 +142,6 @@ temporary localhost server to capture the callback automatically (e.g. Gemini) o
|
||||
code back into the terminal (e.g. Claude). Loki stores the tokens in `~/.cache/loki/oauth` and automatically refreshes
|
||||
them when they expire.
|
||||
|
||||
#### Gemini OAuth Note
|
||||
Loki uses the following scopes for OAuth with Gemini:
|
||||
* https://www.googleapis.com/auth/generative-language.peruserquota
|
||||
* https://www.googleapis.com/auth/userinfo.email
|
||||
* https://www.googleapis.com/auth/generative-language.retriever (Sensitive)
|
||||
|
||||
Since the `generative-language.retriever` scope is a sensitive scope, Google needs to verify Loki, which requires full
|
||||
branding (logo, official website, privacy policy, terms of service, etc.). The Loki app is open-source and is designed
|
||||
to be used as a simple CLI. As such, there's no terms of service or privacy policy associated with it, and thus Google
|
||||
cannot verify Loki.
|
||||
|
||||
So, when you kick off OAuth with Gemini, you may see a page similar to the following:
|
||||

|
||||
|
||||
Simply click the `Advanced` link and click `Go to Loki (unsafe)` to continue the OAuth flow.
|
||||
|
||||

|
||||

|
||||
|
||||
**Step 3: Use normally**
|
||||
|
||||
Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
|
||||
|
||||
@@ -55,7 +55,6 @@ Loki ships with a `functions/mcp.json` file that includes some useful MCP server
|
||||
* [github](https://github.com/github/github-mcp-server) - Interact with GitHub repositories, issues, pull requests, and more.
|
||||
* [docker](https://github.com/ckreiling/mcp-server-docker) - Manage your local Docker containers with natural language
|
||||
* [slack](https://github.com/korotovsky/slack-mcp-server) - Interact with Slack
|
||||
* [ddg-search](https://github.com/nickclyde/duckduckgo-mcp-server) - Perform web searches with the DuckDuckGo search engine
|
||||
|
||||
## Loki Configuration
|
||||
MCP servers, like tools, can be used in a handful of contexts:
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
# - https://platform.openai.com/docs/api-reference/chat
|
||||
- provider: openai
|
||||
models:
|
||||
- name: gpt-5.2
|
||||
max_input_tokens: 400000
|
||||
max_output_tokens: 128000
|
||||
input_price: 1.75
|
||||
output_price: 14
|
||||
supports_vision: true
|
||||
supports_function_calling: true
|
||||
- name: gpt-5.1
|
||||
max_input_tokens: 400000
|
||||
max_output_tokens: 128000
|
||||
|
||||
@@ -11,7 +11,6 @@ use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const API_BASE: &str = "https://api.anthropic.com/v1";
|
||||
const CLAUDE_CODE_PREFIX: &str = "You are Claude Code, Anthropic's official CLI for Claude.";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ClaudeConfig {
|
||||
@@ -95,7 +94,6 @@ async fn prepare_chat_completions(
|
||||
for (key, value) in provider.extra_request_headers() {
|
||||
request_data.header(key, value);
|
||||
}
|
||||
inject_oauth_system_prompt(&mut request_data.body);
|
||||
} else if let Ok(api_key) = self_.get_api_key() {
|
||||
request_data.header("x-api-key", api_key);
|
||||
} else {
|
||||
@@ -109,43 +107,6 @@ async fn prepare_chat_completions(
|
||||
Ok(request_data)
|
||||
}
|
||||
|
||||
/// Anthropic requires OAuth-authenticated requests to include a Claude Code
|
||||
/// system prompt prefix in order to consider a request body as "valid".
|
||||
///
|
||||
/// This behavior was discovered 2026-03-17.
|
||||
///
|
||||
/// So this function injects the Claude Code system prompt into the request
|
||||
/// body to make it a valid request.
|
||||
fn inject_oauth_system_prompt(body: &mut Value) {
|
||||
let prefix_block = json!({
|
||||
"type": "text",
|
||||
"text": CLAUDE_CODE_PREFIX,
|
||||
});
|
||||
|
||||
match body.get("system") {
|
||||
Some(Value::String(existing)) => {
|
||||
let existing_block = json!({
|
||||
"type": "text",
|
||||
"text": existing,
|
||||
});
|
||||
body["system"] = json!([prefix_block, existing_block]);
|
||||
}
|
||||
Some(Value::Array(_)) => {
|
||||
if let Some(arr) = body["system"].as_array_mut() {
|
||||
let already_injected = arr
|
||||
.iter()
|
||||
.any(|block| block["text"].as_str() == Some(CLAUDE_CODE_PREFIX));
|
||||
if !already_injected {
|
||||
arr.insert(0, prefix_block);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
body["system"] = json!([prefix_block]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn claude_chat_completions(
|
||||
builder: RequestBuilder,
|
||||
_model: &Model,
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::oauth::{OAuthProvider, TokenRequestFormat};
|
||||
|
||||
pub struct GeminiOAuthProvider;
|
||||
|
||||
// TODO: Replace with real credentials after registering Loki with Google Cloud Console
|
||||
const GEMINI_CLIENT_ID: &str =
|
||||
"50826443741-upqcebrs4gctqht1f08ku46qlbirkdsj.apps.googleusercontent.com";
|
||||
const GEMINI_CLIENT_SECRET: &str = "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";
|
||||
@@ -28,7 +29,7 @@ impl OAuthProvider for GeminiOAuthProvider {
|
||||
}
|
||||
|
||||
fn scopes(&self) -> &str {
|
||||
"https://www.googleapis.com/auth/generative-language.peruserquota https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/userinfo.email"
|
||||
"https://www.googleapis.com/auth/cloud-platform.readonly https://www.googleapis.com/auth/userinfo.email"
|
||||
}
|
||||
|
||||
fn client_secret(&self) -> Option<&str> {
|
||||
|
||||
@@ -177,10 +177,6 @@ impl Model {
|
||||
self.data.max_output_tokens
|
||||
}
|
||||
|
||||
pub fn supports_function_calling(&self) -> bool {
|
||||
self.data.supports_function_calling
|
||||
}
|
||||
|
||||
pub fn no_stream(&self) -> bool {
|
||||
self.data.no_stream
|
||||
}
|
||||
|
||||
+21
-20
@@ -1,12 +1,12 @@
|
||||
use super::ClientConfig;
|
||||
use super::access_token::{is_valid_access_token, set_access_token};
|
||||
use crate::config::Config;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use anyhow::{Result, bail};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::Utc;
|
||||
use inquire::Text;
|
||||
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
||||
use reqwest::Client as ReqwestClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -76,6 +76,7 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
|
||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let uri = format!("http://127.0.0.1:{port}/callback");
|
||||
// Drop the listener so run_oauth_flow can re-bind below
|
||||
drop(listener);
|
||||
uri
|
||||
} else {
|
||||
@@ -148,15 +149,15 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
|
||||
|
||||
let access_token = response["access_token"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Missing access_token in response: {response}"))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing access_token in response: {response}"))?
|
||||
.to_string();
|
||||
let refresh_token = response["refresh_token"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Missing refresh_token in response: {response}"))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing refresh_token in response: {response}"))?
|
||||
.to_string();
|
||||
let expires_in = response["expires_in"]
|
||||
.as_i64()
|
||||
.ok_or_else(|| anyhow!("Missing expires_in in response: {response}"))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing expires_in in response: {response}"))?;
|
||||
|
||||
let expires_at = Utc::now().timestamp() + expires_in;
|
||||
|
||||
@@ -213,7 +214,7 @@ pub async fn refresh_oauth_token(
|
||||
|
||||
let access_token = response["access_token"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Missing access_token in refresh response: {response}"))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing access_token in refresh response: {response}"))?
|
||||
.to_string();
|
||||
let refresh_token = response["refresh_token"]
|
||||
.as_str()
|
||||
@@ -221,7 +222,7 @@ pub async fn refresh_oauth_token(
|
||||
.unwrap_or_else(|| tokens.refresh_token.clone());
|
||||
let expires_in = response["expires_in"]
|
||||
.as_i64()
|
||||
.ok_or_else(|| anyhow!("Missing expires_in in refresh response: {response}"))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing expires_in in refresh response: {response}"))?;
|
||||
|
||||
let expires_at = Utc::now().timestamp() + expires_in;
|
||||
|
||||
@@ -265,7 +266,7 @@ fn build_token_request(
|
||||
client: &ReqwestClient,
|
||||
provider: &(impl OAuthProvider + ?Sized),
|
||||
params: &[(&str, &str)],
|
||||
) -> RequestBuilder {
|
||||
) -> reqwest::RequestBuilder {
|
||||
let mut request = match provider.token_request_format() {
|
||||
TokenRequestFormat::Json => {
|
||||
let body: serde_json::Map<String, Value> = params
|
||||
@@ -307,7 +308,7 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
||||
let host = url.host_str().unwrap_or("127.0.0.1");
|
||||
let port = url
|
||||
.port()
|
||||
.ok_or_else(|| anyhow!("No port in redirect URI"))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("No port in redirect URI"))?;
|
||||
let path = url.path();
|
||||
|
||||
println!("Waiting for OAuth callback on {redirect_uri} ...\n");
|
||||
@@ -322,11 +323,19 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
||||
let request_path = request_line
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.ok_or_else(|| anyhow!("Malformed HTTP request from OAuth callback"))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Malformed HTTP request from OAuth callback"))?;
|
||||
|
||||
let full_url = format!("http://{host}:{port}{request_path}");
|
||||
let parsed: Url = full_url.parse()?;
|
||||
|
||||
let response_body = "<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>";
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
response_body.len(),
|
||||
response_body
|
||||
);
|
||||
stream.write_all(response.as_bytes())?;
|
||||
|
||||
if !parsed.path().starts_with(path) {
|
||||
bail!("Unexpected callback path: {}", parsed.path());
|
||||
}
|
||||
@@ -341,22 +350,14 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
||||
.find(|(k, _)| k == "error")
|
||||
.map(|(_, v)| v.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
anyhow!("OAuth callback returned error: {error}")
|
||||
anyhow::anyhow!("OAuth callback returned error: {error}")
|
||||
})?;
|
||||
|
||||
let returned_state = parsed
|
||||
.query_pairs()
|
||||
.find(|(k, _)| k == "state")
|
||||
.map(|(_, v)| v.to_string())
|
||||
.ok_or_else(|| anyhow!("Missing state parameter in OAuth callback"))?;
|
||||
|
||||
let response_body = "<html><body><h2>Authentication successful!</h2><p>You can close this tab and return to your terminal.</p></body></html>";
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
response_body.len(),
|
||||
response_body
|
||||
);
|
||||
stream.write_all(response.as_bytes())?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing state parameter in OAuth callback"))?;
|
||||
|
||||
Ok((code, returned_state))
|
||||
}
|
||||
|
||||
+5
-10
@@ -239,17 +239,12 @@ impl Input {
|
||||
patch_messages(&mut messages, model);
|
||||
model.guard_max_input_tokens(&messages)?;
|
||||
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
|
||||
let functions = if model.supports_function_calling() {
|
||||
let fns = self.config.read().select_functions(self.role());
|
||||
if let Some(vec) = &fns {
|
||||
for def in vec {
|
||||
debug!("Function definition: {:?}", def.name);
|
||||
}
|
||||
let functions = self.config.read().select_functions(self.role());
|
||||
if let Some(vec) = &functions {
|
||||
for def in vec {
|
||||
debug!("Function definition: {:?}", def.name);
|
||||
}
|
||||
fns
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
Ok(ChatCompletionsData {
|
||||
messages,
|
||||
temperature,
|
||||
|
||||
@@ -1842,12 +1842,6 @@ impl Config {
|
||||
bail!("Already in an agent, please run '.exit agent' first to exit the current agent.");
|
||||
}
|
||||
let agent = Agent::init(config, agent_name, abort_signal.clone()).await?;
|
||||
if !agent.model().supports_function_calling() {
|
||||
eprintln!(
|
||||
"Warning: The model '{}' does not support function calling. Agent tools (including todo, spawning, and user interaction) will not be available.",
|
||||
agent.model().id()
|
||||
);
|
||||
}
|
||||
let session = session_name.map(|v| v.to_string()).or_else(|| {
|
||||
if config.read().macro_flag {
|
||||
None
|
||||
|
||||
@@ -12,7 +12,6 @@ use tokio::sync::oneshot;
|
||||
pub const USER_FUNCTION_PREFIX: &str = "user__";
|
||||
|
||||
const DEFAULT_ESCALATION_TIMEOUT_SECS: u64 = 300;
|
||||
const CUSTOM_MULTI_CHOICE_ANSWER_OPTION: &str = "Other (custom)";
|
||||
|
||||
pub fn user_interaction_function_declarations() -> Vec<FunctionDeclaration> {
|
||||
vec![
|
||||
@@ -152,14 +151,9 @@ fn handle_direct_ask(args: &Value) -> Result<Value> {
|
||||
.get("question")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||
let mut options = parse_options(args)?;
|
||||
options.push(CUSTOM_MULTI_CHOICE_ANSWER_OPTION.to_string());
|
||||
let options = parse_options(args)?;
|
||||
|
||||
let mut answer = Select::new(question, options).prompt()?;
|
||||
|
||||
if answer == CUSTOM_MULTI_CHOICE_ANSWER_OPTION {
|
||||
answer = Text::new("Custom response:").prompt()?
|
||||
}
|
||||
let answer = Select::new(question, options).prompt()?;
|
||||
|
||||
Ok(json!({ "answer": answer }))
|
||||
}
|
||||
|
||||
+2
-10
@@ -23,7 +23,7 @@ use crate::config::{
|
||||
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, load_env_file,
|
||||
macro_execute,
|
||||
};
|
||||
use crate::render::{prompt_theme, render_error};
|
||||
use crate::render::render_error;
|
||||
use crate::repl::Repl;
|
||||
use crate::utils::*;
|
||||
|
||||
@@ -33,7 +33,7 @@ use anyhow::{Result, anyhow, bail};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_complete::CompleteEnv;
|
||||
use client::ClientConfig;
|
||||
use inquire::{Select, Text, set_global_render_config};
|
||||
use inquire::{Select, Text};
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
@@ -106,14 +106,6 @@ async fn main() -> Result<()> {
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
|
||||
{
|
||||
let cfg = config.read();
|
||||
if cfg.highlight {
|
||||
set_global_render_config(prompt_theme(cfg.render_options()?)?)
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = run(config, cli, text, abort_signal).await {
|
||||
render_error(err);
|
||||
process::exit(1);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
use crate::render::RenderOptions;
|
||||
use anyhow::Result;
|
||||
use inquire::ui::{Attributes, Color, RenderConfig, StyleSheet};
|
||||
use syntect::highlighting::{Highlighter, Theme};
|
||||
use syntect::parsing::Scope;
|
||||
|
||||
const DEFAULT_INQUIRE_PROMPT_THEME: Color = Color::DarkYellow;
|
||||
|
||||
pub fn prompt_theme<'a>(render_options: RenderOptions) -> Result<RenderConfig<'a>> {
|
||||
let theme = render_options.theme.as_ref();
|
||||
let mut render_config = RenderConfig::default();
|
||||
|
||||
if let Some(theme_ref) = theme {
|
||||
let prompt_color = resolve_foreground(theme_ref, "markup.heading")?
|
||||
.unwrap_or(DEFAULT_INQUIRE_PROMPT_THEME);
|
||||
|
||||
render_config.prompt = StyleSheet::new()
|
||||
.with_fg(prompt_color)
|
||||
.with_attr(Attributes::BOLD);
|
||||
render_config.selected_option = Some(
|
||||
render_config
|
||||
.selected_option
|
||||
.unwrap_or(render_config.option)
|
||||
.with_attr(
|
||||
render_config
|
||||
.selected_option
|
||||
.unwrap_or(render_config.option)
|
||||
.att
|
||||
| Attributes::BOLD,
|
||||
),
|
||||
);
|
||||
render_config.selected_checkbox = render_config
|
||||
.selected_checkbox
|
||||
.with_attr(render_config.selected_checkbox.style.att | Attributes::BOLD);
|
||||
render_config.option = render_config
|
||||
.option
|
||||
.with_attr(render_config.option.att | Attributes::BOLD);
|
||||
}
|
||||
|
||||
Ok(render_config)
|
||||
}
|
||||
|
||||
fn resolve_foreground(theme: &Theme, scope_str: &str) -> Result<Option<Color>> {
|
||||
let scope = Scope::new(scope_str)?;
|
||||
let style_mod = Highlighter::new(theme).style_mod_for_stack(&[scope]);
|
||||
let fg = style_mod.foreground.or(theme.settings.foreground);
|
||||
|
||||
Ok(fg.map(|c| Color::Rgb {
|
||||
r: c.r,
|
||||
g: c.g,
|
||||
b: c.b,
|
||||
}))
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
mod inquire;
|
||||
mod markdown;
|
||||
mod stream;
|
||||
|
||||
pub use inquire::prompt_theme;
|
||||
|
||||
pub use self::markdown::{MarkdownRender, RenderOptions};
|
||||
use self::stream::{markdown_stream, raw_stream};
|
||||
|
||||
|
||||
+1
-2
@@ -428,8 +428,7 @@ pub async fn run_repl_command(
|
||||
None => println!("Usage: .model <name>"),
|
||||
},
|
||||
".authenticate" => {
|
||||
let current_model = config.read().current_model().clone();
|
||||
let client = init_client(config, Some(current_model))?;
|
||||
let client = init_client(config, None)?;
|
||||
if !client.supports_oauth() {
|
||||
bail!(
|
||||
"Client '{}' doesn't either support OAuth or isn't configured to use it (i.e. uses an API key instead)",
|
||||
|
||||
Reference in New Issue
Block a user