feat: rename Loki to Coyote

This commit is contained in:
2026-05-27 12:47:32 -06:00
parent c172736362
commit 0facb15e32
58 changed files with 506 additions and 3202 deletions
+7 -7
View File
@@ -9,7 +9,7 @@ use std::env;
use std::ffi::OsStr;
use std::io;
const LOKI_CLI_NAME: &str = "loki";
const COYOTE_CLI_NAME: &str = "coyote";
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
pub enum ShellCompletion {
@@ -24,12 +24,12 @@ pub enum ShellCompletion {
impl ShellCompletion {
pub fn generate_completions(self, cmd: &mut clap::Command) {
match self {
Self::Bash => generate(Shell::Bash, cmd, LOKI_CLI_NAME, &mut io::stdout()),
Self::Elvish => generate(Shell::Elvish, cmd, LOKI_CLI_NAME, &mut io::stdout()),
Self::Fish => generate(Shell::Fish, cmd, LOKI_CLI_NAME, &mut io::stdout()),
Self::PowerShell => generate(Shell::PowerShell, cmd, LOKI_CLI_NAME, &mut io::stdout()),
Self::Zsh => generate(Shell::Zsh, cmd, LOKI_CLI_NAME, &mut io::stdout()),
Self::Nushell => generate(Nushell, cmd, LOKI_CLI_NAME, &mut io::stdout()),
Self::Bash => generate(Shell::Bash, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
Self::Elvish => generate(Shell::Elvish, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
Self::Fish => generate(Shell::Fish, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
Self::PowerShell => generate(Shell::PowerShell, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
Self::Zsh => generate(Shell::Zsh, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
Self::Nushell => generate(Nushell, cmd, COYOTE_CLI_NAME, &mut io::stdout()),
}
}
}
+10 -10
View File
@@ -15,7 +15,7 @@ use std::io::{Read, stdin};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(
name = "loki",
name = "coyote",
author = crate_authors!(),
version = crate_version!(),
about = crate_description!(),
@@ -125,19 +125,19 @@ pub struct Cli {
/// Disable colored log output
#[arg(long, requires = "tail_logs")]
pub disable_log_colors: bool,
/// Add a secret to the Loki vault
/// Add a secret to the Coyote vault
#[arg(long, value_name = "SECRET_NAME", exclusive = true)]
pub add_secret: Option<String>,
/// Decrypt a secret from the Loki vault and print the plaintext
/// Decrypt a secret from the Coyote vault and print the plaintext
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
pub get_secret: Option<String>,
/// Update an existing secret in the Loki vault
/// Update an existing secret in the Coyote vault
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
pub update_secret: Option<String>,
/// Delete a secret from the Loki vault
/// Delete a secret from the Coyote vault
#[arg(long, value_name = "SECRET_NAME", exclusive = true, add = ArgValueCompleter::new(secrets_completer))]
pub delete_secret: Option<String>,
/// List all secrets stored in the Loki vault
/// List all secrets stored in the Coyote vault
#[arg(long, exclusive = true)]
pub list_secrets: bool,
/// Authenticate with an LLM provider using OAuth (e.g., --authenticate client_name)
@@ -146,10 +146,10 @@ pub struct Cli {
/// Generate static shell completion scripts
#[arg(long, value_name = "SHELL", value_enum)]
pub completions: Option<ShellCompletion>,
/// Update Loki to the latest release, or to a specific version
/// Update Coyote to the latest release, or to a specific version
#[arg(long, value_name = "VERSION")]
pub update: Option<Option<String>>,
/// With --update, update even if Loki was installed via a package manager
/// With --update, update even if Coyote was installed via a package manager
#[arg(long, requires = "update")]
pub force: bool,
}
@@ -202,7 +202,7 @@ mod tests {
use clap::Parser;
fn parse(args: &[&str]) -> Cli {
let mut full_args = vec!["loki"];
let mut full_args = vec!["coyote"];
full_args.extend_from_slice(args);
Cli::try_parse_from(full_args).unwrap()
}
@@ -436,6 +436,6 @@ mod tests {
#[test]
fn parse_force_without_update_fails() {
assert!(Cli::try_parse_from(["loki", "--force"]).is_err());
assert!(Cli::try_parse_from(["coyote", "--force"]).is_err());
}
}
+2 -2
View File
@@ -85,7 +85,7 @@ async fn prepare_chat_completions(
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
if !ready {
bail!(
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
"OAuth configured but no tokens found for '{}'. Run: 'coyote --authenticate {}' or '.authenticate' in the REPL",
self_.name(),
self_.name()
);
@@ -100,7 +100,7 @@ async fn prepare_chat_completions(
request_data.header("x-api-key", api_key);
} else {
bail!(
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `loki --authenticate {}`.",
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `coyote --authenticate {}`.",
self_.name(),
self_.name()
);
+3 -3
View File
@@ -111,7 +111,7 @@ async fn prepare_chat_completions(
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
if !ready {
bail!(
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
"OAuth configured but no tokens found for '{}'. Run: 'coyote --authenticate {}' or '.authenticate' in the REPL",
self_.name(),
self_.name()
);
@@ -122,7 +122,7 @@ async fn prepare_chat_completions(
request_data.header("x-goog-api-key", api_key);
} else {
bail!(
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `loki --authenticate {}`.",
"No authentication configured for '{}'. Set `api_key` or use `auth: oauth` with `coyote --authenticate {}`.",
self_.name(),
self_.name()
);
@@ -181,7 +181,7 @@ async fn prepare_embeddings(
let ready = oauth::prepare_oauth_access_token(client, &provider, self_.name()).await?;
if !ready {
bail!(
"OAuth configured but no tokens found for '{}'. Run: 'loki --authenticate {}' or '.authenticate' in the REPL",
"OAuth configured but no tokens found for '{}'. Run: 'coyote --authenticate {}' or '.authenticate' in the REPL",
self_.name(),
self_.name()
);
+2 -2
View File
@@ -879,7 +879,7 @@ mod tests {
#[test]
fn from_files_loads_single_text_file() {
let dir = env::temp_dir().join(format!(
"loki-input-test-{}",
"coyote-input-test-{}",
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
@@ -906,7 +906,7 @@ mod tests {
#[test]
fn from_files_loads_multiple_files() {
let dir = env::temp_dir().join(format!(
"loki-input-test-multi-{}",
"coyote-input-test-multi-{}",
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
+6 -6
View File
@@ -136,7 +136,7 @@ impl Drop for TempRepoDir {
}
fn clone_to_temp(url: &str, reference: Option<&str>) -> Result<TempRepoDir> {
let dest = utils::temp_file("loki-remote-install-", "");
let dest = utils::temp_file("coyote-remote-install-", "");
let dest_arg: OsString = dest.as_os_str().into();
let is_sha = reference
@@ -875,7 +875,7 @@ fn print_secret_summary(added: &[String], deferred: &[String]) {
if !deferred.is_empty() {
println!(
"\nThe following secrets are still required by your MCP servers. \
Add them with `loki --add-secret <NAME>` or `.vault add <NAME>` in the REPL:"
Add them with `coyote --add-secret <NAME>` or `.vault add <NAME>` in the REPL:"
);
for name in deferred {
println!(" {{{{ {name} }}}}");
@@ -1265,12 +1265,12 @@ mod tests {
let target = dir.join("target.json");
write_mcp(
&remote,
r#"{"mcpServers": {"x": {"type":"stdio","command":"echo","env":{"K":"{{LOKI_TEST_MERGE_SECRET}}"}}}}"#,
r#"{"mcpServers": {"x": {"type":"stdio","command":"echo","env":{"K":"{{COYOTE_TEST_MERGE_SECRET}}"}}}}"#,
);
let report = merge_mcp_json(None, &remote, &target, false).unwrap();
assert_eq!(report.missing_secrets, vec!["LOKI_TEST_MERGE_SECRET"]);
assert_eq!(report.missing_secrets, vec!["COYOTE_TEST_MERGE_SECRET"]);
let _ = fs::remove_dir_all(&dir);
}
@@ -1300,8 +1300,8 @@ mod tests {
#[test]
fn handle_missing_secrets_defers_all_in_non_tty() {
let missing = vec![
"LOKI_TEST_STEP4_A".to_string(),
"LOKI_TEST_STEP4_B".to_string(),
"COYOTE_TEST_STEP4_A".to_string(),
"COYOTE_TEST_STEP4_B".to_string(),
];
assert!(handle_missing_secrets(&missing).is_ok());
+3 -3
View File
@@ -104,13 +104,13 @@ const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [
"get_current_weather.sh",
"search_wikipedia.sh",
"search_arxiv.sh",
"web_search_loki.sh",
"web_search_coyote.sh",
];
const CLIENTS_FIELD: &str = "clients";
const SYNC_MODELS_URL: &str =
"https://raw.githubusercontent.com/Dark-Alex-17/loki/refs/heads/main/models.yaml";
"https://raw.githubusercontent.com/Dark-Alex-17/coyote/refs/heads/main/models.yaml";
const SUMMARIZATION_PROMPT: &str =
"Summarize the discussion briefly in 200 words or less to use as a prompt for future context.";
@@ -625,7 +625,7 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?;
let config_data = format!(
"# see https://github.com/Dark-Alex-17/loki/blob/main/config.example.yaml\n\n{config_data}"
"# see https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml\n\n{config_data}"
);
ensure_parent_exists(config_path)?;
+2 -2
View File
@@ -1498,7 +1498,7 @@ impl RequestContext {
if !target_path.exists() {
fs::write(
&target_path,
"# see https://github.com/Dark-Alex-17/loki/blob/main/config.agent.example.yaml\n",
"# see https://github.com/Dark-Alex-17/coyote/blob/main/config.agent.example.yaml\n",
)
.with_context(|| format!("Failed to write to '{}'", target_path.display()))?;
}
@@ -2706,7 +2706,7 @@ mod tests {
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = env::temp_dir().join(format!("loki-request-context-tests-{unique}"));
let path = env::temp_dir().join(format!("coyote-request-context-tests-{unique}"));
create_dir_all(&path).unwrap();
unsafe {
env::set_var(&key, &path);
+17 -17
View File
@@ -69,7 +69,7 @@ fn normalize_version(requested: Option<String>) -> Option<String> {
}
fn is_dir_writable(dir: &Path) -> bool {
let probe = dir.join(format!(".loki-update-write-test-{}", process::id()));
let probe = dir.join(format!(".coyote-update-write-test-{}", process::id()));
match OpenOptions::new().write(true).create_new(true).open(&probe) {
Ok(_) => {
let _ = fs::remove_file(&probe);
@@ -83,24 +83,24 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
let target_tag = normalize_version(requested);
let exe_path = env::current_exe()
.context("Could not determine the path of the running loki executable")?;
.context("Could not determine the path of the running coyote executable")?;
let resolved = canonicalize(&exe_path).unwrap_or_else(|_| exe_path.clone());
let source = classify_install_path(&resolved);
if source.is_package_managed() {
let body = match source {
InstallSource::Homebrew => format!(
"Loki appears to be installed via Homebrew ({}).\n\
"Coyote appears to be installed via Homebrew ({}).\n\
Updating in place replaces the binary inside Homebrew's Cellar; `brew` will\n\
then report a version that no longer matches the file on disk, and a later\n\
`brew upgrade`/`brew reinstall` may overwrite it or fail.\n\
The clean way to update is: brew upgrade loki",
The clean way to update is: brew upgrade coyote",
exe_path.display()
),
InstallSource::Cargo => format!(
"Loki appears to be installed via `cargo install` ({}).\n\
"Coyote appears to be installed via `cargo install` ({}).\n\
Updating in place leaves Cargo's records out of sync with the binary on disk.\n\
The clean way to update is: cargo install --locked loki-ai",
The clean way to update is: cargo install --locked coyote-ai",
exe_path.display()
),
InstallSource::Manual => unreachable!("Manual installs are not package-managed"),
@@ -130,7 +130,7 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
{
bail!(
"No write permission for '{}'. Re-run with elevated permissions (e.g. sudo), \
or update Loki through your package manager.",
or update Coyote through your package manager.",
parent.display()
);
}
@@ -139,8 +139,8 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
let mut builder = Update::configure();
builder
.repo_owner("Dark-Alex-17")
.repo_name("loki")
.bin_name("loki")
.repo_name("coyote")
.bin_name("coyote")
.current_version(env!("CARGO_PKG_VERSION"))
.no_confirm(true)
.show_download_progress(interactive);
@@ -155,10 +155,10 @@ pub fn run_self_update(requested: Option<String>, force: bool) -> Result<()> {
match status {
Status::UpToDate(version) => {
println!("Loki is already up to date (v{version}).");
println!("Coyote is already up to date (v{version}).");
}
Status::Updated(version) => {
println!("Loki updated to v{version}. Restart loki to use the new version.");
println!("Coyote updated to v{version}. Restart coyote to use the new version.");
}
}
Ok(())
@@ -172,7 +172,7 @@ mod tests {
#[test]
fn classify_cargo_install() {
assert_eq!(
classify_install_path(&PathBuf::from("/home/u/.cargo/bin/loki")),
classify_install_path(&PathBuf::from("/home/u/.cargo/bin/coyote")),
InstallSource::Cargo
);
}
@@ -180,7 +180,7 @@ mod tests {
#[test]
fn classify_homebrew_opt_prefix() {
assert_eq!(
classify_install_path(&PathBuf::from("/opt/homebrew/bin/loki")),
classify_install_path(&PathBuf::from("/opt/homebrew/bin/coyote")),
InstallSource::Homebrew
);
}
@@ -188,7 +188,7 @@ mod tests {
#[test]
fn classify_homebrew_cellar() {
assert_eq!(
classify_install_path(&PathBuf::from("/usr/local/Cellar/loki/0.3.0/bin/loki")),
classify_install_path(&PathBuf::from("/usr/local/Cellar/coyote/0.3.0/bin/coyote")),
InstallSource::Homebrew
);
}
@@ -196,7 +196,7 @@ mod tests {
#[test]
fn classify_homebrew_linuxbrew() {
assert_eq!(
classify_install_path(&PathBuf::from("/home/linuxbrew/.linuxbrew/bin/loki")),
classify_install_path(&PathBuf::from("/home/linuxbrew/.linuxbrew/bin/coyote")),
InstallSource::Homebrew
);
}
@@ -204,7 +204,7 @@ mod tests {
#[test]
fn classify_manual_usr_local_bin() {
assert_eq!(
classify_install_path(&PathBuf::from("/usr/local/bin/loki")),
classify_install_path(&PathBuf::from("/usr/local/bin/coyote")),
InstallSource::Manual
);
}
@@ -212,7 +212,7 @@ mod tests {
#[test]
fn classify_manual_local_bin() {
assert_eq!(
classify_install_path(&PathBuf::from("/home/u/.local/bin/loki")),
classify_install_path(&PathBuf::from("/home/u/.local/bin/coyote")),
InstallSource::Manual
);
}
+2 -2
View File
@@ -568,13 +568,13 @@ mod tests {
let entries = vec![
"read_query".to_string(),
"mcp:pubmed-search".to_string(),
"web_search_loki".to_string(),
"web_search_coyote".to_string(),
"mcp:github".to_string(),
];
let (regular, mcp) = categorize_tools(Some(&entries));
assert_eq!(regular, vec!["read_query", "web_search_loki"]);
assert_eq!(regular, vec!["read_query", "web_search_coyote"]);
assert_eq!(mcp, vec!["pubmed-search", "github"]);
}
+1 -1
View File
@@ -423,7 +423,7 @@ mod tests {
#[test]
fn load_from_file_reads_disk() {
let dir = env::temp_dir();
let path = dir.join(format!("loki_graph_parser_test_{}.yaml", process::id()));
let path = dir.join(format!("coyote_graph_parser_test_{}.yaml", process::id()));
let yaml = formatdoc! {r#"
name: disk_graph
version: "1.0"
+2 -2
View File
@@ -812,7 +812,7 @@ model: anthropic:claude-sonnet-4-6
temperature: 0.2
top_p: 0.9
global_tools:
- web_search_loki.sh
- web_search_coyote.sh
mcp_servers:
- pubmed-search
conversation_starters:
@@ -827,7 +827,7 @@ nodes:
assert_eq!(graph.model.as_deref(), Some("anthropic:claude-sonnet-4-6"));
assert_eq!(graph.temperature, Some(0.2));
assert_eq!(graph.top_p, Some(0.9));
assert_eq!(graph.global_tools, vec!["web_search_loki.sh"]);
assert_eq!(graph.global_tools, vec!["web_search_coyote.sh"]);
assert_eq!(graph.mcp_servers, vec!["pubmed-search"]);
assert_eq!(graph.conversation_starters, vec!["Look up 2160-0"]);
}
+1 -1
View File
@@ -369,7 +369,7 @@ mod tests {
.duration_since(UNIX_EPOCH)
.expect("time went backwards")
.as_nanos();
let path = std::env::temp_dir().join(format!("loki_python_parser_{file_name}_{unique}.py"));
let path = std::env::temp_dir().join(format!("coyote_python_parser_{file_name}_{unique}.py"));
fs::write(&path, source).expect("failed to write temp python source");
let file = File::open(&path).expect("failed to open temp python source");
let result = generate_python_declarations(file, file_name, Some(parent));
+1 -1
View File
@@ -425,7 +425,7 @@ mod tests {
.duration_since(UNIX_EPOCH)
.expect("time")
.as_nanos();
let path = std::env::temp_dir().join(format!("loki_ts_parser_{file_name}_{unique}.ts"));
let path = std::env::temp_dir().join(format!("coyote_ts_parser_{file_name}_{unique}.ts"));
fs::write(&path, source).expect("write");
let file = File::open(&path).expect("open");
let result = generate_typescript_declarations(file, file_name, Some(parent));
+2 -2
View File
@@ -215,7 +215,7 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
),
ReplCommand::new(
".vault",
"View or modify the Loki vault",
"View or modify the Coyote vault",
AssertState::pass(),
),
ReplCommand::new(
@@ -225,7 +225,7 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
),
ReplCommand::new(
".update",
"Update Loki to the latest release (or a specified version)",
"Update Coyote to the latest release (or a specified version)",
AssertState::pass(),
),
ReplCommand::new(".exit", "Exit REPL", AssertState::pass()),
+1 -1
View File
@@ -366,7 +366,7 @@ mod tests {
assert!(is_valid_extension(Some(&md_ext), Path::new("Agents.md")));
assert!(is_valid_extension(
Some(&md_ext),
Path::new("/home/atusa/code/loki.wiki/Agents.md")
Path::new("/home/atusa/code/coyote.wiki/Agents.md")
));
assert!(!is_valid_extension(Some(&md_ext), Path::new("notes.txt")));
assert!(!is_valid_extension(Some(&md_ext), Path::new("README")));
+2 -2
View File
@@ -28,7 +28,7 @@ pub fn ensure_password_file_initialized(local_provider: &mut LocalProvider) -> R
}
} else {
Err(anyhow!(
"A password file is required to utilize the Loki vault. Please configure a password file in your config file and try again."
"A password file is required to utilize the Coyote vault. Please configure a password file in your config file and try again."
))
}
}
@@ -95,7 +95,7 @@ pub fn create_vault_password_file(vault: &mut Vault) -> Result<()> {
if !ans {
return Err(anyhow!(
"A password file is required to utilize the Loki vault. Please configure a password file in your config file and try again."
"A password file is required to utilize the Coyote vault. Please configure a password file in your config file and try again."
));
}