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
+231
View File
@@ -0,0 +1,231 @@
//! Strongly-typed notification metadata.
//!
//! Parses the `_meta` JSON from `SessionNotification` into a struct with
//! typed fields. All fields are `Option` — gracefully degrades when
//! grok-shell hasn't been updated or meta is absent.
use serde::{Deserialize, Serialize};
/// Parsed fields from `SessionNotification._meta`.
///
/// Extracted once in [`acp_handler`](super::super::app::acp_handler) and passed
/// downstream to the tracker and agent state.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct NotificationMeta {
/// Accumulated token count across the session (`totalTokens`).
pub total_tokens: Option<u64>,
/// UTC ms when this notification was sent (`agentTimestampMs`).
pub agent_timestamp_ms: Option<i64>,
/// UTC ms when the current LLM streaming response started (`streamStartMs`).
/// Resets each tool-use loop iteration.
pub stream_start_ms: Option<i64>,
/// UTC ms when the current turn started (`turnStartMs`).
/// Constant for the entire turn.
pub turn_start_ms: Option<i64>,
/// Stable id for the prompt this notification belongs to (`promptId`).
/// The client passes a UUID in `PromptRequest._meta.promptId`; the agent
/// echoes it on every notification it emits while processing that
/// prompt. Used to drop chunks for cancelled / rewound turns.
pub prompt_id: Option<String>,
/// Whether this notification is historical replay from `session/load`.
pub is_replay: bool,
/// Raw `eventId` string (`"{sessionId}-{counter}"`). Tracked per session
/// as the reconnect cursor (`_meta.cursor` on `session/load`): the agent
/// resolves it by exact string match against persisted lines, so the full
/// id is kept — the numeric suffix alone is ambiguous across the
/// non-monotonic counter runs of a multi-resume history.
pub event_id: Option<String>,
/// Monotonic per-process sequence parsed from `eventId`
/// (`"{sessionId}-{counter}"`, see `kigi-shell util::event_id`). The
/// agent stamps the SAME `eventId` on the live emission and on the persisted
/// line that is later replayed, so a client can dedup an event it receives
/// twice (replay/live overlap, a re-emit after the reconnect gate, or
/// duplicate routing). Per-session events arrive in increasing order, so the
/// pager keeps a highwater and drops anything `<=` it. `None` when the agent
/// didn't stamp an `eventId` (older shell) — such updates always apply.
pub event_seq: Option<u64>,
}
/// Serializable counterpart of the replay stamp the agent injects on
/// replayed notifications (`_meta.isReplay`, stamped by kigi-shell's
/// `forward_raw_replay_line` during `session/load`).
///
/// [`NotificationMeta::from_json`] is the parse side; this is the build
/// side, so code that constructs a replay-stamped `_meta` (test fixtures,
/// playgrounds) shares the wire key with the parser instead of hand-writing
/// `json!` literals.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReplayMetaStamp {
pub is_replay: bool,
}
impl ReplayMetaStamp {
/// `_meta` value for a replayed (`session/load`) notification.
pub fn replayed() -> serde_json::Value {
serde_json::to_value(Self { is_replay: true }).expect("serialize replay meta stamp")
}
}
/// User-prompt content-block `_meta` keys (`TextContent.meta`), shared by the
/// producers (`dispatch/queue.rs` drain, `effects.rs` prompt send) and the
/// replay consumer (`acp/tracker.rs` `handle_user_message`) so the wire keys
/// cannot drift. Tests keep raw literals — they pin the wire values.
pub mod user_prompt_meta {
/// Clean display text shown in scrollback instead of the wire text.
pub const DISPLAY_TEXT: &str = "displayText";
/// Render the display text as a skill invocation (teal leading token).
pub const DISPLAY_AS_SKILL: &str = "displayAsSkill";
/// Render the display text as a scheduled (cron) prompt.
pub const DISPLAY_AS_CRON: &str = "displayAsCron";
/// `[[start, end], …]` byte ranges of recognized slash tokens into the
/// block's `text`; only meaningful when that text is displayed verbatim
/// (never stamped alongside `displayText`).
pub const SKILL_TOKEN_RANGES: &str = "skillTokenRanges";
}
/// `UserMessageChunk` / `ContentChunk._meta` keys stamped by the shell and
/// read by the pager (live and on replay).
pub mod user_message_chunk_meta {
/// Prompt index for rewind / attribution.
pub const PROMPT_INDEX: &str = "promptIndex";
/// When true, the chunk must not become a scrollback user prompt
/// ([`kigi_shell::session::PromptOrigin::hide_user_echo_from_scrollback`]).
pub const HIDE_FROM_SCROLLBACK: &str = "hideFromScrollback";
}
impl NotificationMeta {
/// Parse from the `_meta` JSON map on a `SessionNotification`.
pub fn from_json(meta: Option<&serde_json::Map<String, serde_json::Value>>) -> Self {
let Some(m) = meta else {
return Self::default();
};
let event_id = m
.get("eventId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
// `eventId` is `"{sessionId}-{counter}"`; the counter is the part
// after the LAST '-' (session ids themselves contain '-').
let event_seq = event_id
.as_deref()
.and_then(|s| s.rsplit('-').next())
.and_then(|c| c.parse::<u64>().ok());
Self {
total_tokens: m.get("totalTokens").and_then(|v| v.as_u64()),
agent_timestamp_ms: m.get("agentTimestampMs").and_then(|v| v.as_i64()),
stream_start_ms: m.get("streamStartMs").and_then(|v| v.as_i64()),
turn_start_ms: m.get("turnStartMs").and_then(|v| v.as_i64()),
prompt_id: m
.get("promptId")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
is_replay: m.get("isReplay").and_then(|v| v.as_bool()).unwrap_or(false),
event_id,
event_seq,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_full_meta() {
let meta_json = json!({
"totalTokens": 5000u64,
"agentTimestampMs": 1700000000000i64,
"streamStartMs": 1700000000000i64 - 3200,
"turnStartMs": 1700000000000i64 - 5000,
"eventId": "sess-1-7",
});
let map = meta_json.as_object().unwrap();
let meta = NotificationMeta::from_json(Some(map));
assert_eq!(meta.total_tokens, Some(5000));
assert_eq!(meta.agent_timestamp_ms, Some(1700000000000));
assert_eq!(meta.stream_start_ms, Some(1700000000000 - 3200));
assert_eq!(meta.turn_start_ms, Some(1700000000000 - 5000));
assert!(!meta.is_replay);
assert_eq!(meta.event_id.as_deref(), Some("sess-1-7"));
assert_eq!(meta.event_seq, Some(7));
}
#[test]
fn parse_missing_new_fields() {
// Simulate old grok-shell that doesn't send streamStartMs/turnStartMs
let meta_json = json!({
"totalTokens": 1000u64,
"agentTimestampMs": 1700000000000i64,
});
let map = meta_json.as_object().unwrap();
let meta = NotificationMeta::from_json(Some(map));
assert_eq!(meta.total_tokens, Some(1000));
assert_eq!(meta.agent_timestamp_ms, Some(1700000000000));
assert_eq!(meta.stream_start_ms, None);
assert_eq!(meta.turn_start_ms, None);
assert!(!meta.is_replay);
}
#[test]
fn parse_replay_flag() {
let meta_json = json!({
"isReplay": true,
});
let map = meta_json.as_object().unwrap();
let meta = NotificationMeta::from_json(Some(map));
assert!(meta.is_replay);
}
/// The build side ([`ReplayMetaStamp::replayed`]) and the parse side
/// ([`NotificationMeta::from_json`]) must agree on the wire key — a
/// rename on either side breaks replay detection silently otherwise.
#[test]
fn replay_meta_stamp_round_trips_through_parser() {
let stamp = ReplayMetaStamp::replayed();
let map = stamp.as_object().unwrap();
let meta = NotificationMeta::from_json(Some(map));
assert!(meta.is_replay, "stamped meta must parse as a replay");
}
#[test]
fn parse_event_id_keeps_raw_string_and_seq() {
let meta_json = json!({
"eventId": "sess-ab-12-42",
});
let map = meta_json.as_object().unwrap();
let meta = NotificationMeta::from_json(Some(map));
assert_eq!(meta.event_id.as_deref(), Some("sess-ab-12-42"));
assert_eq!(meta.event_seq, Some(42));
}
/// A non-numeric suffix yields no seq (dedup disabled) but the raw id is
/// still kept for the reconnect cursor (string-matched agent-side).
#[test]
fn parse_event_id_without_numeric_suffix() {
let meta_json = json!({
"eventId": "weird-id-zzz",
});
let map = meta_json.as_object().unwrap();
let meta = NotificationMeta::from_json(Some(map));
assert_eq!(meta.event_id.as_deref(), Some("weird-id-zzz"));
assert_eq!(meta.event_seq, None);
}
#[test]
fn parse_none_meta() {
let meta = NotificationMeta::from_json(None);
assert_eq!(meta.total_tokens, None);
assert_eq!(meta.agent_timestamp_ms, None);
assert_eq!(meta.stream_start_ms, None);
assert_eq!(meta.turn_start_ms, None);
assert!(!meta.is_replay);
assert_eq!(meta.event_id, None);
assert_eq!(meta.event_seq, None);
}
}