Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "cortex",
"version": "1.7.0",
"description": "Syslog management via MCP",
"author": {
"name": "jmagar"
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.7.0] - 2026-06-02

### Added

- **CLI help now supports nested subcommand help.** `cortex ai search --help`, `cortex db status --help`, `cortex compose logs --help`, and other nested command paths now render focused usage instead of the whole namespace. The top-level `cortex --help` is regrouped into an axon-style sectioned layout with cyan headers.
- **CLI parser recovery now suggests close command and subcommand matches.** Mistyped top-level and nested commands now include `Did you mean ...` hints while preserving strict parsing.
- **AI investigation output now has compact/default controls.** `cortex ai investigate` and `cortex ai blocks` support `--detail compact|full`, with `--include-transcript`, `--max-bytes`, and `--limit` controls for bounded agent-friendly output.

### Fixed

- **Cortex CLI diagnostics no longer point operators at stale `syslog` binary invocations.** Backup guidance, help text, rollout notes, and parser comments now use the `cortex` binary where they refer to CLI commands.
- **`host-state` now fails with actionable usage when no host selector is supplied.** The parser now reports the required `--host-id` or `--hostname` selector before dispatching.
- **`--max-bytes` truncation is now byte-accurate on multibyte input** and yields a leading prefix for budgets smaller than the ellipsis instead of an empty string.

## [1.6.1] - 2026-06-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cortex"
version = "1.6.1"
version = "1.7.0"
edition = "2021"
rust-version = "1.86"
description = "Homelab intelligence platform — syslog/OTLP/Docker log aggregation, fleet awareness, and AI agent coordination over MCP, CLI, and HTTP"
Expand Down
8 changes: 4 additions & 4 deletions docs/rollout.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ grep CORTEX_API_TOKEN ~/.cortex/.env
# 3. Parity check: query the same data via local + HTTP and assert
# the JSON shapes agree. Empty diff = safe to cut over.
cortex --json hosts | jq -S . > /tmp/syslog-local.json
CORTEX_USE_HTTP=1 syslog --json hosts | jq -S . > /tmp/syslog-http.json
CORTEX_USE_HTTP=1 cortex --json hosts | jq -S . > /tmp/syslog-http.json
diff /tmp/syslog-local.json /tmp/syslog-http.json && echo "parity OK"

# 4. ai-watch daemon must be active + binary SHA recorded so we know
Expand All @@ -55,7 +55,7 @@ Order matters. Each step's failure mode is documented inline.
`CORTEX_USE_HTTP=false`). Run BEFORE pulling the new image so the
container has a token to start with.

2. **`cortex compose pull && syslog compose up`** — pull the v0.26
2. **`cortex compose pull && cortex compose up`** — pull the v0.26
image and recreate the container. The container fails fast if
`CORTEX_API_TOKEN` is missing; step 1 prevents that. Wait until
`cortex compose ps` reports `healthy` before proceeding.
Expand Down Expand Up @@ -107,7 +107,7 @@ docker compose logs cortex --since 5m | grep -E "500|ERROR|panic" | wc -l # ex
# Total log count grew from the +0 baseline captured in pre-deploy step 1.
cortex stats
# CLI-to-API latency on a representative read.
time syslog tail -n 100 --json > /dev/null # expect: < 0.2s on a warm cache
time cortex tail -n 100 --json > /dev/null # expect: < 0.2s on a warm cache
```

### +24 hours
Expand Down Expand Up @@ -169,7 +169,7 @@ is required for a same-day revert.

- **VACUUM on large databases**: `db vacuum --full` on a database
larger than ~10 GB may exceed the 10-minute HTTP request timeout.
Workaround: `CORTEX_USE_HTTP=false syslog db vacuum --full --force`
Workaround: `CORTEX_USE_HTTP=false cortex db vacuum --full --force`
to bypass the API and run VACUUM directly against the SQLite file.
This is a known limitation tracked for the v0.27 successor.

Expand Down
2 changes: 1 addition & 1 deletion docs/runbooks/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ cortex compose up
# Option 2: Revert to previous commit
git log --oneline -5 # find the good commit
git revert HEAD # or git reset --hard <sha>
cortex compose pull && syslog compose up
cortex compose pull && cortex compose up
```

