M0: compilable skeleton — Kigi 0.1.0 fork surgery

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).
This commit is contained in:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
//! Input flight recorder — rolling buffer of recent key events.
//!
//! Ctrl+Shift+D dumps to `~/.kigi/logs/input-debug-<timestamp>.json`.
//! Can be better utilized once input bugs are fully resolved.
use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers};
use serde::Serialize;
use std::collections::VecDeque;
use std::time::Instant;
/// Default ring buffer capacity (~10 seconds of fast typing).
const DEFAULT_CAPACITY: usize = 200;
/// Snapshot of textarea state captured by `PromptWidget::handle_key`.
///
/// Stored on `PromptWidget` after each key; read by `AgentView` when
/// building `RawInputEntry`. `None` fields mean the key was handled before
/// reaching the textarea (e.g., file search, slash command).
#[derive(Clone, Debug, Default)]
pub struct LastInputDelta {
pub cursor_before: Option<usize>,
pub cursor_after: Option<usize>,
pub text_len_before: Option<usize>,
pub text_len_after: Option<usize>,
pub had_selection_before: Option<bool>,
pub had_selection_after: Option<bool>,
pub textarea_changed: Option<bool>,
}
/// Copy-friendly snapshot of the active pane. No heap allocation.
#[derive(Clone, Copy, Debug)]
pub enum ActivePaneSnapshot {
Prompt,
Scrollback,
Todo,
Queue,
Tasks,
Catalog,
Other,
}
/// Copy-friendly snapshot of the input outcome. No heap allocation.
#[derive(Clone, Copy, Debug)]
pub enum OutcomeSnapshot {
Changed,
Unchanged,
Action,
}
/// Raw entry stored in the ring buffer. No heap allocations —
/// formatting happens only during [`InputRingBuffer::snapshot_entries`].
#[derive(Clone, Debug)]
pub struct RawInputEntry {
pub wall_ts: u64,
pub key_code: KeyCode,
pub key_modifiers: KeyModifiers,
pub key_kind: KeyEventKind,
pub active_pane: ActivePaneSnapshot,
pub outcome: OutcomeSnapshot,
pub cursor_before: Option<usize>,
pub cursor_after: Option<usize>,
pub text_len_before: Option<usize>,
pub text_len_after: Option<usize>,
pub sel_before: Option<bool>,
pub sel_after: Option<bool>,
pub textarea_changed: Option<bool>,
}
/// Serializable output record, constructed from [`RawInputEntry`] during dump.
#[derive(Clone, Debug, Serialize)]
pub struct InputRecord {
/// Milliseconds relative to the first entry in the buffer (0-based).
pub ts_ms: u64,
/// Wall-clock unix millis for log correlation.
pub wall_ts: u64,
/// Sanitized key category (see [`sanitize_key_code`]).
/// Printable chars are logged as `"Char"` without the character value
/// to prevent reconstructing typed text from the dump.
pub key: String,
/// Modifier flags, e.g. `"NONE"`, `"CONTROL"`.
pub mods: String,
/// Key event kind: `"Press"`, `"Repeat"`, `"Release"`.
pub kind: String,
/// Which pane was active: `"Prompt"`, `"Scrollback"`, etc.
pub pane: String,
/// Input outcome: `"Changed"`, `"Unchanged"`, `"Action"`.
pub outcome: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor_before: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor_after: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_len_before: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_len_after: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sel_before: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sel_after: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub textarea_changed: Option<bool>,
}
/// Fixed-capacity ring buffer of raw input events.
pub struct InputRingBuffer {
entries: VecDeque<(Instant, RawInputEntry)>,
capacity: usize,
}
impl InputRingBuffer {
pub fn new() -> Self {
Self {
entries: VecDeque::with_capacity(DEFAULT_CAPACITY),
capacity: DEFAULT_CAPACITY,
}
}
/// Push a new record, dropping the oldest if at capacity.
pub fn push(&mut self, entry: RawInputEntry) {
let now = Instant::now();
if self.entries.len() >= self.capacity {
self.entries.pop_front();
}
self.entries.push_back((now, entry));
}
/// Snapshot all entries, formatting raw types into serializable records.
/// Timestamps are relative to the first entry in the buffer.
/// Printable characters are scrubbed to `"Char"`.
pub fn snapshot_entries(&self) -> Vec<InputRecord> {
let base = self.entries.front().map(|(t, _)| *t);
self.entries
.iter()
.map(|(t, raw)| InputRecord {
ts_ms: base.map_or(0, |b| t.duration_since(b).as_millis() as u64),
wall_ts: raw.wall_ts,
key: sanitize_key_code(&raw.key_code),
mods: format!("{:?}", raw.key_modifiers),
kind: format!("{:?}", raw.key_kind),
pane: format!("{:?}", raw.active_pane),
outcome: format!("{:?}", raw.outcome),
cursor_before: raw.cursor_before,
cursor_after: raw.cursor_after,
text_len_before: raw.text_len_before,
text_len_after: raw.text_len_after,
sel_before: raw.sel_before,
sel_after: raw.sel_after,
textarea_changed: raw.textarea_changed,
})
.collect()
}
/// Number of raw entries in the buffer.
pub fn entry_count(&self) -> usize {
self.entries.len()
}
/// Time span covered by the buffer in milliseconds.
pub fn time_span_ms(&self) -> u64 {
match (self.entries.front(), self.entries.back()) {
(Some((first, _)), Some((last, _))) => last.duration_since(*first).as_millis() as u64,
_ => 0,
}
}
}
impl Default for InputRingBuffer {
fn default() -> Self {
Self::new()
}
}
/// Top-level structure for the input debug dump file.
#[derive(Serialize)]
pub struct InputDump {
pub dumped_at: String,
pub session_id: Option<String>,
pub pager_version: &'static str,
pub terminal: kigi_pager_render::terminal::TerminalDiagnostics,
pub active_pane: String,
pub textarea_cursor: usize,
pub textarea_text_len: usize,
pub textarea_has_selection: bool,
pub entry_count: usize,
pub time_span_ms: u64,
pub entries: Vec<InputRecord>,
}
/// Return a privacy-safe description of a key code.
pub fn sanitize_key_code(code: &KeyCode) -> String {
match code {
KeyCode::Char('\x08') => "Char(BS)".to_string(),
KeyCode::Char('\x7f') => "Char(DEL)".to_string(),
KeyCode::Char(_) => "Char".to_string(),
other => format!("{other:?}"),
}
}
#[cfg(test)]
pub fn format_key_code_raw(code: &KeyCode) -> String {
match code {
KeyCode::Char(c) if c.is_ascii_graphic() || *c == ' ' => format!("Char({c:?})"),
other => format!("{other:?}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
/// Helper: build a minimal `RawInputEntry` with the given key code.
fn stub_entry(key_code: KeyCode) -> RawInputEntry {
RawInputEntry {
wall_ts: 0,
key_code,
key_modifiers: KeyModifiers::NONE,
key_kind: KeyEventKind::Press,
active_pane: ActivePaneSnapshot::Prompt,
outcome: OutcomeSnapshot::Changed,
cursor_before: None,
cursor_after: None,
text_len_before: None,
text_len_after: None,
sel_before: None,
sel_after: None,
textarea_changed: None,
}
}
#[test]
fn sanitize_printable_char_strips_value() {
assert_eq!(sanitize_key_code(&KeyCode::Char('a')), "Char");
assert_eq!(sanitize_key_code(&KeyCode::Char('Z')), "Char");
assert_eq!(sanitize_key_code(&KeyCode::Char(' ')), "Char");
}
#[test]
fn sanitize_control_chars_preserved() {
assert_eq!(sanitize_key_code(&KeyCode::Char('\x08')), "Char(BS)");
assert_eq!(sanitize_key_code(&KeyCode::Char('\x7f')), "Char(DEL)");
}
#[test]
fn format_key_code_raw_shows_punctuation() {
assert_eq!(format_key_code_raw(&KeyCode::Char(';')), "Char(';')");
assert_eq!(
format_key_code_raw(&KeyCode::Char('\'')),
format!("Char({:?})", '\'')
);
assert_eq!(
format_key_code_raw(&KeyCode::Char('\\')),
format!("Char({:?})", '\\')
);
assert_eq!(format_key_code_raw(&KeyCode::Enter), "Enter");
}
#[test]
fn sanitize_named_keys_use_debug() {
assert_eq!(sanitize_key_code(&KeyCode::Backspace), "Backspace");
assert_eq!(sanitize_key_code(&KeyCode::Enter), "Enter");
assert_eq!(sanitize_key_code(&KeyCode::Delete), "Delete");
assert_eq!(sanitize_key_code(&KeyCode::Tab), "Tab");
}
#[test]
fn ring_buffer_push_and_snapshot() {
let mut buf = InputRingBuffer::new();
for _ in 0..5 {
buf.push(stub_entry(KeyCode::Char('x')));
thread::sleep(Duration::from_millis(1));
}
let entries = buf.snapshot_entries();
assert_eq!(entries.len(), 5);
assert_eq!(entries[0].ts_ms, 0);
for w in entries.windows(2) {
assert!(w[1].ts_ms >= w[0].ts_ms);
}
assert_eq!(entries[0].key, "Char");
assert_eq!(entries[0].mods, format!("{:?}", KeyModifiers::NONE));
assert_eq!(entries[0].pane, "Prompt");
}
#[test]
fn ring_buffer_capacity() {
let mut buf = InputRingBuffer::new();
for _ in 0..250 {
buf.push(stub_entry(KeyCode::Backspace));
}
assert_eq!(buf.entry_count(), DEFAULT_CAPACITY);
let entries = buf.snapshot_entries();
assert_eq!(entries.len(), DEFAULT_CAPACITY);
assert_eq!(entries[0].key, "Backspace");
}
#[test]
fn ring_buffer_empty() {
let buf = InputRingBuffer::new();
assert_eq!(buf.snapshot_entries().len(), 0);
assert_eq!(buf.time_span_ms(), 0);
}
}