Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.
Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
ptyctl, ptyctl-cli, third_party/ unchanged; proto package
xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
(templates re-encrypted)
Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
module & dc_log, heap-profile uploader, auth-diagnostics uploader,
session-analytics halves of feedback; local zero-egress observability
preserved in new kigi-log crate (unified log, --debug firehose,
subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
shell util
Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted
Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean
Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
(new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
fast-worktree); RSS measurement tests serialized via serial_test
Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
notices sustained; kigi-tools ported-code notices extended; README,
CONTRIBUTING, SECURITY, AGENTS.md rewritten
Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
213 lines
7.1 KiB
Rust
213 lines
7.1 KiB
Rust
//! Runtime XTVERSION probe (`CSI > 0 q` → `DCS > | text ST`), run when
|
|
//! env-based brand detection yields Unknown (SSH, plain xterm) or a
|
|
//! headfully-validated allowlisted brand (see [`gate_allows_probe`]).
|
|
//!
|
|
//! Fire-and-forget, parser-integrated model (as in helix and similar TUIs):
|
|
//! the query is written once at startup with no timed read; the reply is
|
|
//! recognized and swallowed by the event loop's `XtversionFilter` whenever
|
|
//! it arrives.
|
|
//!
|
|
//! Safety invariants:
|
|
//! - Query write must happen after `enable_raw_mode()` and before the
|
|
//! `EventStream` filter is constructed.
|
|
//! - Accepted residuals: SSH *from* JediTerm still probes (its env marker
|
|
//! doesn't cross SSH) and leaks the query there; a reply whose first
|
|
//! event arrives only after the filter's 5s arm window types as
|
|
//! Alt+Shift+P + literal text; on a silent terminal with a fully idle
|
|
//! session the `OnceLock` stays unset (`record_no_reply` only runs from
|
|
//! the filter, which only runs on input) — `detected()` is None either
|
|
//! way, so both consumers are unaffected.
|
|
|
|
use std::sync::OnceLock;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
/// Startup probe outcome.
|
|
#[derive(Debug)]
|
|
enum ProbeResult {
|
|
Skipped,
|
|
NoReply,
|
|
Identified(String),
|
|
}
|
|
|
|
/// Unset while the query is in flight (or never sent).
|
|
static XTVERSION: OnceLock<ProbeResult> = OnceLock::new();
|
|
|
|
/// True once the query bytes were written to the terminal.
|
|
static QUERY_SENT: AtomicBool = AtomicBool::new(false);
|
|
|
|
/// XTVERSION query alone — no DA1 sentinel: nothing waits on reply
|
|
/// ordering here, and a stale unsolicited DA1 reply could mis-answer a
|
|
/// future crossterm DA1-waiting probe.
|
|
#[cfg(unix)]
|
|
const QUERY: &[u8] = b"\x1b[>0q";
|
|
|
|
/// Returns the terminal's self-reported name/version, if the terminal
|
|
/// answered (e.g. `"kitty 0.35.2"`, `"foot(1.22.0)"`).
|
|
pub fn detected() -> Option<&'static str> {
|
|
match XTVERSION.get() {
|
|
Some(ProbeResult::Identified(v)) => Some(v),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// True when the query was sent and no reply has been recorded yet — the
|
|
/// event loop arms its response filter on this.
|
|
pub fn reply_pending() -> bool {
|
|
QUERY_SENT.load(Ordering::Relaxed) && XTVERSION.get().is_none()
|
|
}
|
|
|
|
/// Record the DCS payload recognized by the event-loop filter.
|
|
pub fn record_reply(payload: &str) {
|
|
let result = match sanitize_payload(payload) {
|
|
Some(v) => ProbeResult::Identified(v),
|
|
None => ProbeResult::NoReply,
|
|
};
|
|
tracing::info!(?result, "XTVERSION probe");
|
|
let _ = XTVERSION.set(result);
|
|
}
|
|
|
|
/// Record that the filter disarmed without seeing a reply. Only invoked
|
|
/// from the filter on input, so a fully idle session can leave the
|
|
/// `OnceLock` unset (benign — see module doc).
|
|
pub fn record_no_reply() {
|
|
if XTVERSION.set(ProbeResult::NoReply).is_ok() {
|
|
tracing::info!("XTVERSION probe: no reply");
|
|
}
|
|
}
|
|
|
|
/// Send the XTVERSION query once at startup (fire-and-forget); no-ops when
|
|
/// the gate rejects the brand/multiplexer or stdin is not a TTY.
|
|
pub fn probe_at_startup() {
|
|
use std::io::IsTerminal;
|
|
|
|
if XTVERSION.get().is_some() || QUERY_SENT.load(Ordering::Relaxed) {
|
|
return;
|
|
}
|
|
let ctx = super::terminal_context();
|
|
if !gate_allows_probe(ctx) || !std::io::stdin().is_terminal() {
|
|
let _ = XTVERSION.set(ProbeResult::Skipped);
|
|
return;
|
|
}
|
|
send_query();
|
|
}
|
|
|
|
/// Crush-style brand allowlist: Unknown plus brands headfully validated as
|
|
/// clean XTVERSION responders (version fidelity is the payoff there).
|
|
/// CSI-intercepting multiplexers skip — the innermost layer answers as
|
|
/// itself, which the `multiplexer` field already records. Transparent muxes
|
|
/// (e.g. cmux) need no special case.
|
|
fn gate_allows_probe(ctx: &super::TerminalContext) -> bool {
|
|
use super::TerminalName::*;
|
|
matches!(
|
|
ctx.brand,
|
|
Unknown | Kitty | WezTerm | Ghostty | Iterm2 | Rio
|
|
) && !ctx.multiplexer.intercepts_csi_queries()
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn send_query() {
|
|
if super::probe::write_query(QUERY) {
|
|
QUERY_SENT.store(true, Ordering::Relaxed);
|
|
} else {
|
|
// Brand-Unknown TTY whose query can't reach the terminal is a
|
|
// feedback-triage signal worth tracing.
|
|
tracing::debug!("XTVERSION probe skipped: query write failed or output is not a TTY");
|
|
let _ = XTVERSION.set(ProbeResult::Skipped);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn send_query() {
|
|
// ConPTY does not implement XTVERSION.
|
|
let _ = XTVERSION.set(ProbeResult::Skipped);
|
|
}
|
|
|
|
/// Strip controls and trim; `None` for an empty payload.
|
|
fn sanitize_payload(payload: &str) -> Option<String> {
|
|
let cleaned: String = payload.chars().filter(|c| !c.is_control()).collect();
|
|
let cleaned = cleaned.trim().to_owned();
|
|
if cleaned.is_empty() {
|
|
None
|
|
} else {
|
|
Some(cleaned)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn sanitize_plain_payload() {
|
|
assert_eq!(
|
|
sanitize_payload("kitty 0.35.2").as_deref(),
|
|
Some("kitty 0.35.2")
|
|
);
|
|
assert_eq!(
|
|
sanitize_payload("XTerm(388)").as_deref(),
|
|
Some("XTerm(388)")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_strips_controls_and_whitespace() {
|
|
assert_eq!(
|
|
sanitize_payload(" We\x01zTerm 2.0 ").as_deref(),
|
|
Some("WezTerm 2.0")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sanitize_empty_is_none() {
|
|
assert_eq!(sanitize_payload(""), None);
|
|
assert_eq!(sanitize_payload(" \x07 "), None);
|
|
}
|
|
|
|
#[test]
|
|
fn gate_allows_unknown_and_allowlisted_brands() {
|
|
use crate::terminal::{MultiplexerKind, TerminalContext, TerminalName};
|
|
let ctx = |brand, multiplexer| TerminalContext {
|
|
brand,
|
|
multiplexer,
|
|
..Default::default()
|
|
};
|
|
for brand in [
|
|
TerminalName::Unknown,
|
|
TerminalName::Kitty,
|
|
TerminalName::WezTerm,
|
|
TerminalName::Ghostty,
|
|
TerminalName::Iterm2,
|
|
TerminalName::Rio,
|
|
] {
|
|
assert!(
|
|
gate_allows_probe(&ctx(brand, MultiplexerKind::Undetected)),
|
|
"{brand:?} should be probed"
|
|
);
|
|
// Transparent mux (cmux) does not intercept CSI; probe still runs.
|
|
assert!(
|
|
gate_allows_probe(&ctx(brand, MultiplexerKind::Cmux)),
|
|
"{brand:?} under cmux should still be probed"
|
|
);
|
|
// CSI-intercepting multiplexers override the brand allowlist.
|
|
assert!(
|
|
!gate_allows_probe(&ctx(brand, MultiplexerKind::Tmux)),
|
|
"{brand:?} under tmux should be skipped"
|
|
);
|
|
}
|
|
// JediTerm renders the query as garbage and must never be probed.
|
|
assert!(!gate_allows_probe(&ctx(
|
|
TerminalName::JetBrains,
|
|
MultiplexerKind::Undetected
|
|
)));
|
|
}
|
|
|
|
// Sets the process-global OnceLock — safe under nextest's
|
|
// process-per-test isolation.
|
|
#[test]
|
|
fn diagnostics_snapshot_includes_recorded_reply() {
|
|
record_reply("PtyHarnessTerm 9.9");
|
|
let t = crate::terminal::terminal_context().diagnostics_snapshot();
|
|
assert_eq!(t.xtversion, "PtyHarnessTerm 9.9");
|
|
}
|
|
}
|