## Health Check
Expand Down
2 changes: 1 addition & 1 deletion mcpb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"manifest_version": "0.4",
"name": "cortex",
"display_name": "Cortex",
"version": "1.6.1",
"version": "1.7.0",
"description": "Query local cortex SQLite logs through a bundled stdio MCP server.",
"long_description": "cortex packages the existing cortex stdio entrypoint as a local MCP Bundle. It is query-only: it reads the configured SQLite database and does not start syslog listeners, HTTP servers, Docker Compose, REST, or deploy flows.",
"author": {
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
"url": "https://github.com/jmagar/cortex",
"source": "github"
},
"version": "1.6.1",
"version": "1.7.0",
"packages": [
{
"registryType": "oci",
"identifier": "ghcr.io/jmagar/cortex:v1.6.1",
"identifier": "ghcr.io/jmagar/cortex:v1.7.0",
"transport": {
"type": "stdio"
},
Expand Down
20 changes: 11 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ pub(crate) use args::{
AgentCommandCommand, AgentCommandIngestSpoolArgs, AgentCommandWrapArgs, AiAbuseArgs, AiAddArgs,
AiAskHistoryArgs, AiAssessArgs, AiBlocksArgs, AiCheckpointsArgs, AiCommand, AiContextArgs,
AiCorrelateArgs, AiDoctorArgs, AiErrorsArgs, AiIncidentContextArgs, AiIncidentsArgs,
AiIndexArgs, AiInvestigateArgs, AiListArgs, AiPruneCheckpointsArgs, AiSearchArgs,
AiSimilarArgs, AiWatchArgs, CliCommand, ComposeArgs, ComposeCommand, ComposeLogsArgs,
ComposeMutationArgs, CorrelateArgs, DbBackupArgs, DbCheckpointArgs, DbCommand, DbIntegrityArgs,
DbIntegrityStatusArgs, DbStatusArgs, DbVacuumArgs, EntityArgs, FilterArgs, GraphAroundArgs,
GraphCommand, GraphExplainArgs, GraphRebuildArgs, GraphStatusArgs, HeartbeatAgentArgs,
HeartbeatCommand, IncidentArgs, IngestRateArgs, NotifyRecentArgs, NotifyTestArgs, OutputArgs,
PatternsArgs, PluginHookArgs, SearchArgs, ServiceCommand, ServiceLogsArgs, SessionsArgs,
SetupArgs, SetupCommand, ShellAtuinIndexArgs, ShellCommand, ShellIndexArgs, SigAckArgs,
SigListArgs, SigUnackArgs, SourceIpsArgs, TailArgs, TimeRangeArgs, TimelineArgs,
AiIndexArgs, AiInvestigateArgs, AiListArgs, AiOutputDetail, AiPruneCheckpointsArgs,
AiSearchArgs, AiSimilarArgs, AiWatchArgs, CliCommand, ComposeArgs, ComposeCommand,
ComposeLogsArgs, ComposeMutationArgs, CorrelateArgs, DbBackupArgs, DbCheckpointArgs, DbCommand,
DbIntegrityArgs, DbIntegrityStatusArgs, DbStatusArgs, DbVacuumArgs, EntityArgs, FilterArgs,
GraphAroundArgs, GraphCommand, GraphExplainArgs, GraphRebuildArgs, GraphStatusArgs,
HeartbeatAgentArgs, HeartbeatCommand, IncidentArgs, IngestRateArgs, NotifyRecentArgs,
NotifyTestArgs, OutputArgs, PatternsArgs, PluginHookArgs, SearchArgs, ServiceCommand,
ServiceLogsArgs, SessionsArgs, SetupArgs, SetupCommand, ShellAtuinIndexArgs, ShellCommand,
ShellIndexArgs, SigAckArgs, SigListArgs, SigUnackArgs, SourceIpsArgs, TailArgs, TimeRangeArgs,
TimelineArgs,
};
pub(crate) use args_config::{
ConfigCommand, ConfigGetArgs, ConfigListArgs, ConfigSetArgs, ConfigTarget, ConfigUnsetArgs,
Expand Down Expand Up @@ -59,6 +60,7 @@ mod parse_config;
mod parse_logs;
mod setup;
mod sparkline;
mod suggest;
mod table;

pub(crate) use config_cmd::run_config;
Expand Down
4 changes: 2 additions & 2 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ mod surface;
pub(crate) use ai::{
AiAbuseArgs, AiAddArgs, AiAskHistoryArgs, AiAssessArgs, AiBlocksArgs, AiCheckpointsArgs,
AiCommand, AiContextArgs, AiCorrelateArgs, AiDoctorArgs, AiErrorsArgs, AiIncidentContextArgs,
AiIncidentsArgs, AiIndexArgs, AiInvestigateArgs, AiListArgs, AiPruneCheckpointsArgs,
AiSearchArgs, AiSimilarArgs, AiWatchArgs,
AiIncidentsArgs, AiIndexArgs, AiInvestigateArgs, AiListArgs, AiOutputDetail,
AiPruneCheckpointsArgs, AiSearchArgs, AiSimilarArgs, AiWatchArgs,
};
pub(crate) use surface::{
AnomaliesArgs, AppsArgs, ClockSkewArgs, CompareArgs, CorrelateStateArgs, FleetStateArgs,
Expand Down
26 changes: 26 additions & 0 deletions src/cli/args/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,32 @@ pub(crate) struct AiBlocksArgs {
pub tool: Option<String>,
pub from: Option<String>,
pub to: Option<String>,
pub limit: Option<usize>,
pub detail: AiOutputDetail,
pub json: bool,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum AiOutputDetail {
#[default]
Compact,
Full,
}

impl AiOutputDetail {
pub(crate) fn parse(value: &str, flag: &str) -> anyhow::Result<Self> {
match value {
"compact" => Ok(Self::Compact),
"full" => Ok(Self::Full),
_ => anyhow::bail!("{flag} must be compact or full"),
}
}

pub(crate) fn is_compact(self) -> bool {
matches!(self, Self::Compact)
}
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct AiContextArgs {
pub project: String,
Expand Down Expand Up @@ -217,6 +240,9 @@ pub(crate) struct AiInvestigateArgs {
pub window_minutes: Option<u32>,
pub correlation_window_minutes: Option<u32>,
pub terms: Vec<String>,
pub detail: AiOutputDetail,
pub include_transcript: bool,
pub max_bytes: Option<usize>,
pub json: bool,
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/anomalies.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Parse function for `syslog anomalies`.
//! Parse function for `cortex anomalies`.
//!
//! Surface parity (2026-05-22): exposes the `anomalies` MCP action and
//! `GET /api/anomalies` REST route as a top-level CLI subcommand.
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/apps.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Parse function for `syslog apps`.
//! Parse function for `cortex apps`.
//!
//! Surface parity (2026-05-22): exposes the `apps` MCP action and
//! `GET /api/apps` REST route as a top-level CLI subcommand.
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/clock_skew.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Parse function for `syslog clock-skew`.
//! Parse function for `cortex clock-skew`.
//!
//! Surface parity (2026-05-22): exposes the `clock_skew` MCP action and
//! `GET /api/clock-skew` REST route as a top-level CLI subcommand.
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/compare.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Parse function for `syslog compare`.
//! Parse function for `cortex compare`.
//!
//! Surface parity (2026-05-22): exposes the `compare` MCP action and
//! `GET /api/compare` REST route as a top-level CLI subcommand. All four
Expand Down
14 changes: 13 additions & 1 deletion src/cli/commands/host_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ pub(crate) fn parse_host_state(args: &[String]) -> Result<CliCommand> {
} else if let Some(v) = flags.match_value(&arg, "--limit")? {
parsed.limit = Some(parse_u32_flag("--limit", v)?);
} else {
bail!("unknown host-state option: {arg}");
bail!(
"{}",
super::super::suggest::unknown_option(
"host-state",
&arg,
&["--json", "--host-id", "--hostname", "--since", "--limit"],
)
);
}
}
if parsed.host_id.is_none() && parsed.hostname.is_none() {
bail!(
"host-state requires --host-id ID or --hostname HOST\n\nUsage: cortex host-state [--host-id ID] [--hostname HOST] [--since TIME] [--limit N] [--json]"
);
}
Ok(CliCommand::HostState(parsed))
}
9 changes: 8 additions & 1 deletion src/cli/commands/notify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ pub(crate) fn parse_notify(args: &[String]) -> Result<CliCommand> {
match subcommand.as_str() {
"recent" => parse_notify_recent(rest),
"test" => parse_notify_test(rest),
_ => bail!("unknown notify subcommand: {subcommand}"),
_ => bail!(
"{}",
super::super::suggest::unknown_command(
"notify subcommand",
subcommand,
&["recent", "test"],
)
),
}
}

Expand Down
9 changes: 8 additions & 1 deletion src/cli/commands/sig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ pub(crate) fn parse_sig(args: &[String]) -> Result<CliCommand> {
"list" => parse_sig_list(rest),
"ack" => parse_sig_ack(rest),
"unack" => parse_sig_unack(rest),
_ => bail!("unknown sig subcommand: {subcommand}"),
_ => bail!(
"{}",
super::super::suggest::unknown_command(
"sig subcommand",
subcommand,
&["list", "ack", "unack"],
)
),
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/silent_hosts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Parse function for `syslog silent-hosts`.
//! Parse function for `cortex silent-hosts`.
//!
//! Surface parity (2026-05-22): exposes the `silent_hosts` MCP action and
//! `GET /api/silent-hosts` REST route as a top-level CLI subcommand.
Expand Down
22 changes: 17 additions & 5 deletions src/cli/dispatch_ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ use super::output_ai::{
print_checkpoints_response, print_index_response, print_prune_checkpoints_response,
};
use super::output_ai_more::{
print_ai_incidents_response, print_ai_investigate_response, print_ask_history_response,
print_incident_context_response, print_similar_incidents_response,
print_ai_incidents_response, print_ai_investigate_response_with_options,
print_ask_history_response, print_incident_context_response, print_similar_incidents_response,
AiInvestigatePrintOptions,
};
use super::output_logs::{
print_abuse_search_response, print_ai_correlate_response, print_ai_projects_response,
print_ai_tools_response, print_project_context_response, print_search_sessions_response,
print_usage_blocks_response,
print_usage_blocks_response_with_options, UsageBlocksPrintOptions,
};
use super::{
AiAbuseArgs, AiAddArgs, AiAskHistoryArgs, AiAssessArgs, AiBlocksArgs, AiCheckpointsArgs,
Expand Down Expand Up @@ -229,12 +230,18 @@ pub(crate) async fn run_ai_correlate(mode: &CliMode, args: AiCorrelateArgs) -> R

pub(crate) async fn run_ai_blocks(mode: &CliMode, args: AiBlocksArgs) -> Result<()> {
let json = args.json;
let detail = args.detail;
let limit = args.limit;
let req = args.into_request();
let response = match mode {
CliMode::Local(service) => service.usage_blocks(req).await?,
CliMode::Http(client) => http_or_cancel(client.ai_blocks(&req)).await?,
};
print_usage_blocks_response(&response, json)
print_usage_blocks_response_with_options(
&response,
json,
UsageBlocksPrintOptions { detail, limit },
)
}

pub(crate) async fn run_ai_context(mode: &CliMode, args: AiContextArgs) -> Result<()> {
Expand Down Expand Up @@ -461,12 +468,17 @@ pub(crate) async fn run_ai_incidents(mode: &CliMode, args: AiIncidentsArgs) -> R

pub(crate) async fn run_ai_investigate(mode: &CliMode, args: AiInvestigateArgs) -> Result<()> {
let json = args.json;
let print_options = AiInvestigatePrintOptions {
detail: args.detail,
include_transcript: args.include_transcript,
max_bytes: args.max_bytes.unwrap_or(240),
};
let req = args.into_request();
let response = match mode {
CliMode::Local(service) => service.investigate_ai_incidents(req).await?,
CliMode::Http(client) => http_or_cancel(client.ai_investigate(&req)).await?,
};
print_ai_investigate_response(&response, json)
print_ai_investigate_response_with_options(&response, json, print_options)
}

pub(crate) async fn run_ai_assess(mode: &CliMode, args: AiAssessArgs) -> Result<()> {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/dispatch_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ pub(crate) async fn run_db_backup(mode: &CliMode, args: DbBackupArgs) -> Result<
The container is likely running and holds the SQLite write lock.\n\
To backup through the running server (recommended):\n\
\n\
\tsyslog --http db backup --output /data/backup-$(date +%Y%m%d).db\n\
\tcortex --http db backup --output /data/backup-$(date +%Y%m%d).db\n\
\n\
Or backup inside the container directly:\n\
\n\
Expand Down
1 change: 1 addition & 0 deletions src/cli/dispatch_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ fn ai_blocks_args_into_request_snapshot() {
from: None,
to: None,
json: false,
..Default::default()
};
let req = args.into_request();
assert_eq!(
Expand Down
Loading
Loading