Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
d6842d7e29
|
|||
|
fbc0acda2a
|
|||
|
0327d041b6
|
|||
|
6a01fd4fbd
|
|||
| d822180205 | |||
|
89d0fdce26
|
|||
|
b3ecdce979
|
|||
|
3873821a31
|
|||
|
9c2801b643
|
|||
|
d78820dcd4
|
|||
|
d43c4232a2
|
|||
|
f41c85b703
|
|||
|
9e056bdcf0
|
|||
|
d6022b9f98
|
|||
|
6fc1abf94a
|
|||
|
92ea0f624e
|
|||
|
c3fd8fbc1c
|
|||
|
7fd3f7761c
|
|||
|
05e19098b2
|
|||
|
60067ae757
|
|||
|
c72003b0b6
|
|||
|
7c9d500116
|
|||
|
6b2c87b562
|
|||
|
b2dbdfb4b1
|
|||
|
063e198f96
|
|||
|
73cbe16ec1
|
|||
|
bdea854a9f
|
|||
|
9b4c800597
|
Generated
+14
-70
@@ -1238,7 +1238,7 @@ dependencies = [
|
|||||||
"encode_unicode",
|
"encode_unicode",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1338,22 +1338,6 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
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]]
|
[[package]]
|
||||||
name = "crossterm"
|
name = "crossterm"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
@@ -1363,7 +1347,7 @@ dependencies = [
|
|||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"filedescriptor",
|
"filedescriptor",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix 0.38.44",
|
"rustix 0.38.44",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1382,7 +1366,7 @@ dependencies = [
|
|||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"derive_more 2.1.1",
|
"derive_more 2.1.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix 1.1.3",
|
"rustix 1.1.3",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
@@ -2164,7 +2148,7 @@ version = "0.2.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2838,19 +2822,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inquire"
|
name = "inquire"
|
||||||
version = "0.7.5"
|
version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"crossterm 0.25.0",
|
"crossterm 0.29.0",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"fxhash",
|
|
||||||
"newline-converter",
|
|
||||||
"once_cell",
|
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width 0.1.14",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3242,7 +3223,7 @@ dependencies = [
|
|||||||
"tokio-graceful",
|
"tokio-graceful",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3457,18 +3438,6 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"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]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -3533,15 +3502,6 @@ version = "1.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
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]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -4555,7 +4515,7 @@ dependencies = [
|
|||||||
"strum_macros 0.26.4",
|
"strum_macros 0.26.4",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5377,8 +5337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio 0.8.11",
|
"mio",
|
||||||
"mio 1.1.1",
|
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5682,7 +5641,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"terminal-trx",
|
"terminal-trx",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"xterm-color",
|
"xterm-color",
|
||||||
@@ -5717,7 +5676,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"smawk",
|
"smawk",
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5862,7 +5821,7 @@ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio 1.1.1",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
@@ -6293,12 +6252,6 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -6966,15 +6919,6 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ bytes = "1.4.0"
|
|||||||
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
clap = { version = "4.5.40", features = ["cargo", "derive", "wrap_help"] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
futures-util = "0.3.29"
|
futures-util = "0.3.29"
|
||||||
inquire = "0.7.0"
|
inquire = "0.9.4"
|
||||||
is-terminal = "0.4.9"
|
is-terminal = "0.4.9"
|
||||||
reedline = "0.40.0"
|
reedline = "0.40.0"
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
|
|||||||
@@ -2,68 +2,6 @@
|
|||||||
# Shared Agent Utilities - Minimal, focused helper functions
|
# Shared Agent Utilities - Minimal, focused helper functions
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
#############################
|
|
||||||
## CONTEXT FILE MANAGEMENT ##
|
|
||||||
#############################
|
|
||||||
|
|
||||||
get_context_file() {
|
|
||||||
local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
|
||||||
echo "${project_dir}/.loki-context"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Initialize context file for a new task
|
|
||||||
# Usage: init_context "Task description"
|
|
||||||
init_context() {
|
|
||||||
local task="$1"
|
|
||||||
local project_dir="${LLM_AGENT_VAR_PROJECT_DIR:-.}"
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
|
|
||||||
cat > "${context_file}" <<EOF
|
|
||||||
## Project: ${project_dir}
|
|
||||||
## Task: ${task}
|
|
||||||
## Started: $(date -Iseconds)
|
|
||||||
|
|
||||||
### Prior Findings
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Append findings to the context file
|
|
||||||
# Usage: append_context "agent_name" "finding summary
|
|
||||||
append_context() {
|
|
||||||
local agent="$1"
|
|
||||||
local finding="$2"
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
|
|
||||||
if [[ -f "${context_file}" ]]; then
|
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "[${agent}]:"
|
|
||||||
echo "${finding}"
|
|
||||||
} >> "${context_file}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Read the current context (returns empty string if no context)
|
|
||||||
# Usage: context=$(read_context)
|
|
||||||
read_context() {
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
|
|
||||||
if [[ -f "${context_file}" ]]; then
|
|
||||||
cat "${context_file}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear the context file
|
|
||||||
clear_context() {
|
|
||||||
local context_file
|
|
||||||
context_file=$(get_context_file)
|
|
||||||
rm -f "${context_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
## PROJECT DETECTION ##
|
## PROJECT DETECTION ##
|
||||||
#######################
|
#######################
|
||||||
@@ -279,9 +217,9 @@ _detect_with_llm() {
|
|||||||
evidence=$(_gather_project_evidence "${dir}")
|
evidence=$(_gather_project_evidence "${dir}")
|
||||||
local prompt
|
local prompt
|
||||||
prompt=$(cat <<-EOF
|
prompt=$(cat <<-EOF
|
||||||
|
|
||||||
Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, and check (lint/typecheck) it.
|
Analyze this project directory and determine the project type, primary language, and the correct shell commands to build, test, and check (lint/typecheck) it.
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
prompt+=$'\n'"${evidence}"$'\n'
|
prompt+=$'\n'"${evidence}"$'\n'
|
||||||
@@ -348,77 +286,11 @@ detect_project() {
|
|||||||
echo '{"type":"unknown","build":"","test":"","check":""}'
|
echo '{"type":"unknown","build":"","test":"","check":""}'
|
||||||
}
|
}
|
||||||
|
|
||||||
######################
|
|
||||||
## AGENT INVOCATION ##
|
|
||||||
######################
|
|
||||||
|
|
||||||
# Invoke a subagent with optional context injection
|
|
||||||
# Usage: invoke_agent <agent_name> <prompt> [extra_args...]
|
|
||||||
invoke_agent() {
|
|
||||||
local agent="$1"
|
|
||||||
local prompt="$2"
|
|
||||||
shift 2
|
|
||||||
|
|
||||||
local context
|
|
||||||
context=$(read_context)
|
|
||||||
|
|
||||||
local full_prompt
|
|
||||||
if [[ -n "${context}" ]]; then
|
|
||||||
full_prompt="## Orchestrator Context
|
|
||||||
|
|
||||||
The orchestrator (sisyphus) has gathered this context from prior work:
|
|
||||||
|
|
||||||
<context>
|
|
||||||
${context}
|
|
||||||
</context>
|
|
||||||
|
|
||||||
## Your Task
|
|
||||||
|
|
||||||
${prompt}"
|
|
||||||
else
|
|
||||||
full_prompt="${prompt}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
env AUTO_CONFIRM=true loki --agent "${agent}" "$@" "${full_prompt}" 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Invoke a subagent and capture a summary of its findings
|
|
||||||
# Usage: result=$(invoke_agent_with_summary "explore" "find auth patterns")
|
|
||||||
invoke_agent_with_summary() {
|
|
||||||
local agent="$1"
|
|
||||||
local prompt="$2"
|
|
||||||
shift 2
|
|
||||||
|
|
||||||
local output
|
|
||||||
output=$(invoke_agent "${agent}" "${prompt}" "$@")
|
|
||||||
|
|
||||||
local summary=""
|
|
||||||
|
|
||||||
if echo "${output}" | grep -q "FINDINGS:"; then
|
|
||||||
summary=$(echo "${output}" | sed -n '/FINDINGS:/,/^[A-Z_]*COMPLETE/p' | grep "^- " | sed 's/^- / - /')
|
|
||||||
elif echo "${output}" | grep -q "CODER_COMPLETE:"; then
|
|
||||||
summary=$(echo "${output}" | grep "CODER_COMPLETE:" | sed 's/CODER_COMPLETE: *//')
|
|
||||||
elif echo "${output}" | grep -q "ORACLE_COMPLETE"; then
|
|
||||||
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
|
|
||||||
|
|
||||||
if [[ -n "${summary}" ]]; then
|
|
||||||
append_context "${agent}" "${summary}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${output}"
|
|
||||||
}
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
## FILE SEARCH UTILITIES ##
|
## FILE SEARCH UTILITIES ##
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
search_files() {
|
_search_files() {
|
||||||
local pattern="$1"
|
local pattern="$1"
|
||||||
local dir="${2:-.}"
|
local dir="${2:-.}"
|
||||||
|
|
||||||
|
|||||||
@@ -122,3 +122,6 @@ instructions: |
|
|||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
- Shell: {{__shell__}}
|
- Shell: {{__shell__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|||||||
@@ -29,11 +29,30 @@ instructions: |
|
|||||||
## Your Mission
|
## Your Mission
|
||||||
|
|
||||||
Given an implementation task:
|
Given an implementation task:
|
||||||
1. Understand what to build (from context provided)
|
1. Check for orchestrator context first (see below)
|
||||||
2. Study existing patterns (read 1-2 similar files)
|
2. Fill gaps only. Read files NOT already covered in context
|
||||||
3. Write the code (using tools, NOT chat output)
|
3. Write the code (using tools, NOT chat output)
|
||||||
4. Verify it compiles/builds
|
4. Verify it compiles/builds
|
||||||
5. Signal completion
|
5. Signal completion with a summary
|
||||||
|
|
||||||
|
## Using Orchestrator Context (IMPORTANT)
|
||||||
|
|
||||||
|
When spawned by sisyphus, your prompt will often contain a `<context>` block
|
||||||
|
with prior findings: file paths, code patterns, and conventions discovered by
|
||||||
|
explore agents.
|
||||||
|
|
||||||
|
**If context is provided:**
|
||||||
|
1. Use it as your primary reference. Don't re-read files already summarized
|
||||||
|
2. Follow the code patterns shown. Snippets in context ARE the style guide
|
||||||
|
3. Read the referenced files ONLY IF you need more detail (e.g. full function
|
||||||
|
signature, import list, or adjacent code not included in the snippet)
|
||||||
|
4. If context includes a "Conventions" section, follow it exactly
|
||||||
|
|
||||||
|
**If context is NOT provided or is too vague to act on:**
|
||||||
|
Fall back to self-exploration: grep for similar files, read 1-2 examples,
|
||||||
|
match their style.
|
||||||
|
|
||||||
|
**Never ignore provided context.** It represents work already done upstream.
|
||||||
|
|
||||||
## Todo System
|
## Todo System
|
||||||
|
|
||||||
@@ -82,12 +101,13 @@ instructions: |
|
|||||||
|
|
||||||
## Completion Signal
|
## Completion Signal
|
||||||
|
|
||||||
End with:
|
When done, end your response with a summary so the parent agent knows what happened:
|
||||||
|
|
||||||
```
|
```
|
||||||
CODER_COMPLETE: [summary of what was implemented]
|
CODER_COMPLETE: [summary of what was implemented, which files were created/modified, and build status]
|
||||||
```
|
```
|
||||||
|
|
||||||
Or if failed:
|
Or if something went wrong:
|
||||||
```
|
```
|
||||||
CODER_FAILED: [what went wrong]
|
CODER_FAILED: [what went wrong]
|
||||||
```
|
```
|
||||||
@@ -104,4 +124,6 @@ instructions: |
|
|||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
- Shell: {{__shell__}}
|
- Shell: {{__shell__}}
|
||||||
|
|
||||||
|
## Available tools:
|
||||||
|
{{__tools__}}
|
||||||
@@ -14,11 +14,28 @@ _project_dir() {
|
|||||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${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
|
# @cmd Read a file's contents before modifying
|
||||||
# @option --path! Path to the file (relative to project root)
|
# @option --path! Path to the file (relative to project root)
|
||||||
read_file() {
|
read_file() {
|
||||||
|
local file_path
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local file_path="${argc_path}"
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
local full_path="${project_dir}/${file_path}"
|
local full_path="${project_dir}/${file_path}"
|
||||||
@@ -39,7 +56,8 @@ read_file() {
|
|||||||
# @option --path! Path for the file (relative to project root)
|
# @option --path! Path for the file (relative to project root)
|
||||||
# @option --content! Complete file contents to write
|
# @option --content! Complete file contents to write
|
||||||
write_file() {
|
write_file() {
|
||||||
local file_path="${argc_path}"
|
local file_path
|
||||||
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local content="${argc_content}"
|
local content="${argc_content}"
|
||||||
local project_dir
|
local project_dir
|
||||||
@@ -47,7 +65,7 @@ write_file() {
|
|||||||
local full_path="${project_dir}/${file_path}"
|
local full_path="${project_dir}/${file_path}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "${full_path}")"
|
mkdir -p "$(dirname "${full_path}")"
|
||||||
echo "${content}" > "${full_path}"
|
printf '%s' "${content}" > "${full_path}"
|
||||||
|
|
||||||
green "Wrote: ${file_path}" >> "$LLM_OUTPUT"
|
green "Wrote: ${file_path}" >> "$LLM_OUTPUT"
|
||||||
}
|
}
|
||||||
@@ -55,7 +73,8 @@ write_file() {
|
|||||||
# @cmd Find files similar to a given path (for pattern matching)
|
# @cmd Find files similar to a given path (for pattern matching)
|
||||||
# @option --path! Path to find similar files for
|
# @option --path! Path to find similar files for
|
||||||
find_similar_files() {
|
find_similar_files() {
|
||||||
local file_path="${argc_path}"
|
local file_path
|
||||||
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
|
|
||||||
@@ -71,14 +90,14 @@ find_similar_files() {
|
|||||||
! -name "$(basename "${file_path}")" \
|
! -name "$(basename "${file_path}")" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
! -name "*spec*" \
|
! -name "*spec*" \
|
||||||
2>/dev/null | head -3)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
|
||||||
|
|
||||||
if [[ -z "${results}" ]]; then
|
if [[ -z "${results}" ]]; then
|
||||||
results=$(find "${project_dir}/src" -type f -name "*.${ext}" \
|
results=$(find "${project_dir}/src" -type f -name "*.${ext}" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
! -name "*spec*" \
|
! -name "*spec*" \
|
||||||
-not -path '*/target/*' \
|
-not -path '*/target/*' \
|
||||||
2>/dev/null | head -3)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -3)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
@@ -186,6 +205,7 @@ search_code() {
|
|||||||
grep -v '/target/' | \
|
grep -v '/target/' | \
|
||||||
grep -v '/node_modules/' | \
|
grep -v '/node_modules/' | \
|
||||||
grep -v '/.git/' | \
|
grep -v '/.git/' | \
|
||||||
|
sed "s|^${project_dir}/||" | \
|
||||||
head -20) || true
|
head -20) || true
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ variables:
|
|||||||
description: Project directory to explore
|
description: Project directory to explore
|
||||||
default: '.'
|
default: '.'
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
You are a codebase explorer. Your job: Search, find, report. Nothing else.
|
||||||
@@ -67,6 +68,9 @@ instructions: |
|
|||||||
## Context
|
## Context
|
||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
- 'Find how authentication is implemented'
|
- 'Find how authentication is implemented'
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ _project_dir() {
|
|||||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${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
|
# @cmd Get project structure and layout
|
||||||
get_structure() {
|
get_structure() {
|
||||||
local project_dir
|
local project_dir
|
||||||
@@ -45,7 +60,7 @@ search_files() {
|
|||||||
echo "" >> "$LLM_OUTPUT"
|
echo "" >> "$LLM_OUTPUT"
|
||||||
|
|
||||||
local results
|
local results
|
||||||
results=$(search_files "${pattern}" "${project_dir}")
|
results=$(_search_files "${pattern}" "${project_dir}")
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
echo "${results}" >> "$LLM_OUTPUT"
|
echo "${results}" >> "$LLM_OUTPUT"
|
||||||
@@ -78,6 +93,7 @@ search_content() {
|
|||||||
grep -v '/node_modules/' | \
|
grep -v '/node_modules/' | \
|
||||||
grep -v '/.git/' | \
|
grep -v '/.git/' | \
|
||||||
grep -v '/dist/' | \
|
grep -v '/dist/' | \
|
||||||
|
sed "s|^${project_dir}/||" | \
|
||||||
head -30) || true
|
head -30) || true
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
@@ -91,8 +107,9 @@ search_content() {
|
|||||||
# @option --path! Path to the file (relative to project root)
|
# @option --path! Path to the file (relative to project root)
|
||||||
# @option --lines Maximum lines to read (default: 200)
|
# @option --lines Maximum lines to read (default: 200)
|
||||||
read_file() {
|
read_file() {
|
||||||
|
local file_path
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local file_path="${argc_path}"
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local max_lines="${argc_lines:-200}"
|
local max_lines="${argc_lines:-200}"
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
@@ -122,7 +139,8 @@ read_file() {
|
|||||||
# @cmd Find similar files to a given file (for pattern matching)
|
# @cmd Find similar files to a given file (for pattern matching)
|
||||||
# @option --path! Path to the reference file
|
# @option --path! Path to the reference file
|
||||||
find_similar() {
|
find_similar() {
|
||||||
local file_path="${argc_path}"
|
local file_path
|
||||||
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
|
|
||||||
@@ -138,7 +156,7 @@ find_similar() {
|
|||||||
! -name "$(basename "${file_path}")" \
|
! -name "$(basename "${file_path}")" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
! -name "*spec*" \
|
! -name "*spec*" \
|
||||||
2>/dev/null | head -5)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
echo "${results}" >> "$LLM_OUTPUT"
|
echo "${results}" >> "$LLM_OUTPUT"
|
||||||
@@ -147,7 +165,7 @@ find_similar() {
|
|||||||
! -name "$(basename "${file_path}")" \
|
! -name "$(basename "${file_path}")" \
|
||||||
! -name "*test*" \
|
! -name "*test*" \
|
||||||
-not -path '*/target/*' \
|
-not -path '*/target/*' \
|
||||||
2>/dev/null | head -5)
|
2>/dev/null | sed "s|^${project_dir}/||" | head -5)
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
echo "${results}" >> "$LLM_OUTPUT"
|
echo "${results}" >> "$LLM_OUTPUT"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -108,3 +108,6 @@ instructions: |
|
|||||||
## Context
|
## Context
|
||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ variables:
|
|||||||
description: Project directory for context
|
description: Project directory for context
|
||||||
default: '.'
|
default: '.'
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
You are Oracle - a senior architect and debugger consulted for complex decisions.
|
You are Oracle - a senior architect and debugger consulted for complex decisions.
|
||||||
@@ -74,6 +75,9 @@ instructions: |
|
|||||||
## Context
|
## Context
|
||||||
- Project: {{project_dir}}
|
- Project: {{project_dir}}
|
||||||
- CWD: {{__cwd__}}
|
- CWD: {{__cwd__}}
|
||||||
|
|
||||||
|
## Available Tools:
|
||||||
|
{{__tools__}}
|
||||||
|
|
||||||
conversation_starters:
|
conversation_starters:
|
||||||
- 'Review this architecture design'
|
- 'Review this architecture design'
|
||||||
|
|||||||
@@ -14,21 +14,38 @@ _project_dir() {
|
|||||||
(cd "${dir}" 2>/dev/null && pwd) || echo "${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
|
# @cmd Read a file for analysis
|
||||||
# @option --path! Path to the file (relative to project root)
|
# @option --path! Path to the file (relative to project root)
|
||||||
read_file() {
|
read_file() {
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
|
local file_path
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
local full_path="${project_dir}/${argc_path}"
|
file_path=$(_normalize_path "${argc_path}")
|
||||||
|
local full_path="${project_dir}/${file_path}"
|
||||||
|
|
||||||
if [[ ! -f "${full_path}" ]]; then
|
if [[ ! -f "${full_path}" ]]; then
|
||||||
error "File not found: ${argc_path}" >> "$LLM_OUTPUT"
|
error "File not found: ${file_path}" >> "$LLM_OUTPUT"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
info "Reading: ${argc_path}"
|
info "Reading: ${file_path}"
|
||||||
echo ""
|
echo ""
|
||||||
cat "${full_path}"
|
cat "${full_path}"
|
||||||
} >> "$LLM_OUTPUT"
|
} >> "$LLM_OUTPUT"
|
||||||
@@ -80,6 +97,7 @@ search_code() {
|
|||||||
grep -v '/target/' | \
|
grep -v '/target/' | \
|
||||||
grep -v '/node_modules/' | \
|
grep -v '/node_modules/' | \
|
||||||
grep -v '/.git/' | \
|
grep -v '/.git/' | \
|
||||||
|
sed "s|^${project_dir}/||" | \
|
||||||
head -30) || true
|
head -30) || true
|
||||||
|
|
||||||
if [[ -n "${results}" ]]; then
|
if [[ -n "${results}" ]]; then
|
||||||
@@ -113,7 +131,8 @@ analyze_with_command() {
|
|||||||
# @cmd List directory contents
|
# @cmd List directory contents
|
||||||
# @option --path Path to list (default: project root)
|
# @option --path Path to list (default: project root)
|
||||||
list_directory() {
|
list_directory() {
|
||||||
local dir_path="${argc_path:-.}"
|
local dir_path
|
||||||
|
dir_path=$(_normalize_path "${argc_path:-.}")
|
||||||
local project_dir
|
local project_dir
|
||||||
project_dir=$(_project_dir)
|
project_dir=$(_project_dir)
|
||||||
local full_path="${project_dir}/${dir_path}"
|
local full_path="${project_dir}/${dir_path}"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ can_spawn_agents: true
|
|||||||
max_concurrent_agents: 4
|
max_concurrent_agents: 4
|
||||||
max_agent_depth: 3
|
max_agent_depth: 3
|
||||||
inject_spawn_instructions: true
|
inject_spawn_instructions: true
|
||||||
summarization_threshold: 4000
|
summarization_threshold: 8000
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- name: project_dir
|
- name: project_dir
|
||||||
@@ -22,12 +22,13 @@ variables:
|
|||||||
description: Auto-confirm command execution
|
description: Auto-confirm command execution
|
||||||
default: '1'
|
default: '1'
|
||||||
|
|
||||||
|
mcp_servers:
|
||||||
|
- ddg-search
|
||||||
global_tools:
|
global_tools:
|
||||||
- fs_read.sh
|
- fs_read.sh
|
||||||
- fs_grep.sh
|
- fs_grep.sh
|
||||||
- fs_glob.sh
|
- fs_glob.sh
|
||||||
- fs_ls.sh
|
- fs_ls.sh
|
||||||
- web_search_loki.sh
|
|
||||||
- execute_command.sh
|
- execute_command.sh
|
||||||
|
|
||||||
instructions: |
|
instructions: |
|
||||||
@@ -69,6 +70,45 @@ instructions: |
|
|||||||
| coder | Write/edit files, implement features | Creates/modifies files, runs builds |
|
| coder | Write/edit files, implement features | Creates/modifies files, runs builds |
|
||||||
| oracle | Architecture decisions, complex debugging | Advisory, high-quality reasoning |
|
| oracle | Architecture decisions, complex debugging | Advisory, high-quality reasoning |
|
||||||
|
|
||||||
|
## Coder Delegation Format (MANDATORY)
|
||||||
|
|
||||||
|
When spawning the `coder` agent, your prompt MUST include these sections.
|
||||||
|
The coder has NOT seen the codebase. Your prompt IS its entire context.
|
||||||
|
|
||||||
|
### Template:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Goal
|
||||||
|
[1-2 sentences: what to build/modify and where]
|
||||||
|
|
||||||
|
## Reference Files
|
||||||
|
[Files that explore found, with what each demonstrates]
|
||||||
|
- `path/to/file.ext` - what pattern this file shows
|
||||||
|
- `path/to/other.ext` - what convention this file shows
|
||||||
|
|
||||||
|
## Code Patterns to Follow
|
||||||
|
[Paste ACTUAL code snippets from explore results, not descriptions]
|
||||||
|
<code>
|
||||||
|
// From path/to/file.ext - this is the pattern to follow:
|
||||||
|
[actual code explore found, 5-20 lines]
|
||||||
|
</code>
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
[Naming, imports, error handling, file organization]
|
||||||
|
- Convention 1
|
||||||
|
- Convention 2
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
[What NOT to do, scope boundaries]
|
||||||
|
- Do NOT modify X
|
||||||
|
- Only touch files in Y/
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL**: Include actual code snippets, not just file paths.
|
||||||
|
If explore returned code patterns, paste them into the coder prompt.
|
||||||
|
Vague prompts like "follow existing patterns" waste coder's tokens on
|
||||||
|
re-exploration that you already did.
|
||||||
|
|
||||||
## Workflow Examples
|
## Workflow Examples
|
||||||
|
|
||||||
### Example 1: Implementation task (explore -> coder, parallel exploration)
|
### Example 1: Implementation task (explore -> coder, parallel exploration)
|
||||||
@@ -80,12 +120,12 @@ instructions: |
|
|||||||
2. todo__add --task "Explore existing API patterns"
|
2. todo__add --task "Explore existing API patterns"
|
||||||
3. todo__add --task "Implement profile endpoint"
|
3. todo__add --task "Implement profile endpoint"
|
||||||
4. todo__add --task "Verify with build/test"
|
4. todo__add --task "Verify with build/test"
|
||||||
5. agent__spawn --agent explore --prompt "Find existing API endpoint patterns, route structures, and controller conventions"
|
5. agent__spawn --agent explore --prompt "Find existing API endpoint patterns, route structures, and controller conventions. Include code snippets."
|
||||||
6. agent__spawn --agent explore --prompt "Find existing data models and database query patterns"
|
6. agent__spawn --agent explore --prompt "Find existing data models and database query patterns. Include code snippets."
|
||||||
7. agent__collect --id <id1>
|
7. agent__collect --id <id1>
|
||||||
8. agent__collect --id <id2>
|
8. agent__collect --id <id2>
|
||||||
9. todo__done --id 1
|
9. todo__done --id 1
|
||||||
10. agent__spawn --agent coder --prompt "Create user profiles endpoint following existing patterns. [Include context from explore results]"
|
10. agent__spawn --agent coder --prompt "<structured prompt using Coder Delegation Format above, including code snippets from explore results>"
|
||||||
11. agent__collect --id <coder_id>
|
11. agent__collect --id <coder_id>
|
||||||
12. todo__done --id 2
|
12. todo__done --id 2
|
||||||
13. run_build
|
13. run_build
|
||||||
@@ -134,7 +174,6 @@ instructions: |
|
|||||||
|
|
||||||
## When to Do It Yourself
|
## When to Do It Yourself
|
||||||
|
|
||||||
- Single-file reads/writes
|
|
||||||
- Simple command execution
|
- Simple command execution
|
||||||
- Trivial changes (typos, renames)
|
- Trivial changes (typos, renames)
|
||||||
- Quick file searches
|
- Quick file searches
|
||||||
|
|||||||
@@ -16,11 +16,15 @@
|
|||||||
},
|
},
|
||||||
"atlassian": {
|
"atlassian": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/sse"]
|
"args": ["-y", "mcp-remote@0.1.13", "https://mcp.atlassian.com/v1/mcp"]
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["mcp-server-docker"]
|
"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)
|
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
|
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_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)
|
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
|
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
|
- 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).
|
mcp_server_support: true # Enables or disables MCP servers (globally).
|
||||||
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
mapping_mcp_servers: # Alias for an MCP server or set of servers
|
||||||
git: github,gitmcp
|
git: github,gitmcp
|
||||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack')
|
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
||||||
|
|
||||||
# ---- Session ----
|
# ---- Session ----
|
||||||
# See the [Session documentation](./docs/SESSIONS.md) for more information
|
# See the [Session documentation](./docs/SESSIONS.md) for more information
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ 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
|
* `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
|
* `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
|
* `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
|
* `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
|
* `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`.
|
* `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,6 +142,25 @@ 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
|
code back into the terminal (e.g. Claude). Loki stores the tokens in `~/.cache/loki/oauth` and automatically refreshes
|
||||||
them when they expire.
|
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**
|
**Step 3: Use normally**
|
||||||
|
|
||||||
Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
|
Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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.
|
* [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
|
* [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
|
* [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
|
## Loki Configuration
|
||||||
MCP servers, like tools, can be used in a handful of contexts:
|
MCP servers, like tools, can be used in a handful of contexts:
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
# - https://platform.openai.com/docs/api-reference/chat
|
# - https://platform.openai.com/docs/api-reference/chat
|
||||||
- provider: openai
|
- provider: openai
|
||||||
models:
|
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
|
- name: gpt-5.1
|
||||||
max_input_tokens: 400000
|
max_input_tokens: 400000
|
||||||
max_output_tokens: 128000
|
max_output_tokens: 128000
|
||||||
|
|||||||
+40
-1
@@ -11,6 +11,7 @@ use serde::Deserialize;
|
|||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
const API_BASE: &str = "https://api.anthropic.com/v1";
|
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)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct ClaudeConfig {
|
pub struct ClaudeConfig {
|
||||||
@@ -84,7 +85,7 @@ async fn prepare_chat_completions(
|
|||||||
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
if !ready {
|
if !ready {
|
||||||
bail!(
|
bail!(
|
||||||
"OAuth configured but no tokens found for '{}'. Run: loki --authenticate {}",
|
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
@@ -94,6 +95,7 @@ async fn prepare_chat_completions(
|
|||||||
for (key, value) in provider.extra_request_headers() {
|
for (key, value) in provider.extra_request_headers() {
|
||||||
request_data.header(key, value);
|
request_data.header(key, value);
|
||||||
}
|
}
|
||||||
|
inject_oauth_system_prompt(&mut request_data.body);
|
||||||
} else if let Ok(api_key) = self_.get_api_key() {
|
} else if let Ok(api_key) = self_.get_api_key() {
|
||||||
request_data.header("x-api-key", api_key);
|
request_data.header("x-api-key", api_key);
|
||||||
} else {
|
} else {
|
||||||
@@ -107,6 +109,43 @@ async fn prepare_chat_completions(
|
|||||||
Ok(request_data)
|
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(
|
pub async fn claude_chat_completions(
|
||||||
builder: RequestBuilder,
|
builder: RequestBuilder,
|
||||||
_model: &Model,
|
_model: &Model,
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ async fn prepare_chat_completions(
|
|||||||
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
if !ready {
|
if !ready {
|
||||||
bail!(
|
bail!(
|
||||||
"OAuth configured but no tokens found for '{}'. Run: loki --authenticate {}",
|
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
@@ -181,7 +181,7 @@ async fn prepare_embeddings(
|
|||||||
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
|
||||||
if !ready {
|
if !ready {
|
||||||
bail!(
|
bail!(
|
||||||
"OAuth configured but no tokens found for '{}'. Run: loki --authenticate {}",
|
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
|
||||||
self_.name(),
|
self_.name(),
|
||||||
self_.name()
|
self_.name()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use super::oauth::{OAuthProvider, TokenRequestFormat};
|
|||||||
|
|
||||||
pub struct GeminiOAuthProvider;
|
pub struct GeminiOAuthProvider;
|
||||||
|
|
||||||
// TODO: Replace with real credentials after registering Loki with Google Cloud Console
|
|
||||||
const GEMINI_CLIENT_ID: &str =
|
const GEMINI_CLIENT_ID: &str =
|
||||||
"50826443741-upqcebrs4gctqht1f08ku46qlbirkdsj.apps.googleusercontent.com";
|
"50826443741-upqcebrs4gctqht1f08ku46qlbirkdsj.apps.googleusercontent.com";
|
||||||
const GEMINI_CLIENT_SECRET: &str = "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";
|
const GEMINI_CLIENT_SECRET: &str = "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";
|
||||||
@@ -29,7 +28,7 @@ impl OAuthProvider for GeminiOAuthProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scopes(&self) -> &str {
|
fn scopes(&self) -> &str {
|
||||||
"https://www.googleapis.com/auth/cloud-platform.readonly https://www.googleapis.com/auth/userinfo.email"
|
"https://www.googleapis.com/auth/generative-language.peruserquota https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/userinfo.email"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_secret(&self) -> Option<&str> {
|
fn client_secret(&self) -> Option<&str> {
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ impl Model {
|
|||||||
self.data.max_output_tokens
|
self.data.max_output_tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn supports_function_calling(&self) -> bool {
|
||||||
|
self.data.supports_function_calling
|
||||||
|
}
|
||||||
|
|
||||||
pub fn no_stream(&self) -> bool {
|
pub fn no_stream(&self) -> bool {
|
||||||
self.data.no_stream
|
self.data.no_stream
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-21
@@ -1,12 +1,12 @@
|
|||||||
use super::ClientConfig;
|
use super::ClientConfig;
|
||||||
use super::access_token::{is_valid_access_token, set_access_token};
|
use super::access_token::{is_valid_access_token, set_access_token};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use inquire::Text;
|
use inquire::Text;
|
||||||
use reqwest::Client as ReqwestClient;
|
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
@@ -76,7 +76,6 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
|
|||||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||||
let port = listener.local_addr()?.port();
|
let port = listener.local_addr()?.port();
|
||||||
let uri = format!("http://127.0.0.1:{port}/callback");
|
let uri = format!("http://127.0.0.1:{port}/callback");
|
||||||
// Drop the listener so run_oauth_flow can re-bind below
|
|
||||||
drop(listener);
|
drop(listener);
|
||||||
uri
|
uri
|
||||||
} else {
|
} else {
|
||||||
@@ -149,15 +148,15 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
|
|||||||
|
|
||||||
let access_token = response["access_token"]
|
let access_token = response["access_token"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing access_token in response: {response}"))?
|
.ok_or_else(|| anyhow!("Missing access_token in response: {response}"))?
|
||||||
.to_string();
|
.to_string();
|
||||||
let refresh_token = response["refresh_token"]
|
let refresh_token = response["refresh_token"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing refresh_token in response: {response}"))?
|
.ok_or_else(|| anyhow!("Missing refresh_token in response: {response}"))?
|
||||||
.to_string();
|
.to_string();
|
||||||
let expires_in = response["expires_in"]
|
let expires_in = response["expires_in"]
|
||||||
.as_i64()
|
.as_i64()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing expires_in in response: {response}"))?;
|
.ok_or_else(|| anyhow!("Missing expires_in in response: {response}"))?;
|
||||||
|
|
||||||
let expires_at = Utc::now().timestamp() + expires_in;
|
let expires_at = Utc::now().timestamp() + expires_in;
|
||||||
|
|
||||||
@@ -214,7 +213,7 @@ pub async fn refresh_oauth_token(
|
|||||||
|
|
||||||
let access_token = response["access_token"]
|
let access_token = response["access_token"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing access_token in refresh response: {response}"))?
|
.ok_or_else(|| anyhow!("Missing access_token in refresh response: {response}"))?
|
||||||
.to_string();
|
.to_string();
|
||||||
let refresh_token = response["refresh_token"]
|
let refresh_token = response["refresh_token"]
|
||||||
.as_str()
|
.as_str()
|
||||||
@@ -222,7 +221,7 @@ pub async fn refresh_oauth_token(
|
|||||||
.unwrap_or_else(|| tokens.refresh_token.clone());
|
.unwrap_or_else(|| tokens.refresh_token.clone());
|
||||||
let expires_in = response["expires_in"]
|
let expires_in = response["expires_in"]
|
||||||
.as_i64()
|
.as_i64()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing expires_in in refresh response: {response}"))?;
|
.ok_or_else(|| anyhow!("Missing expires_in in refresh response: {response}"))?;
|
||||||
|
|
||||||
let expires_at = Utc::now().timestamp() + expires_in;
|
let expires_at = Utc::now().timestamp() + expires_in;
|
||||||
|
|
||||||
@@ -266,7 +265,7 @@ fn build_token_request(
|
|||||||
client: &ReqwestClient,
|
client: &ReqwestClient,
|
||||||
provider: &(impl OAuthProvider + ?Sized),
|
provider: &(impl OAuthProvider + ?Sized),
|
||||||
params: &[(&str, &str)],
|
params: &[(&str, &str)],
|
||||||
) -> reqwest::RequestBuilder {
|
) -> RequestBuilder {
|
||||||
let mut request = match provider.token_request_format() {
|
let mut request = match provider.token_request_format() {
|
||||||
TokenRequestFormat::Json => {
|
TokenRequestFormat::Json => {
|
||||||
let body: serde_json::Map<String, Value> = params
|
let body: serde_json::Map<String, Value> = params
|
||||||
@@ -308,7 +307,7 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
|||||||
let host = url.host_str().unwrap_or("127.0.0.1");
|
let host = url.host_str().unwrap_or("127.0.0.1");
|
||||||
let port = url
|
let port = url
|
||||||
.port()
|
.port()
|
||||||
.ok_or_else(|| anyhow::anyhow!("No port in redirect URI"))?;
|
.ok_or_else(|| anyhow!("No port in redirect URI"))?;
|
||||||
let path = url.path();
|
let path = url.path();
|
||||||
|
|
||||||
println!("Waiting for OAuth callback on {redirect_uri} ...\n");
|
println!("Waiting for OAuth callback on {redirect_uri} ...\n");
|
||||||
@@ -323,19 +322,11 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
|||||||
let request_path = request_line
|
let request_path = request_line
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.nth(1)
|
.nth(1)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Malformed HTTP request from OAuth callback"))?;
|
.ok_or_else(|| anyhow!("Malformed HTTP request from OAuth callback"))?;
|
||||||
|
|
||||||
let full_url = format!("http://{host}:{port}{request_path}");
|
let full_url = format!("http://{host}:{port}{request_path}");
|
||||||
let parsed: Url = full_url.parse()?;
|
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) {
|
if !parsed.path().starts_with(path) {
|
||||||
bail!("Unexpected callback path: {}", parsed.path());
|
bail!("Unexpected callback path: {}", parsed.path());
|
||||||
}
|
}
|
||||||
@@ -350,14 +341,22 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
|||||||
.find(|(k, _)| k == "error")
|
.find(|(k, _)| k == "error")
|
||||||
.map(|(_, v)| v.to_string())
|
.map(|(_, v)| v.to_string())
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
anyhow::anyhow!("OAuth callback returned error: {error}")
|
anyhow!("OAuth callback returned error: {error}")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let returned_state = parsed
|
let returned_state = parsed
|
||||||
.query_pairs()
|
.query_pairs()
|
||||||
.find(|(k, _)| k == "state")
|
.find(|(k, _)| k == "state")
|
||||||
.map(|(_, v)| v.to_string())
|
.map(|(_, v)| v.to_string())
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing state parameter in OAuth callback"))?;
|
.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((code, returned_state))
|
Ok((code, returned_state))
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-5
@@ -239,12 +239,17 @@ impl Input {
|
|||||||
patch_messages(&mut messages, model);
|
patch_messages(&mut messages, model);
|
||||||
model.guard_max_input_tokens(&messages)?;
|
model.guard_max_input_tokens(&messages)?;
|
||||||
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
|
let (temperature, top_p) = (self.role().temperature(), self.role().top_p());
|
||||||
let functions = self.config.read().select_functions(self.role());
|
let functions = if model.supports_function_calling() {
|
||||||
if let Some(vec) = &functions {
|
let fns = self.config.read().select_functions(self.role());
|
||||||
for def in vec {
|
if let Some(vec) = &fns {
|
||||||
debug!("Function definition: {:?}", def.name);
|
for def in vec {
|
||||||
|
debug!("Function definition: {:?}", def.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
fns
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
Ok(ChatCompletionsData {
|
Ok(ChatCompletionsData {
|
||||||
messages,
|
messages,
|
||||||
temperature,
|
temperature,
|
||||||
|
|||||||
@@ -1842,6 +1842,12 @@ impl Config {
|
|||||||
bail!("Already in an agent, please run '.exit agent' first to exit the current agent.");
|
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?;
|
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(|| {
|
let session = session_name.map(|v| v.to_string()).or_else(|| {
|
||||||
if config.read().macro_flag {
|
if config.read().macro_flag {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use tokio::sync::oneshot;
|
|||||||
pub const USER_FUNCTION_PREFIX: &str = "user__";
|
pub const USER_FUNCTION_PREFIX: &str = "user__";
|
||||||
|
|
||||||
const DEFAULT_ESCALATION_TIMEOUT_SECS: u64 = 300;
|
const DEFAULT_ESCALATION_TIMEOUT_SECS: u64 = 300;
|
||||||
|
const CUSTOM_MULTI_CHOICE_ANSWER_OPTION: &str = "Other (custom)";
|
||||||
|
|
||||||
pub fn user_interaction_function_declarations() -> Vec<FunctionDeclaration> {
|
pub fn user_interaction_function_declarations() -> Vec<FunctionDeclaration> {
|
||||||
vec![
|
vec![
|
||||||
@@ -151,9 +152,14 @@ fn handle_direct_ask(args: &Value) -> Result<Value> {
|
|||||||
.get("question")
|
.get("question")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.ok_or_else(|| anyhow!("'question' is required"))?;
|
.ok_or_else(|| anyhow!("'question' is required"))?;
|
||||||
let options = parse_options(args)?;
|
let mut options = parse_options(args)?;
|
||||||
|
options.push(CUSTOM_MULTI_CHOICE_ANSWER_OPTION.to_string());
|
||||||
|
|
||||||
let answer = Select::new(question, options).prompt()?;
|
let mut answer = Select::new(question, options).prompt()?;
|
||||||
|
|
||||||
|
if answer == CUSTOM_MULTI_CHOICE_ANSWER_OPTION {
|
||||||
|
answer = Text::new("Custom response:").prompt()?
|
||||||
|
}
|
||||||
|
|
||||||
Ok(json!({ "answer": answer }))
|
Ok(json!({ "answer": answer }))
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -23,7 +23,7 @@ use crate::config::{
|
|||||||
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, load_env_file,
|
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, load_env_file,
|
||||||
macro_execute,
|
macro_execute,
|
||||||
};
|
};
|
||||||
use crate::render::render_error;
|
use crate::render::{prompt_theme, render_error};
|
||||||
use crate::repl::Repl;
|
use crate::repl::Repl;
|
||||||
use crate::utils::*;
|
use crate::utils::*;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ use anyhow::{Result, anyhow, bail};
|
|||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
use clap_complete::CompleteEnv;
|
use clap_complete::CompleteEnv;
|
||||||
use client::ClientConfig;
|
use client::ClientConfig;
|
||||||
use inquire::{Select, Text};
|
use inquire::{Select, Text, set_global_render_config};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::append::console::ConsoleAppender;
|
use log4rs::append::console::ConsoleAppender;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
@@ -106,6 +106,14 @@ async fn main() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?,
|
.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 {
|
if let Err(err) = run(config, cli, text, abort_signal).await {
|
||||||
render_error(err);
|
render_error(err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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,6 +1,9 @@
|
|||||||
|
mod inquire;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
mod stream;
|
mod stream;
|
||||||
|
|
||||||
|
pub use inquire::prompt_theme;
|
||||||
|
|
||||||
pub use self::markdown::{MarkdownRender, RenderOptions};
|
pub use self::markdown::{MarkdownRender, RenderOptions};
|
||||||
use self::stream::{markdown_stream, raw_stream};
|
use self::stream::{markdown_stream, raw_stream};
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -428,7 +428,8 @@ pub async fn run_repl_command(
|
|||||||
None => println!("Usage: .model <name>"),
|
None => println!("Usage: .model <name>"),
|
||||||
},
|
},
|
||||||
".authenticate" => {
|
".authenticate" => {
|
||||||
let client = init_client(config, None)?;
|
let current_model = config.read().current_model().clone();
|
||||||
|
let client = init_client(config, Some(current_model))?;
|
||||||
if !client.supports_oauth() {
|
if !client.supports_oauth() {
|
||||||
bail!(
|
bail!(
|
||||||
"Client '{}' doesn't either support OAuth or isn't configured to use it (i.e. uses an API key instead)",
|
"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