Fleet: per-node dev-tool version cards + nightly latest-check (Phase 1, read-only)
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 7s
ci / frontend (push) Successful in 23s
ci / e2e (push) Has been skipped

Each node card now shows installed versions of Docker / Claude Code / Kimi / GLM /
Ollama (conditional per node) under the ssh card, with an "update available" badge.

- daemon: probe_tools() finds docker/claude/kimi-cli/ollama across candidate bin dirs,
  extracts semver from --version, reports {"t":"node_tools",...} on connect + every 15m.
- migration node_tools + tool_latest; cm-db repo node_tools (upsert/list/latest).
- cm-api: fleet.rs NodeTools uplink → upsert; tool_versions.rs spawn_latest_checker
  (24h, npm/pypi/github; docker display-only); GET /api/nodes/{id}/tools (glm mirrors
  claude). Spawned in clawmates-server.
- frontend: NodeTools cards on each HostCard with the ↑latest badge.

Phase 2 (one-click update execution) intentionally deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-27 06:01:51 -07:00
co-authored by Claude Opus 4.8
parent 69c64ad676
commit 9263418fcb
10 changed files with 322 additions and 0 deletions
+45
View File
@@ -174,6 +174,51 @@ pub async fn agent_ws(
upgrade.on_upgrade(move |socket| run_channel(pool, hub, node_id, socket))
}
/// `GET /api/nodes/{id}/tools` — installed dev-tool versions for a node, the
/// latest upstream version, and an update flag (Phase 1: read-only). `glm` is
/// derived from `claude` (it runs Claude Code with a z.ai config).
pub async fn tools(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
) -> Result<Json<Value>, ApiError> {
let node_id = NodeId::from(id);
nodes::get(&state.pool, node_id, user.workspace_id)
.await?
.ok_or(ApiError::NotFound)?;
let installed: std::collections::HashMap<String, String> =
cm_db::repo::node_tools::list(&state.pool, node_id)
.await
.unwrap_or_default()
.into_iter()
.collect();
let latest = cm_db::repo::node_tools::all_latest(&state.pool)
.await
.unwrap_or_default();
// (probe key, display name, ui key) in display order; `glm` mirrors `claude`.
let order = [
("docker", "Docker", "docker"),
("claude", "Claude Code", "claude"),
("kimi-cli", "Kimi", "kimi"),
("glm", "GLM (Claude Code)", "glm"),
("ollama", "Ollama", "ollama"),
];
let mut out: Vec<Value> = Vec::new();
for (probe, name, key) in order {
let src = if probe == "glm" { "claude" } else { probe };
if let Some(inst) = installed.get(src) {
let lat = latest.get(src).cloned();
let update = lat.as_ref().map(|l| l != inst).unwrap_or(false);
out.push(json!({
"name": name, "key": key, "installed": inst,
"latest": lat, "updateAvailable": update,
}));
}
}
Ok(Json(json!({ "tools": out })))
}
/// `POST /api/nodes/{id}/terminal/ticket` — mint a single-use terminal ticket
/// (the browser WS handshake can't carry a bearer header).
pub async fn terminal_ticket(