use crate::daemon::{self, State, Store};
use crate::filter::{Filter, LANGUAGES};
use crate::http::{Handler, Request, Response};
use crate::{chunk, embed, home, store};
use fastembed::TextEmbedding;
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::Ordering;
const FILE_PAGE: i64 = 500;
const DERIVED_SORT_MAX: i64 = 20_000;
const LEDGER_ROWS: usize = 200;
const LEDGER_SESSIONS: usize = 500;
const FILE_OFFSET_MAX: i64 = 10_000;
pub fn handler(state: Arc<State>) -> Handler {
Arc::new(move |request| route(&state, request))
}
fn route(state: &Arc<State>, request: &Request) -> Response {
let path = request.path.as_str();
let get = request.method == "GET";
let post = request.method == "POST";
if get && let Some((kind, bytes)) = crate::portal::asset(path) {
return Response::asset(kind, bytes);
}
match (get, post, path) {
(true, _, "/") => crate::portal::page(),
#[cfg(debug_assertions)]
(true, _, "/api/panic") => panic!("the route that exists to panic, panicking"),
(true, _, "/api/stores") => stores(state, request),
(true, _, "/api/files") => files(state, request),
(true, _, "/api/search") => search(state, request),
(true, _, "/api/brief") => brief(state, request),
(true, _, "/api/models") => models(),
(true, _, "/api/languages") => languages(),
(true, _, "/api/dirs") => dirs(request),
(true, _, "/api/privacy") => privacy(state),
(true, _, "/api/privacy/scan") => privacy_scan(state),
(true, _, "/api/about") => about(state),
(true, _, "/api/agents") => agents(state),
(true, _, "/api/pattern") => pattern(state, request),
(true, _, "/api/read") => read(state, request),
(true, _, "/api/symbol") => symbol(state, request),
(true, _, "/api/neighbors") => neighbors(state, request),
(true, _, "/api/path") => shortest_path(state, request),
(true, _, "/api/impact") => impact(state, request),
(true, _, "/api/trace") => trace(state, request),
(true, _, "/api/map") => map(state, request),
(true, _, "/api/report") => report(state, request),
(_, _, "/api/ledger/replay") if get || post => replay(request),
(true, _, "/api/graph") => graph(state, request),
(true, _, "/api/ledger") => ledger(state),
(true, _, "/api/image") => image_file(state, request),
(true, _, "/api/setup") => setup(),
(true, _, "/api/doctor") => doctor(state),
(true, _, "/api/index/runs") => index_runs(state),
(true, _, "/api/index/log") => index_log(state, request),
(true, _, "/api/projects") => projects(request),
(true, _, "/api/changes") => changes(state),
(_, true, "/api/index") => index(state, request),
(_, true, "/api/add") => add(state, request),
(_, true, "/api/forget") => forget(state, request),
(_, true, "/api/adopt") => adopt(state, request),
(_, true, "/api/trust") => trust(state, request),
(_, true, "/api/ledger/raw-read") => raw_read(state, request),
(_, true, "/api/agents/reveal") => reveal(state),
(_, true, "/api/rotate") => rotate(state),
(_, true, "/api/mcp") => mcp(state, request),
(_, true, crate::http::MCP_PATH) => mcp(state, request),
(_, true, "/api/endpoint") => endpoint(state, request),
(_, true, "/api/key") => key(state, request),
(_, true, "/api/setup") => fix_setup(request),
(_, true, "/api/privacy/fix") => privacy_fix(state, request),
(_, true, "/api/agents/register") => register_clients(state, request),
(_, true, "/api/root") => root(state, request),
(_, true, "/api/store/delete") => delete_store(state, request),
(_, true, "/api/index/control") => index_control(state, request),
(_, true, "/api/index/settings") => index_settings(state, request),
(_, true, "/api/upgrade") => upgrade(request),
(
_,
_,
"/api/index"
| "/api/add"
| "/api/forget"
| "/api/adopt"
| "/api/rotate"
| "/api/mcp"
| "/api/upgrade"
| "/api/privacy/fix"
| "/api/agents/register",
) => Response::error(405, "wrong method for this route"),
_ => Response::error(404, "no such route"),
}
}
fn stores(state: &Arc<State>, request: &Request) -> Response {
let want_coverage = request.query("coverage") == Some("1");
let unopened = state.reconcile();
if let Err(e) = state.open_fleet() {
return Response::error(500, &e.to_string());
}
let mut fleet = state.fleet.lock().unwrap_or_else(|e| e.into_inner());
let registry = home::Registry::load().unwrap_or_default();
let mut out = Vec::new();
for handle in state.stores() {
let stats = fleet
.as_mut()
.and_then(|f| {
f.each()
.find(|(_, s)| s.dir() == handle.dir)
.map(|(_, s)| s)
})
.map(|s| {
(
s.stats(),
s.model().to_string(),
s.dim(),
s.len(),
s.shards(),
store::file_facets(s.db(), &[]).unwrap_or_default(),
store::last_write(s.db()).ok().flatten(),
store::ledger_savings(s.db()).ok(),
if want_coverage {
store::coverage_by_language(s.db()).unwrap_or_default()
} else {
Vec::new()
},
if want_coverage {
let (top, distinct) =
store::unresolved_targets(s.db(), 5).unwrap_or_default();
let several =
store::names_with_several_definitions(s.db()).unwrap_or_default();
let months = store::chunks_by_month(s.db()).unwrap_or_default();
Some(json!({
"unresolved_top": top
.iter()
.map(|(name, n)| json!({ "name": name, "edges": n }))
.collect::<Vec<_>>(),
"unresolved_names": distinct,
"several_definitions": several,
"months": months
.iter()
.map(|(month, n)| json!({ "month": month, "chunks": n }))
.collect::<Vec<_>>(),
}))
} else {
None
},
)
});
#[allow(clippy::type_complexity)]
let (
files,
chunks,
bytes,
model,
dim,
vectors,
shards,
facets,
written,
savings,
coverage,
health,
) = match stats {
Some((
Ok((f, c, b)),
model,
dim,
len,
shards,
facets,
written,
savings,
coverage,
health,
)) => (
f, c, b, model, dim, len, shards, facets, written, savings, coverage, health,
),
_ => (
0,
0,
0,
String::new(),
0,
0,
None,
store::Facets::default(),
None,
None,
Vec::new(),
None,
),
};
let session = handle.last_write.load(Ordering::Relaxed) as i64;
let last_write = written.unwrap_or(0).max(session);
let mut readers: Vec<&'static str> = facets
.extensions
.iter()
.map(|ext| chunk::reader_of(Path::new(&format!("x.{ext}"))))
.collect();
readers.sort_unstable();
readers.dedup();
out.push(json!({
"name": handle.name,
"dir": crate::plain(&handle.dir.display().to_string()),
"loose_mode": home::loose_mode(&handle.dir).map(|m| format!("{m:o}")),
"trusted": registry.trusts(&handle.dir),
"roots": handle.roots.iter().map(|r| json!({
"path": crate::plain(&r.display().to_string()),
"present": r.exists(),
})).collect::<Vec<_>>(),
"files": files,
"chunks": chunks,
"bytes": bytes,
"model": model,
"dim": dim,
"vectors": vectors,
"shards": shards.map(|(n, max)| json!({ "count": n, "resident": max })),
"coverage": coverage.iter().map(|row| json!({
"language": row.language,
"files": row.files,
"parser_failed": row.parser_failed,
"definitions": row.definitions,
"extracted": row.extracted,
"resolved": row.resolved,
"ambiguous": row.ambiguous,
"unresolved": row.unresolved,
"settled": row.settled_share(),
})).collect::<Vec<_>>(),
"health": health,
"lines": facets.lines,
"formats": facets.extensions.len(),
"readers": readers.len(),
"watching": handle.watching.load(Ordering::Relaxed),
"queue": handle.queue_depth(),
"last_write": last_write,
"pruned": handle.pruned.load(Ordering::Relaxed),
"missing": false,
"savings": savings.map(|s| json!({
"net_tokens": s.net,
"credited": s.credited,
"total": s.total,
"coverage": s.coverage(),
"tier": s.tier(),
})),
"events": handle.events(),
}));
}
for store in unopened {
out.push(json!({
"name": store.name,
"dir": crate::plain(&store.dir.display().to_string()),
"unopened": store.why,
"missing": store.missing,
"roots": [],
"files": 0,
"chunks": 0,
"bytes": 0,
"model": "",
"dim": 0,
"vectors": 0,
"lines": 0,
"formats": 0,
"readers": 0,
"watching": false,
"queue": 0,
"last_write": 0,
"pruned": 0,
"events": [],
}));
}
Response::json(&json!({ "stores": out }))
}
fn files(state: &Arc<State>, request: &Request) -> Response {
let filter = match filter_of(request) {
Ok(f) => f,
Err(e) => return Response::error(400, &e),
};
let only = request.query_all("store");
let offset = request
.query("offset")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(0);
if !(0..=FILE_OFFSET_MAX).contains(&offset) {
return Response::error(
400,
&format!("offset must be between 0 and {FILE_OFFSET_MAX}"),
);
}
if let Err(e) = state.open_fleet() {
return Response::error(500, &e.to_string());
}
let mut fleet = state.fleet.lock().unwrap_or_else(|e| e.into_inner());
let Some(fleet) = fleet.as_mut() else {
return Response::json(&json!({ "files": [], "total": 0 }));
};
let sort = request
.query("sort")
.and_then(store::FileSort::parse)
.unwrap_or(store::FileSort::Path);
let desc = request.query("dir") == Some("desc");
let limit = request
.query("limit")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(15);
if !(1..=FILE_PAGE).contains(&limit) {
return Response::error(400, &format!("limit must be between 1 and {FILE_PAGE}"));
}
let mut merged: Vec<(String, store::FileRow)> = Vec::new();
let mut total = 0i64;
let mut extensions: Vec<String> = Vec::new();
let mut stores = 0usize;
for (label, opened) in fleet.each() {
if !only.is_empty() && !only.iter().any(|n| n == label) {
continue;
}
match store::file_count(opened.db(), filter.groups()) {
Ok(count) => {
total += count;
stores += usize::from(count > 0);
}
Err(e) => return Response::error(500, &e.to_string()),
}
match store::file_facets(opened.db(), filter.groups()) {
Ok(facets) => extensions.extend(facets.extensions),
Err(e) => return Response::error(500, &e.to_string()),
}
}
if sort.derived() && total > DERIVED_SORT_MAX {
return Response::error(
400,
&format!(
"this column is derived rather than stored, so ordering by it needs every \
matching row; narrow the filter to {DERIVED_SORT_MAX} files or fewer"
),
);
}
let take = if sort.derived() {
total
} else {
offset.saturating_add(limit)
};
for (label, opened) in fleet.each() {
if !only.is_empty() && !only.iter().any(|n| n == label) {
continue;
}
let listed = match store::file_rows(opened.db(), filter.groups(), sort, desc, take) {
Ok(r) => r,
Err(e) => return Response::error(500, &e.to_string()),
};
merged.extend(listed.into_iter().map(|row| (label.to_string(), row)));
}
merged.sort_by(|(a_store, a), (b_store, b)| {
let order = match sort {
store::FileSort::Path => a.path.cmp(&b.path),
store::FileSort::Bytes => a.bytes.cmp(&b.bytes),
store::FileSort::Chunks => a.chunks.cmp(&b.chunks),
store::FileSort::Lines => a.lines.cmp(&b.lines),
store::FileSort::Indexed => a.indexed_at.cmp(&b.indexed_at),
store::FileSort::Store => a_store.cmp(b_store),
store::FileSort::Reader => {
chunk::reader_of(Path::new(&a.path)).cmp(chunk::reader_of(Path::new(&b.path)))
}
store::FileSort::Lang => {
language_of(Path::new(&a.path)).cmp(language_of(Path::new(&b.path)))
}
};
let order = if desc { order.reverse() } else { order };
order.then_with(|| a.path.cmp(&b.path))
});
let rows: Vec<Value> = merged
.into_iter()
.skip(offset as usize)
.take(limit as usize)
.map(|(label, row)| {
let as_path = Path::new(&row.path);
json!({
"store": label,
"path": crate::plain(&row.path),
"ext": as_path.extension().and_then(|e| e.to_str()).unwrap_or(""),
"lang": language_of(as_path),
"reader": chunk::reader_of(as_path),
"bytes": row.bytes,
"chunks": row.chunks,
"lines": row.lines,
"indexed_at": row.indexed_at,
})
})
.collect();
extensions.sort();
extensions.dedup();
Response::json(&json!({
"files": rows,
"total": total,
"offset": offset,
"limit": limit,
"sort": request.query("sort").unwrap_or("path"),
"dir": if desc { "desc" } else { "asc" },
"formats": extensions.len(),
"stores": stores,
}))
}
fn brief(state: &Arc<State>, request: &Request) -> Response {
let Some(question) = request.query("question").filter(|q| !q.trim().is_empty()) else {
return Response::error(400, "missing question");
};
let budget = request
.query("budget")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(crate::brief::DEFAULT_BUDGET)
.clamp(1, 200_000);
let filter = match filter_of(request) {
Ok(f) => f,
Err(e) => return Response::error(400, &e),
};
let prefer = match request.query("prefer") {
Some(raw) => match crate::Prefer::parse(raw) {
Ok(p) => p,
Err(e) => return Response::error(400, &e.to_string()),
},
None => crate::Prefer::default(),
};
let only: Vec<String> = request
.query("store")
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_default();
if let Err(e) = state.open_fleet() {
return Response::error(500, &e.to_string());
}
let mut fleet = state.fleet.lock().unwrap_or_else(|e| e.into_inner());
let Some(fleet) = fleet.as_mut() else {
return Response::json(&json!({ "spans": [], "symbols": [] }));
};
let started = std::time::Instant::now();
let only = (!only.is_empty()).then_some(only);
let brief = match crate::brief::brief(fleet, only.as_deref(), question, budget, &filter, prefer)
{
Ok(b) => b,
Err(e) => return Response::error(500, &e.to_string()),
};
let elapsed = started.elapsed();
let body = match serde_json::to_value(&brief) {
Ok(v) => v,
Err(e) => return Response::error(500, &e.to_string()),
};
if state.ledger {
crate::ledger::brief(
fleet,
&crate::ledger::Who {
client: "portal",
session: "portal",
},
question,
&brief,
&body.to_string(),
elapsed,
);
}
Response::json(&json!({
"brief": body,
"chunks": fleet.chunks(),
"micros": elapsed.as_micros() as u64,
}))
}
fn search(state: &Arc<State>, request: &Request) -> Response {
let Some(query) = request.query("query").filter(|q| !q.trim().is_empty()) else {
return Response::error(400, "missing query");
};
let k = request
.query("k")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(8)
.clamp(1, 100);
let filter = match filter_of(request) {
Ok(f) => f,
Err(e) => return Response::error(400, &e),
};
let only = request.query_all("store");
let offset = request
.query("offset")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(0);
if !(0..=FILE_OFFSET_MAX).contains(&offset) {
return Response::error(
400,
&format!("offset must be between 0 and {FILE_OFFSET_MAX}"),
);
}
if let Err(e) = state.open_fleet() {
return Response::error(500, &e.to_string());
}
let mut fleet = state.fleet.lock().unwrap_or_else(|e| e.into_inner());
let Some(fleet) = fleet.as_mut() else {
return Response::json(&json!({ "hits": [], "selected": 0 }));
};
let selected = if filter.is_empty() {
None
} else {
match fleet.matching_files(&filter) {
Ok(n) => Some(n),
Err(e) => return Response::error(500, &e.to_string()),
}
};
if selected == Some(0) {
return Response::json(&json!({ "hits": [], "selected": 0, "chunks": fleet.chunks() }));
}
let prefer = match request.query("prefer") {
Some(raw) => match crate::Prefer::parse(raw) {
Ok(p) => p,
Err(e) => return Response::error(400, &e.to_string()),
},
None => crate::Prefer::default(),
};
let started = std::time::Instant::now();
let only = (!only.is_empty()).then_some(only);
let deep = (offset as usize)
.saturating_add(k)
.min(FILE_OFFSET_MAX as usize);
let found = match fleet.search_preferring(only.as_deref(), query, deep, &filter, prefer) {
Ok(h) => h,
Err(e) => return Response::error(500, &e.to_string()),
};
let elapsed = started.elapsed();
let hits: Vec<_> = found.into_iter().skip(offset as usize).take(k).collect();
let single = fleet.len() == 1;
let label = fleet.labels().first().map(|s| s.to_string());
let locate = request
.query("format")
.is_some_and(|f| f.eq_ignore_ascii_case("locate"));
let out: Vec<Value> = hits
.iter()
.map(|h| {
json!({
"score": h.score,
"path": crate::plain(&h.path),
"start_line": h.start_line,
"end_line": h.end_line,
"text": if locate { String::new() } else { h.text.clone() },
"store": h.store.clone().or_else(|| single.then(|| label.clone()).flatten()),
"lists": h.lists,
"image": h.image.map(|px| json!({ "width": px.width, "height": px.height })),
"fresh": h.fresh,
"symbol": h.symbol,
"symbol_kind": h.symbol_kind,
"provenance": h.provenance,
})
})
.collect();
if state.ledger {
crate::ledger::search(
fleet,
&crate::ledger::Who {
client: "portal",
session: "portal",
},
query,
&hits,
elapsed,
);
}
let shape = crate::shape_of(query);
Response::json(&json!({
"hits": out,
"offset": offset,
"selected": selected,
"chunks": fleet.chunks(),
"micros": elapsed.as_micros() as u64,
"shape": shape,
"shape_label": shape.as_str(),
"weighting": shape.weighting(),
"prefer": prefer,
}))
}
fn raw_read(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let Some(path) = body.get("path").and_then(Value::as_str) else {
return Response::error(400, "no path given");
};
let client = body
.get("client")
.and_then(Value::as_str)
.unwrap_or("unknown");
let session = body.get("session").and_then(Value::as_str).unwrap_or("");
if !state.ledger {
return Response::json(&json!({ "recorded": false, "reason": "not recording" }));
}
with_fleet(state, json!({ "recorded": false }), move |fleet| {
Ok(json!({ "recorded": crate::ledger::raw_read(fleet, client, session, path) }))
})
}
fn ledger(state: &Arc<State>) -> Response {
let empty = json!({
"recording": state.ledger,
"queries": 0,
"clients": 0,
"excerpt_tokens": 0,
"whole_file_tokens": 0,
"ratio": null,
"intact": true,
});
let recording = state.ledger;
with_fleet(state, empty, move |fleet| {
let (mut clients, mut excerpt, mut whole) = (0, 0, 0);
let mut intact = true;
let mut seen: std::collections::BTreeSet<String> = Default::default();
for (label, store) in fleet.each() {
let (_, c, e, w) = store::ledger_totals(store.db())?;
seen.extend(store::ledger_keys(
store.db(),
label,
store::LedgerScope::All,
)?);
clients = clients.max(c);
excerpt += e;
whole += w;
intact = intact && store::ledger_break(store.db())?.is_none();
}
let queries = seen.len() as i64;
let mut net = 0;
let mut measured = true;
let mut credited_keys: std::collections::BTreeSet<String> = Default::default();
let mut estimated_keys: std::collections::BTreeSet<String> = Default::default();
let mut by_client: std::collections::BTreeMap<String, i64> = Default::default();
let mut rows: Vec<Value> = Vec::new();
let mut sessions: Vec<Value> = Vec::new();
let mut refunds = 0;
let mut zero_hit = 0;
let mut refunds_measured = false;
for (label, store) in fleet.each() {
let savings = store::ledger_savings(store.db())?;
net += savings.net;
measured = measured && savings.measured;
let misses = store::ledger_misses(store.db())?;
refunds += misses.refunds;
zero_hit += misses.zero_hit;
refunds_measured = refunds_measured || misses.measured;
credited_keys.extend(store::ledger_keys(
store.db(),
label,
store::LedgerScope::Credited,
)?);
estimated_keys.extend(store::ledger_keys(
store.db(),
label,
store::LedgerScope::Estimated,
)?);
for (client, count) in store::ledger_clients(store.db())? {
*by_client.entry(client).or_default() += count;
}
for session in store::ledger_sessions(store.db(), LEDGER_SESSIONS)? {
sessions.push(json!({
"session": session.session,
"client": session.client,
"store": label,
"first": session.first,
"last": session.last,
"when": crate::clock::local_stamp(session.last),
"retrievals": session.retrievals,
"zero_hit": session.zero_hit,
"excerpt_tokens": session.excerpt_tokens,
"whole_file_tokens": session.whole_file_tokens,
"net_tokens": session.net,
"tier": session.tier(),
}));
}
for row in store::retrievals(store.db(), LEDGER_ROWS)? {
rows.push(json!({
"at": row.at,
"when": crate::clock::local_stamp(row.at),
"store": label,
"client": row.client,
"query": row.query,
"hits": row.hits,
"ms": row.micros / 1000,
"excerpt_tokens": row.excerpt_tokens,
"whole_file_tokens": row.whole_file_tokens,
"query_id": row.query_id,
}));
}
}
rows.sort_by_key(|row| std::cmp::Reverse(row["at"].as_i64().unwrap_or(0)));
rows.truncate(LEDGER_ROWS);
sessions.sort_by_key(|row| std::cmp::Reverse(row["last"].as_i64().unwrap_or(0)));
sessions.truncate(LEDGER_SESSIONS);
let credited = credited_keys.len() as i64;
let measured = measured && estimated_keys.is_empty();
let coverage = if queries == 0 {
0
} else {
credited * 100 / queries
};
let ratio = (excerpt > 0).then(|| whole as f64 / excerpt as f64);
Ok(json!({
"recording": recording,
"queries": queries,
"clients": clients,
"excerpt_tokens": excerpt,
"whole_file_tokens": whole,
"ratio": ratio,
"intact": intact,
"net_tokens": net,
"credited": credited,
"coverage": coverage,
"tier": if measured && credited > 0 { "measured" } else { "modelled" },
"refunds": refunds,
"refunds_measured": refunds_measured,
"zero_hit": zero_hit,
"by_client": by_client,
"rows": rows,
"sessions": sessions,
"legacy_rows": rows.iter().any(|row| {
row["query_id"].as_str().unwrap_or_default().is_empty()
}),
}))
})
}
const MODEL_NOTES: &[(&str, &str)] = &[
(
"BGEBaseENV15Q",
"Quantized v1.5 release of the base English model",
),
(
"GTEBaseENV15",
"Base English embedding model from Alibaba DAMO Academy",
),
(
"GTEBaseENV15Q",
"Quantized base English embedding model from Alibaba DAMO Academy",
),
(
"BGESmallENV15",
"Fast and small English model from BAAI. Not the one semlith builds a store with unless it is asked for",
),
];
fn models() -> Response {
let mut out = vec![
json!({
"name": embed::GRANITE_NAME,
"dim": 384,
"description": "default. IBM Granite R2 small, int8, English",
"code": embed::GRANITE_REPO,
}),
json!({
"name": crate::image::MODEL_NAME,
"dim": crate::image::DIM,
"description": "images. CLIP ViT-B/32, vision and text, fixed",
"code": "Qdrant/clip-ViT-B-32-vision",
}),
];
for info in TextEmbedding::list_supported_models() {
let name = info.model.to_string();
let description = MODEL_NOTES
.iter()
.find(|(model, _)| *model == name)
.map(|(_, note)| (*note).to_string())
.unwrap_or_else(|| info.description.clone());
out.push(json!({
"name": name,
"dim": info.dim,
"description": description,
"code": info.model_code,
}));
}
let cached = cached_model_sizes();
for model in &mut out {
let code = model.get("code").and_then(Value::as_str).unwrap_or("");
let key = code.rsplit('/').next().unwrap_or(code).to_ascii_lowercase();
if let Some(bytes) = cached.get(&key) {
model["bytes"] = json!(bytes);
}
}
Response::json(&json!({ "models": out }))
}
fn cached_model_sizes() -> std::collections::HashMap<String, u64> {
let mut out = std::collections::HashMap::new();
let Ok(entries) = crate::model_cache_dir().and_then(|dir| Ok(std::fs::read_dir(dir)?)) else {
return out;
};
for entry in entries.flatten() {
let Ok(kind) = entry.file_type() else {
continue;
};
if !kind.is_dir() {
continue;
}
let name = entry.file_name().to_string_lossy().to_ascii_lowercase();
let total = walk_bytes(&entry.path());
if total > 0 {
let key = name.rsplit("--").next().unwrap_or(&name).to_string();
*out.entry(key).or_insert(0) += total;
}
}
out
}
fn walk_bytes(dir: &Path) -> u64 {
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
entries
.flatten()
.map(|entry| match entry.file_type() {
Ok(kind) if kind.is_dir() => walk_bytes(&entry.path()),
Ok(_) => entry.metadata().map(|m| m.len()).unwrap_or(0),
Err(_) => 0,
})
.sum()
}
const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024;
fn image_file(state: &Arc<State>, request: &Request) -> Response {
let Some(want) = request.query("path") else {
return Response::error(400, "missing path");
};
let path = crate::canonical(Path::new(want));
let looked_up = path.to_string_lossy().into_owned();
if let Err(e) = state.open_fleet() {
return Response::error(500, &e.to_string());
}
let mut fleet = state.fleet.lock().unwrap_or_else(|e| e.into_inner());
let Some(fleet) = fleet.as_mut() else {
return Response::error(404, "no such image");
};
let indexed = fleet.each().any(|(_, store)| {
store
.db()
.query_row(
"SELECT 1 FROM images i JOIN files f ON f.id = i.file_id WHERE f.path = ?1",
rusqlite::params![&looked_up],
|_| Ok(()),
)
.is_ok()
});
if !indexed {
return Response::error(404, "no such image");
}
let Ok(file) = std::fs::File::open(&path) else {
return Response::error(404, "the file is no longer on disk");
};
let Ok(meta) = file.metadata() else {
return Response::error(404, "the file is no longer on disk");
};
if !meta.is_file() {
return Response::error(404, "no such image");
}
if meta.len() > MAX_IMAGE_BYTES {
return Response::error(413, "the image is larger than the portal will draw");
}
use std::io::Read;
let mut bytes = Vec::with_capacity(meta.len() as usize);
if file.take(MAX_IMAGE_BYTES).read_to_end(&mut bytes).is_err() {
return Response::error(404, "the file could not be read");
}
let path = path.as_path();
let kind = match path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.as_deref()
{
Some("png") => "image/png",
Some("jpg" | "jpeg") => "image/jpeg",
Some("webp") => "image/webp",
Some("gif") => "image/gif",
_ => return Response::error(404, "no such image"),
};
Response::new(200, kind, bytes)
}
fn languages() -> Response {
let out: Vec<Value> = LANGUAGES
.iter()
.map(|entry| {
json!({
"name": entry.name,
"extensions": entry.extensions,
"filenames": entry.filenames,
})
})
.collect();
Response::json(&json!({ "languages": out }))
}
fn dirs(request: &Request) -> Response {
let home = match crate::home::user_home() {
Ok(home) => crate::canonical(&home),
Err(e) => return Response::error(500, &e.to_string()),
};
let asked = request
.query("path")
.filter(|p| !p.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| home.clone());
let Ok(resolved) = std::fs::canonicalize(&asked) else {
return Response::error(400, "that path cannot be resolved");
};
if !resolved.starts_with(&home) {
return Response::error(400, "outside the home directory");
}
let mut entries = Vec::new();
let listing = match std::fs::read_dir(&resolved) {
Ok(l) => l,
Err(e) => return Response::error(400, &e.to_string()),
};
for entry in listing.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with('.') && name != crate::home::LOCAL_DIR {
continue;
}
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
let path = entry.path();
entries.push(json!({
"name": name,
"path": crate::plain(&path.display().to_string()),
"dir": is_dir,
"adoptable": is_dir && (is_store(&path) || is_store(&path.join(crate::home::LOCAL_DIR))),
}));
}
entries.sort_by(|a, b| {
let (ad, bd) = (a["dir"].as_bool(), b["dir"].as_bool());
bd.cmp(&ad).then_with(|| {
let (an, bn) = (
a["name"].as_str().unwrap_or(""),
b["name"].as_str().unwrap_or(""),
);
an.to_lowercase()
.cmp(&bn.to_lowercase())
.then_with(|| an.cmp(bn))
})
});
Response::json(&json!({
"path": crate::plain(&resolved.display().to_string()),
"home": crate::plain(&home.display().to_string()),
"parent": resolved
.parent()
.filter(|p| p.starts_with(&home))
.map(|p| crate::plain(&p.display().to_string())),
"entries": entries,
}))
}
fn is_store(dir: &Path) -> bool {
dir.join("store.db").exists()
}
fn privacy(state: &Arc<State>) -> Response {
let cache = crate::model_cache_dir().unwrap_or_default();
Response::json(&json!({
"bind": format!("127.0.0.1:{}", state.server.port()),
"bind_is_fixed": true,
"airgap": state.airgap,
"model_cache": crate::plain(&cache.display().to_string()),
"model_cached": cache.exists()
&& std::fs::read_dir(&cache).map(|mut d| d.next().is_some()).unwrap_or(false),
"token_header": crate::http::TOKEN_HEADER,
"token_preview": preview(&state.server.token()),
"key_grace_seconds": state.server.key_grace().map(|left| left.as_secs()),
"host_allowed": ["localhost", "127.0.0.1", "::1"],
"csp": "default-src 'self'",
"cors": false,
"store_home": shown(home::home_or_error()),
"rules": rules(state),
}))
}
fn rules(state: &Arc<State>) -> Value {
let registry = home::Registry::load().unwrap_or_default();
let measured = crate::doctor::privacy_findings(&open_stores(state));
let measured = |id: &str| {
measured
.iter()
.find(|finding| finding.id == id)
.expect("every measured rule is reported")
};
json!([
{
"id": "header-borne token",
"rule": format!(
"The session token travels in a {} header, never in a cookie. Every port on localhost is the same site, so a cookie would reach a page served by anything else on this machine.",
crate::http::TOKEN_HEADER
),
"check": "no route sets a cookie and none reads one",
"ok": true,
},
{
"id": "same-origin writes",
"rule": "Every request that is not a GET carries Sec-Fetch-Site: same-origin — or, from a client that sends no fetch metadata, an Origin that matches or none at all — and a JSON content type. Both are checked before the token, so a cross-origin page cannot tell a right guess from a wrong one.",
"check": "enforced in http::answer before any route runs",
"ok": true,
},
{
"id": "store trust",
"rule": "A store outside the store home is opened only after semlith trust has recorded it. A .semlith directory can arrive inside a repository.",
"check": if registry.trusted.is_empty() {
"no store outside the home is trusted".to_string()
} else {
format!("{} trusted outside the home", registry.trusted.len())
},
"ok": true,
},
{
"id": "index boundary",
"rule": "An agent holding the key indexes only under this store's registered roots or the home directory. The command line is not held to this — the person typing it owns the machine.",
"check": "enforced per path in Semlith::index_set",
"ok": true,
},
{
"id": "deny-list",
"rule": "No path under a credential directory, and no file named like a credential, is indexed by an agent or the portal — with the hidden-file rule applied to an explicitly named path too.",
"check": format!(
"{} directories and {} name patterns",
crate::filter::DENIED_DIRS.len(),
crate::filter::DENIED_NAMES.len()
),
"ok": true,
},
row(
"private addresses",
"semlith add resolves every hop and refuses an address that is not on the public internet: loopback, RFC 1918, link-local, carrier-grade NAT, unique local.",
measured("private addresses"),
),
{
"id": "pinned models",
"rule": "Every model file is fetched at a pinned commit and verified against a digest recorded in the source and in docs/models.md. The weights are what computes every vector in every store.",
"check": format!("granite at {}", &crate::embed::GRANITE_REVISION[..12]),
"ok": true,
},
row(
"model cache",
"Weights are not loaded from a cache another account owns or can write to.",
measured("model cache"),
),
row(
"directory modes",
"The store home, every store, the model cache and daemon.json are readable by their owner and nobody else.",
measured("directory modes"),
),
row(
"agent key",
"The key is in one file, readable by you alone. No registration semlith writes carries it and no command line shows it: a client launches `semlith mcp`, which reads the key from that file itself.",
measured("agent key"),
),
])
}
fn open_stores(state: &Arc<State>) -> Vec<(String, std::path::PathBuf)> {
state
.stores()
.iter()
.map(|s| (s.name.clone(), s.dir.clone()))
.collect()
}
fn row(id: &str, rule: &str, finding: &crate::doctor::Finding) -> Value {
json!({
"id": id,
"rule": rule,
"check": finding.check,
"ok": finding.ok,
"applicable": finding.applicable,
"manual": finding.manual,
"repair": finding.repair,
})
}
fn doctor(state: &Arc<State>) -> Response {
Response::json(&json!({
"clients": crate::doctor::clients_report(),
"rules": crate::doctor::privacy_findings(&open_stores(state)),
"unregisterable": crate::clients::UNREGISTERABLE,
}))
}
fn privacy_scan(state: &Arc<State>) -> Response {
if let Err(e) = state.open_fleet() {
return Response::error(500, &e.to_string());
}
let mut fleet = state.fleet.lock().unwrap_or_else(|e| e.into_inner());
let Some(fleet) = fleet.as_mut() else {
return Response::json(&json!({ "findings": [] }));
};
let mut findings = Vec::new();
for (label, opened) in fleet.each() {
match opened.scan() {
Ok(found) => {
for finding in found {
findings.push(json!({
"store": label,
"path": crate::plain(&finding.path),
"key": finding.path,
"why": finding.why,
}));
}
}
Err(e) => return Response::error(500, &e.to_string()),
}
}
Response::json(&json!({ "findings": findings }))
}
fn privacy_fix(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let stores = open_stores(state);
let applied: Vec<Value> = match body.get("rule").and_then(Value::as_str) {
Some(id) => {
let Some(repair) = crate::doctor::privacy_findings(&stores)
.into_iter()
.find(|finding| finding.id == id)
.and_then(|finding| finding.repair)
else {
return Response::error(400, &format!("{id} has no repair semlith can apply"));
};
match crate::doctor::apply(&repair, &stores) {
Ok(a) => vec![json!(a)],
Err(e) => return Response::error(400, &e.to_string()),
}
}
None => crate::doctor::apply_all(&stores)
.into_iter()
.map(|r| match r {
Ok(a) => json!(a),
Err(e) => json!({ "error": e.to_string() }),
})
.collect(),
};
Response::json(&json!({
"applied": applied,
"rules": rules(state),
}))
}
fn register_clients(state: &Arc<State>, request: &Request) -> Response {
let _ = state;
let body = request.json().unwrap_or(json!({}));
let writable: Vec<&crate::clients::Client> = crate::clients::clients()
.iter()
.filter(|client| client.needs_a_file_written())
.collect();
let plans = crate::clientfile::plan(&writable);
if body.get("confirm").and_then(Value::as_bool) != Some(true) {
return Response::json(&json!({ "plan": plans, "confirmed": false }));
}
let stanzas: Vec<(&str, &crate::clients::Stanza)> = writable
.iter()
.flat_map(|client| {
client
.config_files()
.map(move |stanza| (client.name.as_str(), stanza))
})
.collect();
match crate::clientfile::apply(&plans, &stanzas) {
Ok(written) => Response::json(&json!({
"plan": plans,
"confirmed": true,
"written": written,
"clients": crate::doctor::clients_report(),
})),
Err(e) => Response::error(500, &e.to_string()),
}
}
fn shown(path: anyhow::Result<PathBuf>) -> String {
match path {
Ok(p) => crate::plain(&p.display().to_string()),
Err(e) => format!("unresolved — {e}"),
}
}
fn preview(secret: &str) -> String {
let head: String = secret.chars().take(16).collect();
if head.len() < secret.len() {
format!("{head}…")
} else {
head
}
}
fn about(state: &Arc<State>) -> Response {
let binary = std::env::current_exe().unwrap_or_default();
Response::json(&json!({
"version": env!("CARGO_PKG_VERSION"),
"format_version": store::FORMAT_VERSION,
"binary": crate::plain(&binary.display().to_string()),
"binary_bytes": std::fs::metadata(&binary).map(|m| m.len()).unwrap_or(0),
"target": format!("{} · {}", std::env::consts::ARCH, std::env::consts::OS),
"bind": format!("127.0.0.1:{}", state.server.port()),
"revisions": crate::mcp::SUPPORTED,
"port": state.server.port(),
"pid": std::process::id(),
"uptime": daemon::uptime(state),
"store_home": shown(home::home_or_error()),
"model_cache": shown(crate::model_cache_dir()),
"models": TextEmbedding::list_supported_models().len() + 1,
"languages": LANGUAGES.len(),
"graph_languages": crate::graph::languages(),
"edge_kinds": crate::graph::KINDS,
"stores": state.stores().len(),
}))
}
fn reveal(state: &Arc<State>) -> Response {
Response::json(&json!({ "key": state.server.agent_key() }))
}
fn tool_list_tokens(state: &Arc<State>) -> (i64, &'static str) {
let text = crate::mcp::tool_list()
.into_iter()
.map(|(name, about)| format!("{name} {about}"))
.collect::<Vec<_>>()
.join(" ");
let guard = state.fleet.lock().ok();
let counter = guard
.as_ref()
.and_then(|f| f.as_ref())
.map(|f| f.counter())
.unwrap_or(crate::ledger::Counter::Chars4);
let count = counter.count(&text);
let bytes = crate::mcp::tool_list_bytes() as i64;
let prose = text.len().max(1) as i64;
(count * bytes / prose, counter.label())
}
fn agents(state: &Arc<State>) -> Response {
let connections = state.clients();
let key = state.server.agent_key();
Response::json(&json!({
"forwarding": state.proxy_count() > 0,
"connected": connections.len(),
"connections": connections,
"tools": crate::mcp::tool_list()
.into_iter()
.map(|(name, about)| json!({ "name": name, "about": about }))
.collect::<Vec<_>>(),
"tool_list_bytes": crate::mcp::tool_list_bytes(),
"tool_list_tokens": tool_list_tokens(state).0,
"tool_list_tier": tool_list_tokens(state).1,
"revisions": crate::mcp::SUPPORTED,
"clients": crate::clients::clients(),
"service": {
"status": crate::service::status(),
"last_started": crate::service::last_started(),
},
"doctor": crate::doctor::clients_report(),
"endpoint": {
"url": format!("http://127.0.0.1:{}{}", state.server.port(), crate::http::MCP_PATH),
"open": state.server.mcp_open(),
},
"key_env": crate::setup::KEY_ENV,
"key_set": home::is_agent_key(&key),
"key_path": shown(home::agent_key_path()),
"install": {
"sh": crate::setup::INSTALL_SH,
"ps1": crate::setup::INSTALL_PS1,
},
}))
}
fn setup() -> Response {
Response::json(&json!(crate::setup::status()))
}
fn fix_setup(request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
match body.get("step").and_then(Value::as_str) {
Some("path") => match crate::setup::run_path_step() {
Ok(step) => Response::json(&json!({ "step": step, "status": crate::setup::status() })),
Err(e) => Response::error(500, &e.to_string()),
},
Some(other) => Response::error(400, &format!("{other} is not a step this route runs")),
None => Response::error(400, "missing step"),
}
}
fn root(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let (Some(store), Some(path)) = (
body.get("store").and_then(Value::as_str),
body.get("root").and_then(Value::as_str),
) else {
return Response::error(400, "missing store or root");
};
match home::repoint(store, Path::new(path)) {
Ok(()) => Response::json(&json!({
"store": store,
"root": path,
"message": format!(
"{store} now covers {path}. It is watched from the next start of \
the daemon; nothing was re-embedded."
),
"restart": state.stores().iter().any(|s| s.name == store),
})),
Err(e) => Response::error(409, &e.to_string()),
}
}
fn with_fleet(
state: &Arc<State>,
empty: Value,
read: impl FnOnce(&crate::fleet::Fleet) -> anyhow::Result<Value>,
) -> Response {
if let Err(e) = state.open_fleet() {
return Response::error(500, &e.to_string());
}
let fleet = state.fleet.lock().unwrap_or_else(|e| e.into_inner());
let Some(fleet) = fleet.as_ref() else {
return Response::json(&empty);
};
match read(fleet) {
Ok(value) => Response::json(&value),
Err(e) => Response::error(500, &e.to_string()),
}
}
fn pattern(state: &Arc<State>, request: &Request) -> Response {
let Some(query) = request.query("query").filter(|q| !q.trim().is_empty()) else {
return Response::error(400, "missing query");
};
let Some(lang) = request.query("lang").filter(|l| !l.trim().is_empty()) else {
return Response::error(400, "missing lang");
};
let only = request.query_all("store");
let filter = match filter_of(request) {
Ok(f) => f,
Err(e) => return Response::error(400, &e),
};
let offset = request
.query("offset")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
let empty = json!({ "language": lang, "matches": [], "files": 0, "truncated": false });
with_fleet(state, empty, move |fleet| {
let only = (!only.is_empty()).then_some(only);
match fleet.pattern_in(only.as_deref(), lang, query, &filter, offset) {
Ok(found) => Ok(serde_json::to_value(found)?),
Err(e) => Ok(json!({
"language": lang,
"matches": [],
"files": 0,
"truncated": false,
"error": e.to_string(),
})),
}
})
}
fn read(state: &Arc<State>, request: &Request) -> Response {
let Some(raw) = request.query("target").filter(|t| !t.trim().is_empty()) else {
return Response::error(400, "missing target");
};
let target = crate::Target::parse(raw);
let only = request.query_all("store");
with_fleet(state, json!({ "span": null }), move |fleet| {
let only = (!only.is_empty()).then_some(only);
match fleet.read_in(only.as_deref(), &target, &crate::filter::Filter::default())? {
None => Ok(json!({ "span": null, "definitions": [] })),
Some(crate::Read::One(span)) => Ok(json!({ "span": span, "definitions": [] })),
Some(crate::Read::Choose(rows)) => Ok(json!({ "span": null, "definitions": rows })),
}
})
}
fn symbol(state: &Arc<State>, request: &Request) -> Response {
let Some(name) = request.query("name").filter(|n| !n.trim().is_empty()) else {
return Response::error(400, "missing name");
};
let name = name.to_string();
let k = request
.query("k")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(20)
.clamp(1, 200);
let only = request.query_all("store");
with_fleet(state, json!({ "symbols": [] }), move |fleet| {
let only = (!only.is_empty()).then_some(only);
let found = fleet.evidence_in(
only.as_deref(),
&name,
&crate::graph::dependency_kinds(),
k,
false,
)?;
Ok(json!({
"symbols": found.definitions,
"callers": found.callers,
"callees": found.callees,
"ego": found.ego,
}))
})
}
fn neighbors(state: &Arc<State>, request: &Request) -> Response {
let Some(name) = request.query("name").filter(|n| !n.trim().is_empty()) else {
return Response::error(400, "missing name");
};
let name = name.to_string();
let kinds = request.query_all("kind");
if let Some(bad) = kinds
.iter()
.find(|k| !crate::graph::KINDS.contains(&k.as_str()))
{
return Response::error(400, &format!("unknown edge kind {bad:?}"));
}
let only = request.query_all("store");
let all = request
.query("all")
.is_some_and(|v| v == "1" || v == "true");
let empty = json!({ "callers": [], "callees": [] });
with_fleet(state, empty, move |fleet| {
let only = (!only.is_empty()).then_some(only);
Ok(json!(fleet.neighbours_in(
only.as_deref(),
&name,
&kinds,
all
)?))
})
}
fn shortest_path(state: &Arc<State>, request: &Request) -> Response {
let (Some(from), Some(to)) = (request.query("from"), request.query("to")) else {
return Response::error(400, "missing from or to");
};
let (from, to) = (from.to_string(), to.to_string());
let depth = request
.query("depth")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(6)
.clamp(1, 20);
let only = request.query_all("store");
let all_edges = request
.query("all_edges")
.is_some_and(|v| v == "1" || v == "true");
with_fleet(state, json!({ "path": null }), move |fleet| {
let only = (!only.is_empty()).then_some(only);
let chain = fleet.path_in(only.as_deref(), &from, &to, depth, all_edges)?;
Ok(json!({
"path": chain.as_ref().map(|c| &c.steps),
"summary": chain.as_ref().map(|c| &c.summary),
"all_edges": all_edges,
}))
})
}
fn impact(state: &Arc<State>, request: &Request) -> Response {
let Some(name) = request.query("name") else {
return Response::error(400, "missing name");
};
let name = name.to_string();
let kinds = request.query_all("kind");
let depth = request
.query("depth")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(3)
.clamp(1, 10);
let only = request.query_all("store");
let all_edges = request
.query("all_edges")
.is_some_and(|v| v == "1" || v == "true");
with_fleet(state, json!({ "impact": null }), move |fleet| {
let only = (!only.is_empty()).then_some(only);
let impact = fleet.impact_in(only.as_deref(), &name, &kinds, depth, all_edges)?;
Ok(json!({
"impact": impact,
"headline": impact.headline(),
"limit": crate::graph::IMPACT_LIMIT,
}))
})
}
fn trace(state: &Arc<State>, request: &Request) -> Response {
let (Some(from), Some(to)) = (request.query("from"), request.query("to")) else {
return Response::error(400, "missing from or to");
};
let (from, to) = (from.to_string(), to.to_string());
let depth = request
.query("depth")
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(6)
.clamp(1, 20);
let only = request.query_all("store");
let all_edges = request
.query("all_edges")
.is_some_and(|v| v == "1" || v == "true");
with_fleet(state, json!({ "trace": null }), move |fleet| {
let only = (!only.is_empty()).then_some(only);
let trace = fleet.trace_in(only.as_deref(), &from, &to, depth, all_edges, &crate::plain)?;
Ok(json!({
"trace": trace,
"evidence": trace.evidence(&crate::plain),
}))
})
}
fn map(state: &Arc<State>, request: &Request) -> Response {
let only = request.query_all("store");
let shown = request
.query("shown")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(crate::graph::COMMUNITIES_SHOWN)
.clamp(1, 50);
with_fleet(state, json!({ "communities": [] }), move |fleet| {
let only = (!only.is_empty()).then_some(only);
let (communities, total, edges) = fleet.communities_in(only.as_deref(), shown)?;
Ok(json!({
"communities": communities,
"shown": communities.len(),
"total": total,
"edges": edges,
}))
})
}
fn replay(request: &Request) -> Response {
if request.method == "POST" {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let Some(on) = body.get("on").and_then(Value::as_bool) else {
return Response::error(400, "missing on");
};
let mut saved = home::Settings::load();
saved.session_replay = Some(on);
if let Err(e) = saved.save() {
return Response::error(500, &e.to_string());
}
return Response::json(&json!({ "enabled": on }));
}
let enabled = home::Settings::load().session_replay.unwrap_or(false);
let dir = crate::replay::transcripts_dir().ok();
if !enabled {
return Response::json(&json!({
"enabled": false,
"client": crate::replay::CLIENT,
"from": dir.map(|d| crate::plain(&d.display().to_string())),
"sessions": [],
}));
}
let Some(dir) = dir else {
return Response::error(
500,
"this machine has no home directory to read transcripts from",
);
};
match crate::replay::read(&dir, crate::replay::FILES) {
Ok(found) => Response::json(&json!({
"enabled": true,
"client": found.client,
"from": found.from,
"skipped": found.skipped,
"sessions": found.sessions,
})),
Err(e) => Response::error(500, &e.to_string()),
}
}
fn report(state: &Arc<State>, request: &Request) -> Response {
let Some(kind) = request.query("kind") else {
return Response::error(400, "missing kind");
};
let kind = kind.to_string();
let format = request.query("format").unwrap_or("markdown").to_string();
let model = request.query("model").unwrap_or("Sonnet 5").to_string();
let only = request.query_all("store");
with_fleet(state, json!({ "report": null }), move |fleet| {
let _ = &only;
let report = crate::report::generate(fleet, &kind, &model)?;
Ok(json!({
"kind": report.kind,
"title": report.title,
"generated": report.generated,
"format": format,
"text": report.render(&format)?,
"report": report,
}))
})
}
fn graph(state: &Arc<State>, request: &Request) -> Response {
let focus = request.query("name").map(str::to_string);
let prefix = request.query("path").map(str::to_string);
let limit = request
.query("limit")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(70)
.clamp(1, crate::graph::MAX_NODES);
let only = request.query_all("store");
let empty = json!({ "nodes": [], "edges": [], "total": 0, "shown": 0 });
with_fleet(state, empty, move |fleet| {
let only = (!only.is_empty()).then_some(only);
let chosen: Vec<(&str, &crate::Semlith)> = match &only {
Some(names) => fleet
.each()
.filter(|(label, _)| names.iter().any(|n| n == label))
.collect(),
None => fleet.each().collect(),
};
let many = fleet.len() > 1;
crate::graph::scoped(&chosen, focus.as_deref(), prefix.as_deref(), limit, many)
})
}
fn index(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let paths: Vec<PathBuf> = strings(&body, "path")
.into_iter()
.map(PathBuf::from)
.collect();
if paths.is_empty() {
return Response::error(400, "no path given");
}
let (paths, unreadable) = crate::check_roots(&paths);
if !unreadable.is_empty() {
let named = unreadable
.iter()
.map(|(path, why)| format!("{}: {why}", path.display()))
.collect::<Vec<_>>()
.join("; ");
return Response::error(400, &format!("cannot index {named}"));
}
let asked = body.get("store").and_then(Value::as_str);
let each = match asked {
Some("each") => true,
Some(_) => false,
None => true,
};
if each {
let mut started = Vec::new();
for path in &paths {
let store = match store_for(state, path) {
Ok(store) => store,
Err(e) => return Response::error(409, &format!("{}: {e:#}", path.display())),
};
match state.index(&store, vec![path.clone()]) {
Ok((run, _progress)) => started.push(json!({
"run": run,
"store": store.name,
"path": crate::plain(&path.display().to_string()),
})),
Err(e) => started.push(json!({
"store": store.name,
"path": crate::plain(&path.display().to_string()),
"error": e.to_string(),
})),
}
}
return Response::json(&json!({ "runs": started, "target": "each" }));
}
let store = match state.writable(asked) {
Ok(s) => s,
Err(e) if asked.is_none() && state.stores().is_empty() => {
match store_for(state, &paths[0]) {
Ok(store) => store,
Err(made) => return Response::error(409, &format!("{e}: {made:#}")),
}
}
Err(e) => return Response::error(409, &e.to_string()),
};
let roots = home::index_roots(&store.dir);
let outside: Vec<String> = paths
.iter()
.filter(|path| !crate::filter::within_boundary(path, &roots))
.map(|path| crate::plain(&path.display().to_string()))
.collect();
if !outside.is_empty() {
return Response::error(
403,
&format!(
"outside the boundary for store {}: {}. A path must be under one of that \
store's registered roots, or under the store's own directory when it is a \
`.semlith` beside its corpus. Add it as a root first, or post no store and \
let the path have one of its own.",
store.name,
outside.join(", ")
),
);
}
adopt_roots(&store, &paths);
match state.index(&store, paths) {
Ok((run, _progress)) => Response::json(&json!({
"runs": [{ "run": run, "store": store.name }],
"target": "store",
})),
Err(e) => Response::error(409, &e.to_string()),
}
}
fn index_runs(state: &Arc<State>) -> Response {
let admission = &state.admission;
let runs: Vec<Value> = state
.stores()
.iter()
.flat_map(|store| store.run_snapshots(admission.position_of(&store.name)))
.collect();
let queue: Vec<Value> = admission
.waiting()
.into_iter()
.enumerate()
.map(|(at, (run, store, paths))| {
json!({
"run": run,
"store": store,
"position": at + 1,
"paths": paths.iter()
.map(|p| crate::plain(&p.display().to_string()))
.collect::<Vec<_>>(),
})
})
.collect();
let limits = daemon::Limits::in_force();
Response::json(&json!({
"runs": runs,
"queue": queue,
"running": admission.running(),
"limits": limits,
}))
}
fn index_log(state: &Arc<State>, request: &Request) -> Response {
let Some(name) = request.query("store") else {
return Response::error(400, "missing store");
};
let Some(store) = state.store(name) else {
return Response::error(404, &format!("no store called {name} is open"));
};
let after = request.query("after").and_then(|v| v.parse::<u64>().ok());
let run = request.query("run").and_then(|v| v.parse::<u64>().ok());
let lines = store.log_after(run, after);
let last = lines
.last()
.and_then(|line| line.get("seq"))
.and_then(Value::as_u64)
.or(after);
Response::json(&json!({ "store": name, "run": run, "lines": lines, "cursor": last }))
}
fn projects(request: &Request) -> Response {
let home = match crate::home::user_home() {
Ok(home) => crate::canonical(&home),
Err(e) => return Response::error(500, &e.to_string()),
};
let asked = request
.query("path")
.filter(|p| !p.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| home.clone());
let Ok(resolved) = std::fs::canonicalize(&asked) else {
return Response::error(400, "that path cannot be resolved");
};
if !resolved.starts_with(&home) {
return Response::error(400, "outside the home directory");
}
let (found, repositories) = match home::projects_under(&resolved) {
Ok(found) => found,
Err(e) => return Response::error(400, &format!("{e:#}")),
};
let registry = home::Registry::load().unwrap_or_default();
let rows: Vec<Value> = found
.iter()
.map(|path| {
json!({
"name": path.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default(),
"path": crate::plain(&path.display().to_string()),
"repository": repositories,
"indexed": registry.covering(path).map(|(name, _)| name.to_string()),
})
})
.collect();
Response::json(&json!({
"path": crate::plain(&resolved.display().to_string()),
"home": crate::plain(&home.display().to_string()),
"parent": resolved
.parent()
.filter(|p| p.starts_with(&home))
.map(|p| crate::plain(&p.display().to_string())),
"projects": rows,
"folders": folders(&resolved),
"repositories": repositories,
}))
}
fn folders(dir: &Path) -> Vec<Value> {
let Ok(listing) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut out: Vec<Value> = listing
.flatten()
.filter(|entry| {
entry.file_type().map(|t| t.is_dir()).unwrap_or(false)
&& !entry.file_name().to_string_lossy().starts_with('.')
})
.map(|entry| {
json!({
"name": entry.file_name().to_string_lossy().into_owned(),
"path": crate::plain(&entry.path().display().to_string()),
})
})
.collect();
out.sort_by_key(|row| row["name"].as_str().unwrap_or("").to_lowercase());
out
}
fn changes(state: &Arc<State>) -> Response {
let _ = state;
daemon::changes::notice_registry();
let mut out = serde_json::Map::new();
for domain in daemon::changes::DOMAINS {
out.insert(
domain.as_str().to_string(),
json!(daemon::changes::read(domain)),
);
}
Response::json(&Value::Object(out))
}
fn index_settings(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let mut saved = home::Settings::load();
let limits = daemon::Limits::in_force();
let read = |key: &str| {
body.get(key)
.and_then(Value::as_u64)
.map(|n| n.max(1) as usize)
};
if let Some(n) = read("runs_at_once") {
if limits.runs_at_once.source == daemon::Source::Environment {
return Response::error(
409,
&format!(
"{} is set in this daemon's environment, so the page cannot change it",
daemon::PARALLEL_ENV
),
);
}
saved.runs_at_once = Some(n);
}
if let Some(n) = read("embed_threads") {
if limits.embed_threads.source == daemon::Source::Environment {
return Response::error(
409,
&format!(
"{} is set in this daemon's environment, so the page cannot change it",
crate::embed::THREADS_ENV
),
);
}
saved.embed_threads = Some(n);
}
if let Some(n) = read("index_memory_mb") {
if limits.index_memory_mb.source == daemon::Source::Environment {
return Response::error(
409,
&format!(
"{} is set in this daemon's environment, so the page cannot change it",
crate::index::INDEX_MEMORY_ENV
),
);
}
saved.index_memory_mb = Some(n);
}
if let Err(e) = saved.save() {
return Response::error(500, &format!("{e:#}"));
}
let limits = daemon::Limits::in_force();
state.admission.set_limit(limits.runs_at_once.value);
Response::json(&json!({ "limits": limits }))
}
fn index_control(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let store = match state.writable(body.get("store").and_then(Value::as_str)) {
Ok(s) => s,
Err(e) => return Response::error(409, &e.to_string()),
};
let mut dequeued = 0;
let mut removed = 0;
let run = body.get("run").and_then(Value::as_u64);
match body.get("action").and_then(Value::as_str) {
Some("pause") => store
.paused
.store(true, std::sync::atomic::Ordering::Relaxed),
Some("resume") => store
.paused
.store(false, std::sync::atomic::Ordering::Relaxed),
Some("stop") => {
if !store.run_live() && state.admission.position_of(&store.name).is_none() {
return Response::error(
409,
&format!(
"{} has no run to stop — the last one has already finished. \
Remove its card instead.",
store.name
),
);
}
dequeued += state.admission.dequeue(&store.name);
let cancelled = store.cancel_queued();
dequeued += cancelled.len();
for run in cancelled {
state.admission.finish(run);
}
store
.cancelled
.store(true, std::sync::atomic::Ordering::Relaxed);
store
.paused
.store(false, std::sync::atomic::Ordering::Relaxed);
store.mark_stopping();
}
Some("dequeue") => {
dequeued += state.admission.dequeue(&store.name);
if dequeued == 0 {
return Response::error(
409,
&format!("{} has nothing waiting in the queue", store.name),
);
}
}
Some("remove") => {
let Some(id) = run else {
return Response::error(400, "remove needs the run to remove");
};
if !store.remove_run(id) {
return Response::error(
409,
&format!("run {id} is not a finished run of {}", store.name),
);
}
removed += 1;
}
Some("clear") => removed += store.clear_finished_runs(),
_ => {
return Response::error(
400,
"action must be \"pause\", \"resume\", \"stop\", \"dequeue\", \"remove\" \
or \"clear\"",
);
}
}
Response::json(&json!({
"store": store.name,
"paused": store.paused.load(std::sync::atomic::Ordering::Relaxed),
"stopping": store.cancelled.load(std::sync::atomic::Ordering::Relaxed),
"dequeued": dequeued,
"removed": removed,
}))
}
fn adopt_roots(store: &Arc<Store>, paths: &[PathBuf]) {
let registry = match home::Registry::load() {
Ok(r) => r,
Err(_) => return,
};
let Some(name) = registry.name_of(&store.dir) else {
return;
};
let choice = home::Choice::Registered {
name: name.to_string(),
dir: store.dir.clone(),
};
let model = crate::Semlith::open(&store.dir, None)
.map(|opened| opened.model().to_string())
.unwrap_or_default();
let _ = home::record(&choice, paths, &model);
}
fn store_for(state: &Arc<State>, path: &Path) -> Result<Arc<Store>, anyhow::Error> {
let choice = home::resolve(&[], path, None)?;
let dir = choice.one()?;
{
let store = crate::Semlith::open(&dir, None)?;
let model = store.model().to_string();
home::record(&choice, std::slice::from_ref(&path.to_path_buf()), &model)?;
}
state.open_store(&dir, true)
}
fn add(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let Some(url) = body
.get("url")
.and_then(Value::as_str)
.filter(|u| !u.is_empty())
else {
return Response::error(400, "no url given");
};
let store = match state.writable(body.get("store").and_then(Value::as_str)) {
Ok(s) => s,
Err(e) => return Response::error(409, &e.to_string()),
};
let fetched = match crate::add::fetch(url, &store.dir) {
Ok(fetched) => fetched,
Err(e) => return Response::error(400, &format!("{e:#}")),
};
match state.index(&store, vec![fetched.path.clone()]) {
Ok((run, _progress)) => Response::json(&json!({
"runs": [{ "run": run, "store": store.name }],
"target": "store",
"fetched": crate::plain(&fetched.path.display().to_string()),
"url": fetched.url,
})),
Err(e) => Response::error(409, &e.to_string()),
}
}
fn upgrade(request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
match body.get("action").and_then(Value::as_str) {
Some("check") => match crate::upgrade::check() {
Ok(found) => Response::json(&json!(found)),
Err(e) => Response::error(502, &e.to_string()),
},
Some("apply") => match crate::upgrade::apply(
body.get("version")
.and_then(Value::as_str)
.map(String::from),
) {
Ok(()) => Response::json(&json!({
"installed": true,
"restart": "semlith start is still running the old binary — restart it",
})),
Err(e) => Response::error(409, &e.to_string()),
},
_ => Response::error(400, "action must be \"check\" or \"apply\""),
}
}
fn forget(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let paths: Vec<String> = match (
body.get("path").and_then(Value::as_str),
body.get("paths").and_then(Value::as_array),
) {
(Some(one), _) => vec![one.to_string()],
(None, Some(many)) => many
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect(),
(None, None) => return Response::error(400, "no path given"),
};
if paths.is_empty() {
return Response::error(400, "no path given");
}
let store = match state.writable(body.get("store").and_then(Value::as_str)) {
Ok(s) => s,
Err(e) => return Response::error(409, &e.to_string()),
};
let single = paths.len() == 1;
let mut forgot = 0_i64;
let mut images = 0_i64;
let mut missing: Vec<String> = Vec::new();
for path in &paths {
let progress = match state.forget(&store, PathBuf::from(path)) {
Ok(p) => p,
Err(e) => return Response::error(409, &e.to_string()),
};
let value = match progress.recv() {
Ok(v) => v,
Err(_) => return Response::error(500, "the writer stopped before answering"),
};
let chunks = value.get("forgot").and_then(Value::as_i64).unwrap_or(0);
let pictures = value.get("images").and_then(Value::as_i64).unwrap_or(0);
if single {
if chunks == 0 && pictures == 0 {
return Response::error(404, &format!("{path} is not indexed"));
}
return Response::json(&value);
}
if chunks == 0 && pictures == 0 {
missing.push(path.clone());
}
forgot += chunks;
images += pictures;
}
let kept = paths.len() - missing.len();
let body = json!({
"files": kept,
"asked": paths.len(),
"forgot": forgot,
"images": images,
"not_indexed": missing,
"message": format!(
"{kept} file{} forgotten, {forgot} chunk{} removed.",
if kept == 1 { "" } else { "s" },
if forgot == 1 { "" } else { "s" },
),
});
if kept == 0 {
return Response::new(404, "application/json", body.to_string().into_bytes());
}
Response::json(&body)
}
fn delete_store(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let Some(name) = body.get("store").and_then(Value::as_str) else {
return Response::error(400, "no store given");
};
match state.delete_store(name) {
Ok(dir) => Response::json(&json!({
"store": name,
"deleted": crate::plain(&dir.display().to_string()),
"message": format!(
"{name} is gone: its vectors, chunks, graph and ledger were deleted and the \
registry no longer lists it. The files it indexed are untouched."
),
})),
Err(e) => Response::error(409, &e.to_string()),
}
}
fn trust(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let Some(dir) = body.get("path").and_then(Value::as_str) else {
return Response::error(400, "no path given");
};
let mut registry = match home::Registry::load() {
Ok(r) => r,
Err(e) => return Response::error(500, &e.to_string()),
};
match registry.trust(Path::new(dir)) {
Ok(dir) => Response::json(&json!({
"dir": crate::plain(&dir.display().to_string()),
"trusted": registry.trusted.iter().map(|d| crate::plain(&d.display().to_string())).collect::<Vec<_>>(),
"restart_required": !state.stores().iter().any(|s| s.dir == dir),
})),
Err(e) => Response::error(400, &e.to_string()),
}
}
fn adopt(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let Some(dir) = body.get("path").and_then(Value::as_str) else {
return Response::error(400, "no path given");
};
let dir = PathBuf::from(dir);
let dir = if !is_store(&dir) && is_store(&dir.join(crate::home::LOCAL_DIR)) {
dir.join(crate::home::LOCAL_DIR)
} else {
dir
};
if state
.stores()
.iter()
.any(|s| s.dir == crate::canonical(&dir))
{
return Response::error(409, "this daemon is already using that store");
}
match home::adopt(&dir, None, None) {
Ok((name, target)) => Response::json(&json!({
"name": name,
"dir": crate::plain(&target.display().to_string()),
"restart_required": true,
})),
Err(e) => Response::error(400, &e.to_string()),
}
}
fn mcp(state: &Arc<State>, request: &Request) -> Response {
let proxy = crate::proxy::proxy_pid(request.header("semlith-proxy"));
if let Some(pid) = proxy {
state.saw_proxy(pid);
}
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let transport = match proxy {
Some(_) => "stdio proxy",
None if request.path == crate::http::MCP_PATH => "http /mcp",
None => "portal",
};
let method = body.get("method").and_then(Value::as_str).unwrap_or("");
let params = body.get("params");
let name = params
.and_then(|p| p.get("clientInfo"))
.and_then(|c| c.get("name"))
.and_then(Value::as_str);
let revision = params
.and_then(|p| p.get("protocolVersion"))
.and_then(Value::as_str);
let session = request
.header("mcp-session-id")
.filter(|id| is_session_id(id))
.map(str::to_string)
.or_else(|| proxy.map(|pid| pid.to_string()))
.unwrap_or_else(|| {
if method == "initialize" {
new_session()
} else {
String::from("anonymous")
}
});
state.note_client(&session, transport, name, revision, method == "tools/call");
let corpus_free = matches!(method, "initialize" | "tools/list" | "ping")
|| method.starts_with("notifications/");
if let Err(e) = state.open_mcp_fleet()
&& !corpus_free
{
return Response::error(409, &e.to_string());
}
let writer = daemon::Writer(Arc::clone(state));
let mut fleet = state.mcp_fleet.lock().unwrap_or_else(|e| e.into_inner());
let mut nothing = crate::fleet::Fleet::empty();
let fleet = match fleet.as_mut() {
Some(fleet) => fleet,
None if corpus_free => &mut nothing,
None => return Response::error(409, "this daemon has no store open"),
};
let mut mcp_session = crate::mcp::Session::new(session.clone());
if let Some(named) = state.client_name(&session, transport) {
mcp_session.client = named;
}
let response = match crate::mcp::answer(fleet, Some(&writer), &body, &mut mcp_session) {
Some(value) => Response::json(&value),
None => Response::new(200, "application/json; charset=utf-8", Vec::new()),
};
if method == "initialize" {
return response.header("Mcp-Session-Id", session);
}
response
}
fn is_session_id(value: &str) -> bool {
value.len() == 16 && value.bytes().all(|b| b.is_ascii_hexdigit())
}
fn new_session() -> String {
let mut bytes = [0u8; 8];
getrandom::fill(&mut bytes).expect("the OS random source");
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn key(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let now = body.get("now").and_then(Value::as_bool).unwrap_or(false);
let previous = crate::home::agent_key().unwrap_or_default();
let fresh = match body.get("key").and_then(Value::as_str) {
Some(key) if crate::home::is_agent_key(key) => key.to_string(),
Some(_) => return Response::error(400, "that is not an agent key"),
None => match crate::home::rotate_agent_key() {
Ok(key) => key,
Err(e) => return Response::error(500, &e.to_string()),
},
};
state.server.rotate_agent(&fresh, now);
let updated: Vec<String> = crate::setup::recarry_key(&previous, &fresh)
.iter()
.map(|p| crate::plain(&p.display().to_string()))
.collect();
Response::json(&json!({
"key": fresh,
"previous_valid": !now,
"updated": updated,
"stanzas": crate::clients::http_stanzas(&fresh),
}))
}
fn endpoint(state: &Arc<State>, request: &Request) -> Response {
let body = match request.json() {
Ok(b) => b,
Err(e) => return Response::error(400, &e.to_string()),
};
let Some(open) = body.get("open").and_then(Value::as_bool) else {
return Response::error(400, "missing open");
};
state.server.set_mcp_open(open);
Response::json(&json!({
"open": open,
"url": format!("http://127.0.0.1:{}{}", state.server.port(), crate::http::MCP_PATH),
}))
}
fn rotate(state: &Arc<State>) -> Response {
let fresh = state.rotate();
Response::json(&json!({ "token": fresh, "url": daemon::url(state) }))
}
fn filter_of(request: &Request) -> Result<Filter, String> {
Filter::new(
&request.query_all("path"),
&request.query_all("ext"),
&request.query_all("lang"),
)
.map_err(|e| e.to_string())
}
fn strings(value: &Value, key: &str) -> Vec<String> {
match value.get(key) {
Some(Value::String(s)) => vec![s.clone()],
Some(Value::Array(items)) => items
.iter()
.filter_map(|i| i.as_str().map(str::to_string))
.collect(),
_ => Vec::new(),
}
}
fn language_of(path: &Path) -> &'static str {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
LANGUAGES
.iter()
.find(|entry| {
(!ext.is_empty() && entry.extensions.contains(&ext.as_str()))
|| entry
.filenames
.iter()
.any(|pattern| matches(pattern, &name))
})
.map(|entry| entry.name)
.unwrap_or("")
}
fn matches(pattern: &str, name: &str) -> bool {
match pattern.strip_suffix('*') {
Some(prefix) => name.starts_with(prefix),
None => name == pattern,
}
}
#[allow(dead_code)]
fn named(state: &Arc<State>, name: &str) -> Option<Arc<Store>> {
state.store(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_file_is_labelled_with_the_reader_that_would_parse_it() {
assert_eq!(chunk::reader_of(Path::new("a/b.docx")), "word");
assert_eq!(chunk::reader_of(Path::new("a/b.ipynb")), "notebook");
assert_eq!(chunk::reader_of(Path::new("a/b.rs")), "text");
assert_eq!(chunk::reader_of(Path::new("a/b.PDF")), "pdf");
}
#[test]
fn a_file_is_labelled_with_the_language_lang_would_select() {
assert_eq!(language_of(Path::new("src/lib.rs")), "rust");
assert_eq!(language_of(Path::new("a/b.MD")), "markdown");
assert_eq!(language_of(Path::new("a/b.unknown")), "");
assert_eq!(language_of(Path::new("LICENSE")), "");
}
#[test]
fn a_path_is_taken_as_a_string_or_an_array() {
assert_eq!(strings(&json!({ "path": "a" }), "path"), vec!["a"]);
assert_eq!(
strings(&json!({ "path": ["a", "b"] }), "path"),
vec!["a", "b"]
);
assert!(strings(&json!({}), "path").is_empty());
}
#[test]
fn every_path_this_file_hands_out_is_plain() {
let source = include_str!("routes.rs");
let raw: Vec<(usize, &str)> = source
.lines()
.enumerate()
.filter(|(_, line)| line.contains(&format!("display(){}", ".to_string()")))
.filter(|(_, line)| !line.contains("crate::plain("))
.filter(|(_, line)| !line.trim_start().starts_with("///"))
.map(|(n, line)| (n + 1, line.trim()))
.collect();
assert!(
raw.is_empty(),
"these paths go out as JSON without being made plain, so on Windows they carry \
the verbatim prefix: {raw:#?}"
);
}
#[test]
fn every_corrected_model_note_names_a_model_that_is_listed() {
let listed: Vec<String> = TextEmbedding::list_supported_models()
.into_iter()
.map(|info| info.model.to_string())
.collect();
for (name, _) in MODEL_NOTES {
assert!(
listed.iter().any(|model| model == name),
"{name} is corrected here and is not in fastembed's catalogue any more"
);
}
}
}