Compare commits

..
4 Commits
Author SHA1 Message Date
Dark-Alex-17 00a21ee777 Merge branch 'main' of github.com:Dark-Alex-17/coyote
CI / All (macos-latest) (push) Waiting to run
CI / All (windows-latest) (push) Waiting to run
CI / All (ubuntu-latest) (push) Failing after 32s
2026-08-31 13:49:09 -06:00
Dark-Alex-17 016f2654e4 fix: make binary shims for custom tools cross-device compatible so users can't accidentally break sandboxes via sbx cp ~/.config/coyote <sbx-name>:/home/agent/.config 2026-08-31 13:48:56 -06:00
Dark-Alex-17 9a1d00f5e6 docs: fixed a broken link in the readme 2026-08-29 13:16:34 -06:00
Dark-Alex-17 a14c768f9a docs: corrected a typo in the README 2026-08-29 12:59:33 -06:00
8 changed files with 281 additions and 106 deletions
+3 -3
View File
@@ -27,7 +27,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
* [Installation](#install): Install Coyote
* [Getting Started](#getting-started): Get started with Coyote by doing first-run setup steps.
* [Sharing Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Sharing-Configurations): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents.
* [Bundles](https://github.com/Dark-Alex-17/coyote/wiki/Bundles): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents.
* [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Coyote.
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
* [Vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
@@ -43,8 +43,8 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
* Models interact with each server through a compact set of capability-gated meta-tools: `mcp_search`/`mcp_describe` for discovery across tools, resources, and prompts, `mcp_invoke` for tool calls, `mcp_read` for paged and regex-filterable resource reads, and `mcp_prompt` for server-defined prompts. Binary content is spilled to disk instead of inlined, and oversized tool results are bounded before they reach the model.
* Invoke server prompts yourself with `.prompt <server> <name> [key=value ...]` in the REPL, with live staged tab-completion (servers, then prompt names, then `key=` arguments), and discover them with `.list prompts`.
* [Macros](https://github.com/Dark-Alex-17/coyote/wiki/Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros). Macros are Coyote's custom commands: invoke any macro directly by name (e.g. `.review main`), with tab-completion, right alongside the built-in REPL commands.
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL.
* Commit project-specific macros to `.coyote/macros/` in your repo — they shadow same-named global macros (opt out with `--no-workspace-macros`).
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends, by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL.
* Commit project-specific macros to `.coyote/macros/` in your repo. They shadow same-named global macros (opt out with `--no-workspace-macros`).
* Pass variables positionally or by name: leading `name=value` args set declared variables directly (letting earlier variables keep their defaults), and remaining args fill the rest in order. Tab completion after a macro name lists each variable with its description and default.
* Scope which macros are invocable with `enabled_macros` in the global config, a role, an agent, or a session (most specific wins; an empty list disables all macros), and toggle at runtime with `.macro enable|disable <name>`.
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
+21 -5
View File
@@ -27,14 +27,30 @@ def _ensure_cwd_venv():
_ensure_cwd_venv()
def resolve_dir(env_name, default_path):
"""Resolve a directory at run time.
Prefer the override env var when set, otherwise fall back to the default
path derived from this script's own location, so the shim keeps working
when the config dir moves or is shared across environments with different
home directories.
"""
value = os.environ.get(env_name)
if value:
return value
return os.path.normpath(default_path)
def main():
(agent_func, raw_data) = parse_argv()
agent_data = parse_raw_data(raw_data)
root_dir = "{config_dir}"
setup_env(root_dir, agent_func, raw_data)
self_dir = os.path.dirname(os.path.abspath(__file__))
agent_dir = os.path.normpath(os.path.join(self_dir, ".."))
root_dir = resolve_dir("{root_dir_env}", os.path.join(self_dir, "{root_dir_rel}"))
setup_env(root_dir, agent_dir, agent_func, raw_data)
agent_tools_path = os.path.join(root_dir, "agents/{agent_name}/tools.py")
agent_tools_path = os.path.join(agent_dir, "tools.py")
run(agent_tools_path, agent_func, agent_data)
@@ -65,12 +81,12 @@ def parse_argv():
return agent_func, agent_data
def setup_env(root_dir, agent_func, raw_data):
def setup_env(root_dir, agent_dir, agent_func, raw_data):
load_env(os.path.join(root_dir, ".env"))
os.environ["LLM_ROOT_DIR"] = root_dir
os.environ["LLM_AGENT_NAME"] = "{agent_name}"
os.environ["LLM_AGENT_FUNC"] = agent_func
os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", "{agent_name}")
os.environ["LLM_AGENT_ROOT_DIR"] = agent_dir
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", "{agent_name}")
os.environ["LLM_AGENT_RAW_JSON"] = raw_data
+25 -5
View File
@@ -5,13 +5,30 @@
set -e
main() {
root_dir="{config_dir}"
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
agent_dir="$(cd "$self_dir/.." && pwd)"
root_dir="$(resolve_dir "{root_dir_env}" "$self_dir/{root_dir_rel}")"
functions_dir="$(resolve_dir "{functions_dir_env}" "$self_dir/{functions_dir_rel}")"
parse_argv "$@"
setup_env
tools_path="$root_dir/agents/{agent_name}/tools.sh"
tools_path="$agent_dir/tools.sh"
run
}
# Resolve a directory at run time: prefer the override env var ($1) when set,
# otherwise fall back to the default path ($2) derived from this script's own
# location, so the shim keeps working when the config dir moves or is shared
# across environments with different home directories.
resolve_dir() {
local override
override="$(printenv "$1" 2>/dev/null || true)"
if [[ -n "$override" ]]; then
echo "$override"
else
(cd "$2" 2>/dev/null && pwd) || echo "$2"
fi
}
parse_argv() {
agent_func="$1"
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
@@ -29,9 +46,9 @@ setup_env() {
export LLM_ROOT_DIR="$root_dir"
export LLM_AGENT_NAME="{agent_name}"
export LLM_AGENT_FUNC="$agent_func"
export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/{agent_name}"
export LLM_AGENT_ROOT_DIR="$agent_dir"
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/{agent_name}"
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
export LLM_PROMPT_UTILS_FILE="$functions_dir/utils/prompt-utils.sh"
export LLM_AGENT_RAW_JSON="$agent_data"
}
@@ -59,6 +76,10 @@ run() {
die "error: no JSON data"
fi
if [[ ! -f "$tools_path" ]]; then
die "error: agent tools script not found: $tools_path"
fi
if [[ "$OS" == "Windows_NT" ]]; then
set -o igncr
tools_path="$(cygpath -w "$tools_path")"
@@ -122,4 +143,3 @@ die() {
}
main "$@"
+33 -7
View File
@@ -3,17 +3,38 @@
// Usage: ./{agent_name}.ts <agent-func> <agent-data>
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { pathToFileURL } from "url";
import { dirname, join, resolve } from "path";
import { fileURLToPath, pathToFileURL } from "url";
function selfDir(): string {
if (typeof __dirname !== "undefined") {
return __dirname;
}
return dirname(fileURLToPath(import.meta.url));
}
// Resolve a directory at run time: prefer the override env var when set,
// otherwise fall back to the default path derived from this script's own
// location, so the shim keeps working when the config dir moves or is shared
// across environments with different home directories.
function resolveDir(envName: string, defaultPath: string): string {
const value = process.env[envName];
if (value) {
return value;
}
return resolve(defaultPath);
}
async function main(): Promise<void> {
const { agentFunc, rawData } = parseArgv();
const agentData = parseRawData(rawData);
const configDir = "{config_dir}";
setupEnv(configDir, agentFunc, rawData);
const binDir = selfDir();
const agentDir = resolve(binDir, "..");
const configDir = resolveDir("{root_dir_env}", join(binDir, "{root_dir_rel}"));
setupEnv(configDir, agentDir, agentFunc, rawData);
const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts");
const agentToolsPath = join(agentDir, "tools.ts");
await run(agentToolsPath, agentFunc, agentData);
}
@@ -48,12 +69,17 @@ function parseArgv(): { agentFunc: string; rawData: string } {
return { agentFunc, rawData: agentData };
}
function setupEnv(configDir: string, agentFunc: string, rawData: string): void {
function setupEnv(
configDir: string,
agentDir: string,
agentFunc: string,
rawData: string,
): void {
loadEnv(join(configDir, ".env"));
process.env["LLM_ROOT_DIR"] = configDir;
process.env["LLM_AGENT_NAME"] = "{agent_name}";
process.env["LLM_AGENT_FUNC"] = agentFunc;
process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}");
process.env["LLM_AGENT_ROOT_DIR"] = agentDir;
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
process.env["LLM_AGENT_RAW_JSON"] = rawData;
}
+18 -2
View File
@@ -27,14 +27,30 @@ def _ensure_cwd_venv():
_ensure_cwd_venv()
def resolve_dir(env_name, default_path):
"""Resolve a directory at run time.
Prefer the override env var when set, otherwise fall back to the default
path derived from this script's own location, so the shim keeps working
when the config dir moves or is shared across environments with different
home directories.
"""
value = os.environ.get(env_name)
if value:
return value
return os.path.normpath(default_path)
def main():
raw_data = parse_argv()
tool_data = parse_raw_data(raw_data)
root_dir = "{root_dir}"
self_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = resolve_dir("{root_dir_env}", os.path.join(self_dir, "{root_dir_rel}"))
functions_dir = resolve_dir("{functions_dir_env}", os.path.join(self_dir, "{functions_dir_rel}"))
setup_env(root_dir, raw_data)
tool_path = "{tool_path}.py"
tool_path = os.path.join(functions_dir, "tools", "{function_name}.py")
run(tool_path, "run", tool_data)
+23 -3
View File
@@ -5,13 +5,29 @@
set -e
main() {
root_dir="{root_dir}"
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root_dir="$(resolve_dir "{root_dir_env}" "$self_dir/{root_dir_rel}")"
functions_dir="$(resolve_dir "{functions_dir_env}" "$self_dir/{functions_dir_rel}")"
parse_argv "$@"
setup_env
tool_path="{tool_path}.sh"
tool_path="$functions_dir/tools/{function_name}.sh"
run
}
# Resolve a directory at run time: prefer the override env var ($1) when set,
# otherwise fall back to the default path ($2) derived from this script's own
# location, so the shim keeps working when the config dir moves or is shared
# across environments with different home directories.
resolve_dir() {
local override
override="$(printenv "$1" 2>/dev/null || true)"
if [[ -n "$override" ]]; then
echo "$override"
else
(cd "$2" 2>/dev/null && pwd) || echo "$2"
fi
}
parse_argv() {
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
tool_data="$(cat "$LLM_TOOL_DATA_FILE")"
@@ -28,7 +44,7 @@ setup_env() {
export LLM_ROOT_DIR="$root_dir"
export LLM_TOOL_NAME="{function_name}"
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/{function_name}"
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
export LLM_PROMPT_UTILS_FILE="$functions_dir/utils/prompt-utils.sh"
export LLM_TOOL_RAW_JSON="$tool_data"
}
@@ -56,6 +72,10 @@ run() {
die "error: no JSON data"
fi
if [[ ! -f "$tool_path" ]]; then
die "error: tool script not found: $tool_path"
fi
if [[ "$OS" == "Windows_NT" ]]; then
set -o igncr
tool_path="$(cygpath -w "$tool_path")"
+28 -4
View File
@@ -3,17 +3,41 @@
// Usage: ./{function_name}.ts <tool-data>
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { pathToFileURL } from "url";
import { dirname, join, resolve } from "path";
import { fileURLToPath, pathToFileURL } from "url";
function selfDir(): string {
if (typeof __dirname !== "undefined") {
return __dirname;
}
return dirname(fileURLToPath(import.meta.url));
}
// Resolve a directory at run time: prefer the override env var when set,
// otherwise fall back to the default path derived from this script's own
// location, so the shim keeps working when the config dir moves or is shared
// across environments with different home directories.
function resolveDir(envName: string, defaultPath: string): string {
const value = process.env[envName];
if (value) {
return value;
}
return resolve(defaultPath);
}
async function main(): Promise<void> {
const rawData = parseArgv();
const toolData = parseRawData(rawData);
const rootDir = "{root_dir}";
const binDir = selfDir();
const rootDir = resolveDir("{root_dir_env}", join(binDir, "{root_dir_rel}"));
const functionsDir = resolveDir(
"{functions_dir_env}",
join(binDir, "{functions_dir_rel}"),
);
setupEnv(rootDir, rawData);
const toolPath = "{tool_path}.ts";
const toolPath = join(functionsDir, "tools", "{function_name}.ts");
await run(toolPath, "run", toolData);
}
+130 -77
View File
@@ -1175,6 +1175,35 @@ impl Functions {
Self::build_binaries(name, language, BinaryType::Agent, custom_runtime.as_deref())
}
fn render_shim_template(
content_template: &str,
binary_name: &str,
binary_type: &BinaryType,
) -> String {
let functions_dir_env = get_env_name("functions_dir");
match binary_type {
BinaryType::Tool(None) => content_template
.replace("{function_name}", binary_name)
.replace("{root_dir_env}", &functions_dir_env)
.replace("{root_dir_rel}", "..")
.replace("{functions_dir_rel}", ".."),
BinaryType::Tool(Some(agent_name)) => content_template
.replace("{function_name}", binary_name)
.replace(
"{root_dir_env}",
&format!("{}_DATA_DIR", normalize_env_name(agent_name)),
)
.replace("{root_dir_rel}", "..")
.replace("{functions_dir_rel}", "../../../functions"),
BinaryType::Agent => content_template
.replace("{agent_name}", binary_name)
.replace("{root_dir_env}", &get_env_name("config_dir"))
.replace("{root_dir_rel}", "../../..")
.replace("{functions_dir_rel}", "../../../functions"),
}
.replace("{functions_dir_env}", &functions_dir_env)
}
#[cfg(windows)]
fn build_binaries(
binary_name: &str,
@@ -1218,41 +1247,7 @@ impl Functions {
)
})?;
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
let to_script_path = |p: &str| -> String { p.replace('\\', "/") };
let content = match binary_type {
BinaryType::Tool(None) => {
let root_dir = paths::functions_dir();
let tool_path = format!(
"{}/{binary_name}",
paths::global_tools_dir().to_string_lossy()
);
content_template
.replace("{function_name}", binary_name)
.replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
.replace("{tool_path}", &to_script_path(&tool_path))
}
BinaryType::Tool(Some(agent_name)) => {
let root_dir = paths::agent_data_dir(agent_name);
let tool_path = format!(
"{}/{binary_name}",
paths::global_tools_dir().to_string_lossy()
);
content_template
.replace("{function_name}", binary_name)
.replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
.replace("{tool_path}", &to_script_path(&tool_path))
}
BinaryType::Agent => content_template
.replace("{agent_name}", binary_name)
.replace(
"{config_dir}",
&to_script_path(&paths::config_dir().to_string_lossy()),
),
}
.replace(
"{prompt_utils_file}",
&to_script_path(&paths::bash_prompt_utils_file().to_string_lossy()),
);
let content = Self::render_shim_template(content_template, binary_name, &binary_type);
write_file_atomic(&binary_script_file, &content, None)?;
info!(
@@ -1298,21 +1293,17 @@ impl Functions {
_ => bail!("Unsupported language: {}", language.as_ref()),
}
};
let bin_dir = binary_file
.parent()
.expect("Failed to get parent directory of binary file");
let canonical_bin_dir = dunce::canonicalize(bin_dir)?.to_string_lossy().into_owned();
let wrapper_binary = dunce::canonicalize(&binary_script_file)?
.to_string_lossy()
.into_owned();
// %~dp0 (the .cmd's own directory) keeps the launcher relocatable: no
// absolute paths may be baked into it (see render_shim_template).
let script_name = format!("run-{binary_name}.{}", language.to_extension());
let content = formatdoc!(
r#"
@echo off
setlocal
set "bin_dir={canonical_bin_dir}"
set "bin_dir=%~dp0"
{run} "{wrapper_binary}" %*"#,
{run} "%~dp0{script_name}" %*"#,
);
write_file_atomic(&binary_file, &content, None)?;
@@ -1352,37 +1343,7 @@ impl Functions {
)
})?;
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
let mut content = match binary_type {
BinaryType::Tool(None) => {
let root_dir = paths::functions_dir();
let tool_path = format!(
"{}/{binary_name}",
paths::global_tools_dir().to_string_lossy()
);
content_template
.replace("{function_name}", binary_name)
.replace("{root_dir}", &root_dir.to_string_lossy())
.replace("{tool_path}", &tool_path)
}
BinaryType::Tool(Some(agent_name)) => {
let root_dir = paths::agent_data_dir(agent_name);
let tool_path = format!(
"{}/{binary_name}",
paths::global_tools_dir().to_string_lossy()
);
content_template
.replace("{function_name}", binary_name)
.replace("{root_dir}", &root_dir.to_string_lossy())
.replace("{tool_path}", &tool_path)
}
BinaryType::Agent => content_template
.replace("{agent_name}", binary_name)
.replace("{config_dir}", &paths::config_dir().to_string_lossy()),
}
.replace(
"{prompt_utils_file}",
&paths::bash_prompt_utils_file().to_string_lossy(),
);
let mut content = Self::render_shim_template(content_template, binary_name, &binary_type);
if let Some(rt) = custom_runtime
&& let Some(newline_pos) = content.find('\n')
@@ -1398,9 +1359,10 @@ impl Functions {
write_file_atomic(&script_file, &content, Some(0o755))?;
let ts_runtime = custom_runtime.unwrap_or("tsx");
// Locate the script next to the wrapper at run time instead of
// baking an absolute path (see render_shim_template).
let wrapper = format!(
"#!/bin/sh\nexec {ts_runtime} \"{}\" \"$@\"\n",
script_file.display()
"#!/bin/sh\nexec {ts_runtime} \"$(dirname \"$0\")/run-{binary_name}.ts\" \"$@\"\n",
);
write_file_atomic(&binary_file, &wrapper, Some(0o755))?;
} else {
@@ -4613,6 +4575,97 @@ mod tests {
fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
#[serial]
fn built_shims_resolve_paths_at_runtime_after_relocation() {
use std::os::unix::fs::PermissionsExt;
let root = temp_file("-shim-relocate-", "");
let home_a = root.join("home-a");
let config_a = home_a.join("coyote");
let tools_dir = config_a.join("functions").join("tools");
fs::create_dir_all(&tools_dir).unwrap();
fs::create_dir_all(config_a.join("functions").join("bin")).unwrap();
let tool_src = tools_dir.join("relocheck.sh");
fs::write(
&tool_src,
"#!/usr/bin/env bash\necho \"relocated-ok\" >> \"$LLM_OUTPUT\"\n",
)
.unwrap();
fs::set_permissions(&tool_src, fs::Permissions::from_mode(0o755)).unwrap();
let config_env = get_env_name("config_dir");
let functions_env = get_env_name("functions_dir");
let saved_config = env::var(&config_env).ok();
let saved_functions = env::var(&functions_env).ok();
unsafe {
env::set_var(&config_env, &config_a);
env::remove_var(&functions_env);
}
let build_result = Functions::build_global_function_binary("relocheck.sh", None);
unsafe {
match saved_config {
Some(v) => env::set_var(&config_env, v),
None => env::remove_var(&config_env),
}
match saved_functions {
Some(v) => env::set_var(&functions_env, v),
None => env::remove_var(&functions_env),
}
}
build_result.unwrap();
let shim = config_a.join("functions").join("bin").join("relocheck");
let content = fs::read_to_string(&shim).unwrap();
assert!(
!content.contains(config_a.to_str().unwrap()),
"shim must not bake the generation-time config path:\n{content}"
);
assert!(
content.contains(&functions_env),
"shim must consult the functions dir env override at run time:\n{content}"
);
// Simulate the config dir landing under a different home directory
// (shared/synced config dir, or a sandbox with another $HOME).
let home_b = root.join("home-b");
fs::rename(&home_a, &home_b).unwrap();
let moved_shim = home_b
.join("coyote")
.join("functions")
.join("bin")
.join("relocheck");
if which::which("bash").is_err() || which::which("jq").is_err() {
fs::remove_dir_all(&root).unwrap();
return;
}
let output = process::Command::new(&moved_shim)
.arg("{}")
.env_remove(&config_env)
.env_remove(&functions_env)
.env_remove("LLM_OUTPUT")
.env_remove("LLM_TOOL_DATA_FILE")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"relocated shim failed: stderr={} stdout={stdout}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
stdout.contains("relocated-ok"),
"unexpected output: {stdout}"
);
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn eval_tool_calls_partitions_mcp_and_sequential_then_resorts() {
let mut ctx = RequestContext::new(Arc::new(AppState::test_default()), WorkingMode::Cmd);