use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
use basemind::config::{self, Config, DocumentsCliOverrides};
use basemind::render::{self, Verbosity};
use basemind::store::{LockHolder, Store};
use basemind::watcher::{BatchKind, WatchBatch};
mod lang_cli;
#[derive(Parser, Debug)]
#[command(
name = "basemind",
version,
about = "File-watcher and code-map generator using tree-sitter",
long_about = None
)]
struct Cli {
#[arg(long, global = true)]
root: Option<PathBuf>,
#[arg(short, long, global = true, conflicts_with = "verbose")]
quiet: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(long, global = true)]
no_color: bool,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true, default_value_t = basemind::store::VIEW_WORKING.to_string())]
view: String,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand, Debug)]
enum Cmd {
Init,
Scan(ScanArgs),
Rescan(RescanArgs),
Watch,
#[command(subcommand)]
Query(basemind::cli::codemap::QueryCmd),
#[command(subcommand)]
Git(basemind::cli::git::GitCmd),
#[command(subcommand)]
Memory(basemind::cli::memory::MemoryCmd),
#[command(subcommand)]
Governance(basemind::cli::governance::GovernanceCmd),
#[command(subcommand)]
Web(basemind::cli::web::WebCmd),
Telemetry {
#[arg(long)]
window: Option<String>,
#[arg(long)]
tool: Option<String>,
},
Hook {
#[command(subcommand)]
action: HookCmd,
},
Lang {
#[command(subcommand)]
action: LangCmd,
},
CompressOutput(basemind::textcompress::cli::CompressOutputArgs),
Delta(basemind::textcompress::cli::DeltaArgs),
Checkpoint(basemind::textcompress::cli::CheckpointArgs),
DetectWaste(basemind::textcompress::cli::DetectWasteArgs),
Serve(ServeArgs),
#[command(subcommand)]
Cache(basemind::cli::admin::CacheCmd),
#[cfg(all(feature = "comms", unix))]
Comms {
#[command(subcommand)]
action: CommsLifecycleCmd,
},
#[cfg(feature = "a2a")]
A2a {
#[command(subcommand)]
action: A2aCmd,
},
}
#[cfg(feature = "a2a")]
#[derive(Subcommand, Debug)]
enum A2aCmd {
Serve(A2aServeArgs),
}
#[cfg(feature = "a2a")]
#[derive(clap::Args, Debug)]
struct A2aServeArgs {
#[arg(long, default_value = "127.0.0.1:8723")]
addr: std::net::SocketAddr,
#[arg(long)]
name: Option<String>,
#[arg(long)]
description: Option<String>,
#[arg(long, env = "BASEMIND_A2A_TOKEN")]
token: Option<String>,
#[arg(long)]
token_file: Option<std::path::PathBuf>,
#[arg(long, requires = "tls_key")]
tls_cert: Option<std::path::PathBuf>,
#[arg(long, requires = "tls_cert")]
tls_key: Option<std::path::PathBuf>,
}
#[cfg(all(feature = "comms", unix))]
#[derive(Subcommand, Debug)]
enum CommsLifecycleCmd {
Daemon,
Start,
Stop,
Status,
#[command(flatten)]
Agent(basemind::cli::comms::CommsAgentCmd),
}
#[derive(clap::Args, Debug)]
struct ScanArgs {
#[arg(long, conflicts_with = "rev")]
staged: bool,
#[arg(long, value_name = "REV")]
rev: Option<String>,
#[command(flatten)]
documents: DocumentsCliOverrides,
}
#[derive(clap::Args, Debug)]
struct RescanArgs {
#[arg(value_name = "PATH")]
paths: Vec<String>,
#[arg(long)]
full: bool,
}
#[derive(clap::Args, Debug)]
struct ServeArgs {
#[arg(long, default_value_t = 1024)]
git_cache_mem: usize,
#[arg(long)]
no_git_cache_disk: bool,
#[arg(long)]
no_watch: bool,
#[command(flatten)]
documents: DocumentsCliOverrides,
}
#[derive(Subcommand, Debug)]
enum LangCmd {
List,
Install,
Clean,
}
#[derive(Subcommand, Debug)]
enum HookCmd {
Install,
}
fn default_log_directive(verbosity: Verbosity) -> &'static str {
match verbosity {
Verbosity::Quiet => "warn",
Verbosity::Default => "info",
Verbosity::Verbose => "debug",
}
}
fn main() -> Result<()> {
let cli = Cli::parse();
let verbosity = Verbosity::from_flags(cli.quiet, cli.verbose);
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(default_log_directive(verbosity))),
)
.with_target(false)
.with_writer(std::io::stderr)
.init();
#[cfg(feature = "a2a")]
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let no_color = cli.no_color;
let start = cli
.root
.clone()
.map(|p| p.canonicalize().unwrap_or(p))
.unwrap_or_else(|| std::env::current_dir().expect("cwd"));
let root = match basemind::git::Repo::discover(&start) {
Ok(repo) => repo.workdir().to_path_buf(),
Err(_) => start,
};
let json = cli.json;
let view = cli.view.clone();
warn_ignored_global_flags(&cli.cmd, json, &view);
let dispatch =
|tc| basemind::cli::run(&root, &view, DocumentsCliOverrides::default(), json, tc);
match cli.cmd {
Cmd::Init => cmd_init(&root),
Cmd::Scan(args) => cmd_scan(&root, &args, verbosity, no_color),
Cmd::Rescan(args) => cmd_rescan(&root, &args, verbosity, no_color),
Cmd::Watch => cmd_watch(&root, verbosity, no_color),
Cmd::Query(q) => {
let _ = basemind::lang::ensure_grammars();
dispatch(basemind::cli::ToolCmd::Query(q))
}
Cmd::Git(g) => dispatch(basemind::cli::ToolCmd::Git(g)),
Cmd::Memory(m) => dispatch(basemind::cli::ToolCmd::Memory(m)),
Cmd::Governance(g) => dispatch(basemind::cli::ToolCmd::Governance(g)),
Cmd::Web(w) => dispatch(basemind::cli::ToolCmd::Web(w)),
Cmd::Telemetry { window, tool } => {
dispatch(basemind::cli::ToolCmd::Telemetry { window, tool })
}
Cmd::Hook { action } => match action {
HookCmd::Install => cmd_hook_install(&root),
},
Cmd::Lang { action } => match action {
LangCmd::List => lang_cli::cmd_lang_list(no_color),
LangCmd::Install => lang_cli::cmd_lang_install(verbosity, no_color),
LangCmd::Clean => lang_cli::cmd_lang_clean(),
},
Cmd::CompressOutput(args) => basemind::textcompress::cli::run(&args),
Cmd::Delta(args) => basemind::textcompress::cli::run_delta(&args),
Cmd::Checkpoint(args) => basemind::textcompress::cli::run_checkpoint(&root, &args),
Cmd::DetectWaste(args) => basemind::textcompress::cli::run_detect_waste(&args),
Cmd::Serve(args) => cmd_serve(&root, &view, &args),
Cmd::Cache(action) => basemind::cli::run_cache(&root, action, json),
#[cfg(all(feature = "comms", unix))]
Cmd::Comms { action } => cmd_comms(&root, action, json),
#[cfg(feature = "a2a")]
Cmd::A2a { action } => cmd_a2a(action),
}
}
#[cfg(feature = "a2a")]
fn cmd_a2a(action: A2aCmd) -> Result<()> {
match action {
A2aCmd::Serve(args) => {
let options = basemind::a2a::A2aServeOptions {
addr: args.addr,
name: args.name,
description: args.description,
token: args.token,
token_file: args.token_file,
tls_cert: args.tls_cert,
tls_key: args.tls_key,
};
basemind::a2a::run_server(options).context("run A2A server")
}
}
}
#[cfg(all(feature = "comms", unix))]
fn cmd_comms(root: &std::path::Path, action: CommsLifecycleCmd, json: bool) -> Result<()> {
match action {
CommsLifecycleCmd::Daemon => cmd_comms_daemon(),
CommsLifecycleCmd::Start => cmd_comms_start(),
CommsLifecycleCmd::Stop => cmd_comms_lifecycle_rpc(CommsRpc::Stop, json),
CommsLifecycleCmd::Status => cmd_comms_lifecycle_rpc(CommsRpc::Status, json),
CommsLifecycleCmd::Agent(cmd) => basemind::cli::comms::run(root, json, cmd),
}
}
#[cfg(all(feature = "comms", unix))]
enum CommsRpc {
Stop,
Status,
}
#[cfg(all(feature = "comms", unix))]
fn cmd_comms_daemon() -> Result<()> {
use std::sync::Arc;
use basemind::comms::daemon::Broker;
use basemind::comms::singleton;
use basemind::comms::store::CommsStore;
let paths = singleton::resolve_paths().context("resolve comms paths")?;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
runtime.block_on(async move {
let listener = match singleton::bind_listener(&paths.socket_path, singleton::probe_alive) {
Ok(listener) => listener,
Err(basemind::comms::singleton::SingletonError::AlreadyRunning(p)) => {
tracing::info!(socket = %p.display(), "comms daemon already running; exiting");
return Ok(());
}
Err(e) => return Err(anyhow::anyhow!("bind comms socket: {e}")),
};
let store = Arc::new(CommsStore::open(&paths.comms_dir).context("open comms store")?);
let broker = Arc::new(Broker::new(store));
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
let broker_for_signal = broker.clone();
let shutdown_for_signal = shutdown_tx.clone();
tokio::spawn(async move {
wait_for_shutdown_signal().await;
tracing::info!("comms: shutdown signal received; draining");
broker_for_signal.begin_drain().await;
let _ = shutdown_for_signal.send(true);
});
let frontend: Box<dyn CommsFrontendObj> = Box::new(UdsFrontendBox(
basemind::comms::frontend_uds::UdsFrontend::from_listener(
listener,
paths.socket_path.clone(),
),
));
frontend
.serve_obj(broker, shutdown_rx)
.await
.context("comms front-end serve loop")
})?;
Ok(())
}
#[cfg(all(feature = "comms", unix))]
trait CommsFrontendObj: Send {
fn serve_obj(
self: Box<Self>,
broker: std::sync::Arc<basemind::comms::daemon::Broker>,
shutdown: tokio::sync::watch::Receiver<bool>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + Send>>;
}
#[cfg(all(feature = "comms", unix))]
struct UdsFrontendBox(basemind::comms::frontend_uds::UdsFrontend);
#[cfg(all(feature = "comms", unix))]
impl CommsFrontendObj for UdsFrontendBox {
fn serve_obj(
self: Box<Self>,
broker: std::sync::Arc<basemind::comms::daemon::Broker>,
shutdown: tokio::sync::watch::Receiver<bool>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + Send>> {
use basemind::comms::transport::CommsFrontend;
Box::pin(async move { Box::new(self.0).serve(broker, shutdown).await })
}
}
#[cfg(all(feature = "comms", unix))]
async fn wait_for_shutdown_signal() {
use tokio::signal::unix::{SignalKind, signal};
let mut term = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(_) => {
let _ = tokio::signal::ctrl_c().await;
return;
}
};
tokio::select! {
_ = term.recv() => {}
_ = tokio::signal::ctrl_c() => {}
}
}
#[cfg(all(feature = "comms", unix))]
fn cmd_comms_start() -> Result<()> {
use basemind::comms::singleton;
let paths = singleton::resolve_paths().context("resolve comms paths")?;
let socket_path = paths.socket_path.clone();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
runtime.block_on(async move {
singleton::ensure_daemon(&paths)
.await
.map_err(|e| anyhow::anyhow!("ensure comms daemon: {e}"))
})?;
println!("comms daemon is running ({})", socket_path.display());
Ok(())
}
#[cfg(all(feature = "comms", unix))]
fn cmd_comms_lifecycle_rpc(rpc: CommsRpc, json: bool) -> Result<()> {
use basemind::comms::client::CommsClient;
use basemind::comms::ids::AgentId;
use basemind::comms::singleton;
let paths = singleton::resolve_paths().context("resolve comms paths")?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
runtime.block_on(async move {
let agent = AgentId::parse("basemind-cli").map_err(|e| anyhow::anyhow!("agent id: {e}"))?;
let mut client = CommsClient::connect(&paths, agent, None, None)
.await
.map_err(|e| anyhow::anyhow!("connect to comms daemon: {e}"))?;
match rpc {
CommsRpc::Stop => {
client
.stop()
.await
.map_err(|e| anyhow::anyhow!("stop: {e}"))?;
if json {
println!("{{\"stopped\":true}}");
} else {
println!("comms daemon stopping");
}
}
CommsRpc::Status => {
let status = client
.status()
.await
.map_err(|e| anyhow::anyhow!("status: {e}"))?;
if json {
println!(
"{}",
serde_json::to_string(&status)
.map_err(|e| anyhow::anyhow!("serialize status: {e}"))?
);
} else {
println!(
"pid={} version={} proto={} uptime={}s rooms={} subscribers={}",
status.pid,
status.version,
status.proto_ver,
status.uptime_secs,
status.rooms,
status.subscribers,
);
}
}
}
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}
fn warn_ignored_global_flags(cmd: &Cmd, json: bool, view: &str) {
let consumes_json = matches!(
cmd,
Cmd::Query(_)
| Cmd::Git(_)
| Cmd::Memory(_)
| Cmd::Web(_)
| Cmd::Telemetry { .. }
| Cmd::Cache(_)
);
#[cfg(all(feature = "comms", unix))]
let consumes_json = consumes_json || matches!(cmd, Cmd::Comms { .. });
let consumes_view = consumes_json || matches!(cmd, Cmd::Serve(_));
if json && !consumes_json {
tracing::warn!("--json has no effect on this subcommand; ignoring");
}
if view != basemind::store::VIEW_WORKING && !consumes_view {
tracing::warn!(view = %view, "--view has no effect on this subcommand; ignoring");
}
}
fn bootstrap_grammars(verbosity: Verbosity, no_color: bool) -> Result<()> {
let summary = basemind::lang::ensure_grammars()
.map_err(|e| anyhow::anyhow!("grammar bootstrap failed: {e}"))?;
let mut out = render::stdout(no_color);
render::render_grammar_bootstrap(&mut out, &summary, verbosity);
Ok(())
}
fn cmd_init(root: &std::path::Path) -> Result<()> {
let dir = root.join(config::BASEMIND_DIR);
std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?;
let path = config::config_path(root);
if path.exists() {
anyhow::bail!("config already exists at {}", path.display());
}
let default_toml = r##""$schema" = "v1"
[scan]
include = ["**/*.rs", "**/*.py", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.go"]
exclude = ["**/target/**", "**/node_modules/**", "**/dist/**", "**/.venv/**", "**/.basemind/**", "**/.git/**"]
respect_gitignore = true
max_file_bytes = 2097152
[watch]
debounce_ms = 250
[cache]
file_map_lru = 256
[mcp]
transport = "stdio"
"##;
std::fs::write(&path, default_toml).with_context(|| format!("write {}", path.display()))?;
println!("wrote {}", path.display());
Ok(())
}
fn load_or_default(root: &std::path::Path) -> Result<Config> {
load_or_default_with(root, None)
}
fn load_or_default_with(
root: &std::path::Path,
cli: Option<DocumentsCliOverrides>,
) -> Result<Config> {
match config::load_with_overrides(root, None, cli) {
Ok(loaded) => Ok(loaded.config),
Err(config::ConfigError::NotFound(_)) => {
tracing::info!("no .basemind/basemind.toml; using defaults");
Ok(config::default_for_root(root))
}
Err(e) => Err(anyhow::anyhow!(e)),
}
}
fn open_store_for_write(
root: &std::path::Path,
view: &str,
what: &str,
holder: LockHolder,
) -> Result<Store> {
Store::open_with_holder(root, view, holder).map_err(|err| {
if err.is_lock_contention() {
anyhow::Error::new(err).context(basemind::store::LOCK_CONTENTION_HELP.to_string())
} else {
anyhow::Error::new(err).context(format!("open store ({what})"))
}
})
}
fn cmd_scan(
root: &std::path::Path,
args: &ScanArgs,
verbosity: Verbosity,
no_color: bool,
) -> Result<()> {
bootstrap_grammars(verbosity, no_color)?;
let config = load_or_default_with(root, Some(args.documents.clone()))?;
let mut out = render::stdout(no_color);
if args.staged {
let repo = basemind::git::Repo::discover(root)
.context("`--staged` requires being inside a git repository")?;
let mut store = open_store_for_write(
root,
basemind::store::VIEW_STAGED,
"staged",
LockHolder::Scan,
)?;
render::render_scan_header(&mut out, "staged index", verbosity);
let report = basemind::scanner::scan(
root,
&mut store,
&config,
basemind::scanner::ScanSource::Staged(&repo),
)
.context("scan staged")?;
render::render_report(&mut out, &report, verbosity);
return Ok(());
}
if let Some(rev_spec) = &args.rev {
let repo = basemind::git::Repo::discover(root)
.context("`--rev` requires being inside a git repository")?;
let sha = repo.resolve_rev(rev_spec).context("resolve rev")?;
let short = &sha[..7.min(sha.len())];
let view = basemind::store::view_name_for_rev(short);
let mut store = open_store_for_write(root, &view, "rev", LockHolder::Scan)?;
render::render_scan_header(&mut out, &format!("rev {short}"), verbosity);
let report = basemind::scanner::scan(
root,
&mut store,
&config,
basemind::scanner::ScanSource::Rev {
repo: &repo,
sha: sha.clone(),
},
)
.context("scan rev")?;
render::render_report(&mut out, &report, verbosity);
return Ok(());
}
let mut store = open_store_for_write(
root,
basemind::store::VIEW_WORKING,
"scan",
LockHolder::Scan,
)?;
let report = basemind::scanner::scan(
root,
&mut store,
&config,
basemind::scanner::ScanSource::WorkingTree,
)
.context("scan")?;
render::render_report(&mut out, &report, verbosity);
Ok(())
}
fn cmd_rescan(
root: &std::path::Path,
args: &RescanArgs,
verbosity: Verbosity,
no_color: bool,
) -> Result<()> {
bootstrap_grammars(verbosity, no_color)?;
let config = load_or_default(root)?;
let mut store = open_store_for_write(
root,
basemind::store::VIEW_WORKING,
"rescan",
LockHolder::Rescan,
)?;
let mut out = render::stdout(no_color);
let report = if args.full || args.paths.is_empty() {
basemind::scanner::scan(
root,
&mut store,
&config,
basemind::scanner::ScanSource::WorkingTree,
)
.context("rescan (full)")?
} else {
let abs: Vec<PathBuf> = args.paths.iter().map(|p| root.join(p)).collect();
basemind::scanner::scan_paths(root, &mut store, &config, &abs).context("rescan (paths)")?
};
render::render_report(&mut out, &report, verbosity);
Ok(())
}
fn cmd_watch(root: &std::path::Path, verbosity: Verbosity, no_color: bool) -> Result<()> {
bootstrap_grammars(verbosity, no_color)?;
let config = Arc::new(load_or_default(root)?);
let store = Arc::new(Mutex::new(
Store::open_with_holder(root, basemind::store::VIEW_WORKING, LockHolder::Watch)
.context("open store")?,
));
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
let store_w = Arc::clone(&store);
let config_w = Arc::clone(&config);
let root_buf = root.to_path_buf();
let watcher_handle = std::thread::spawn(move || {
let mut stdout = render::stdout(no_color);
let cb: basemind::watcher::BatchCallback =
Box::new(move |batch: WatchBatch<'_>| match batch.kind {
BatchKind::InitialScan => {
render::render_report(&mut stdout, batch.report, verbosity);
}
BatchKind::Incremental { paths } => {
render::render_batch_header(&mut stdout, paths, verbosity);
render::render_lines(&mut stdout, batch.report, verbosity);
}
});
basemind::watcher::watch(&root_buf, store_w, config_w, shutdown_rx, cb)
});
runtime.block_on(async {
let _ = tokio::signal::ctrl_c().await;
tracing::info!("ctrl-c received; shutting down");
let _ = shutdown_tx.send(());
});
match watcher_handle.join() {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(anyhow::anyhow!(e)),
Err(_) => Err(anyhow::anyhow!("watcher thread panicked")),
}
}
fn cmd_serve(root: &std::path::Path, view: &str, args: &ServeArgs) -> Result<()> {
if view != basemind::store::VIEW_WORKING {
let index_path = root
.join(basemind::config::BASEMIND_DIR)
.join(basemind::store::VIEWS_DIR)
.join(view)
.join(basemind::store::INDEX_FILE);
if !index_path.exists() {
anyhow::bail!(
"view {view:?} has not been scanned; run `basemind scan --view {view}` first \
(or omit --view to serve the working view)"
);
}
}
let store = Store::open_with_holder(root, view, LockHolder::Serve).context("open store")?;
let basemind_dir = root.join(basemind::config::BASEMIND_DIR);
let root_buf = root.to_path_buf();
let config = Arc::new(load_or_default_with(root, Some(args.documents.clone()))?);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.context("build tokio runtime")?;
let repo = basemind::git::Repo::discover(root).ok().map(Arc::new);
let git_cache = Arc::new(
basemind::git_cache::GitCache::open(
&basemind_dir,
args.git_cache_mem,
!args.no_git_cache_disk,
)
.context("open git cache")?,
);
let options = basemind::mcp::ServerOptions {
background: true,
watch: !args.no_watch,
};
tracing::info!(
pid = std::process::id(),
version = env!("CARGO_PKG_VERSION"),
view,
root = %root.display(),
"basemind serve: MCP server starting"
);
let outcome = runtime.block_on(async move {
use rmcp::ServiceExt;
let server = basemind::mcp::BasemindServer::new_with_options(
store, root_buf, config, repo, git_cache, options,
);
let transport = rmcp::transport::stdio();
let service = server
.serve(transport)
.await
.map_err(|e| anyhow::anyhow!("rmcp serve: {e}"))?;
service
.waiting()
.await
.map_err(|e| anyhow::anyhow!("rmcp waiting: {e}"))?;
Ok::<(), anyhow::Error>(())
});
match &outcome {
Ok(()) => tracing::info!(
pid = std::process::id(),
"basemind serve: client disconnected, exiting"
),
Err(error) => {
tracing::error!(pid = std::process::id(), %error, "basemind serve: exiting on error")
}
}
outcome
}
fn cmd_hook_install(root: &std::path::Path) -> Result<()> {
let hooks_dir = root.join(".git").join("hooks");
if !hooks_dir.exists() {
anyhow::bail!("no .git/hooks directory at {}", hooks_dir.display());
}
let hook_path = hooks_dir.join("pre-commit");
let body = r#"#!/usr/bin/env sh
# Installed by basemind hook install.
set -e
exec basemind scan --staged --quiet
"#;
std::fs::write(&hook_path, body)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&hook_path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&hook_path, perms)?;
}
println!("installed pre-commit hook at {}", hook_path.display());
Ok(())
}