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:
+246
@@ -0,0 +1,246 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use serde_yaml::Value;
|
||||
|
||||
use crate::theme::{MermaidTheme, MermaidThemePreset, MermaidThemeVariables};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct ParsedMermaidSource<'a> {
|
||||
pub body: Cow<'a, str>,
|
||||
pub frontmatter: Option<MermaidFrontmatter>,
|
||||
pub config: RenderConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct MermaidFrontmatter {
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct RenderConfig {
|
||||
pub theme: Option<MermaidThemePreset>,
|
||||
pub theme_variables: MermaidThemeVariables,
|
||||
/// Parsed for Mermaid frontmatter compatibility, but not currently rendered.
|
||||
pub layout: Option<String>,
|
||||
/// Parsed for Mermaid frontmatter compatibility, but not currently rendered.
|
||||
pub look: Option<String>,
|
||||
/// Parsed for Mermaid frontmatter compatibility, but not currently rendered.
|
||||
pub security_level: Option<String>,
|
||||
pub font_family: Option<String>,
|
||||
pub font_size: Option<String>,
|
||||
pub flowchart: FlowchartConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct FlowchartConfig {
|
||||
pub curve: Option<String>,
|
||||
/// Parsed for Mermaid frontmatter compatibility, but not currently rendered.
|
||||
pub html_labels: Option<bool>,
|
||||
pub node_spacing: Option<u32>,
|
||||
pub rank_spacing: Option<u32>,
|
||||
pub padding: Option<u32>,
|
||||
/// Parsed for Mermaid frontmatter compatibility, but not currently rendered.
|
||||
pub diagram_padding: Option<u32>,
|
||||
pub wrapping_width: Option<u32>,
|
||||
/// Parsed for Mermaid frontmatter compatibility, but not currently rendered.
|
||||
pub use_max_width: Option<bool>,
|
||||
/// Parsed for Mermaid frontmatter compatibility, but not currently rendered.
|
||||
pub default_renderer: Option<String>,
|
||||
}
|
||||
|
||||
impl RenderConfig {
|
||||
pub fn to_mermaid_theme(&self) -> Option<MermaidTheme> {
|
||||
if self.theme.is_none() && self.theme_variables.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut theme = self.theme.unwrap_or(MermaidThemePreset::Default).to_theme();
|
||||
self.theme_variables.apply_to(&mut theme);
|
||||
Some(theme)
|
||||
}
|
||||
|
||||
pub fn font_size_px(&self) -> Option<f64> {
|
||||
self.font_size.as_deref().and_then(parse_font_size)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_mermaid_frontmatter(source: &str) -> ParsedMermaidSource<'_> {
|
||||
let Some((yaml_start, yaml_end, body_start)) = frontmatter_bounds(source) else {
|
||||
return ParsedMermaidSource {
|
||||
body: Cow::Borrowed(source),
|
||||
frontmatter: None,
|
||||
config: RenderConfig::default(),
|
||||
};
|
||||
};
|
||||
|
||||
let body = Cow::Owned(source[body_start..].to_string());
|
||||
let yaml = &source[yaml_start..yaml_end];
|
||||
let Some(value) = parse_yaml_value(yaml) else {
|
||||
return ParsedMermaidSource {
|
||||
body,
|
||||
frontmatter: Some(MermaidFrontmatter::default()),
|
||||
config: RenderConfig::default(),
|
||||
};
|
||||
};
|
||||
|
||||
let frontmatter = parse_frontmatter_metadata(&value);
|
||||
let config = parse_render_config(&value);
|
||||
|
||||
ParsedMermaidSource {
|
||||
body,
|
||||
frontmatter: Some(frontmatter),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_yaml_value(yaml: &str) -> Option<Value> {
|
||||
if yaml.trim().is_empty() {
|
||||
return Some(Value::Null);
|
||||
}
|
||||
|
||||
serde_yaml::from_str::<Value>(yaml).ok()
|
||||
}
|
||||
|
||||
fn parse_frontmatter_metadata(value: &Value) -> MermaidFrontmatter {
|
||||
MermaidFrontmatter {
|
||||
title: mapping_value(value, "title").and_then(value_to_string),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_render_config(value: &Value) -> RenderConfig {
|
||||
let Some(config) = mapping_value(value, "config") else {
|
||||
return RenderConfig::default();
|
||||
};
|
||||
|
||||
RenderConfig {
|
||||
theme: mapping_value(config, "theme")
|
||||
.and_then(value_to_string)
|
||||
.and_then(|theme| MermaidThemePreset::parse(&theme)),
|
||||
theme_variables: parse_theme_variables(mapping_value(config, "themeVariables")),
|
||||
layout: mapping_value(config, "layout").and_then(value_to_string),
|
||||
look: mapping_value(config, "look").and_then(value_to_string),
|
||||
security_level: mapping_value(config, "securityLevel").and_then(value_to_string),
|
||||
font_family: mapping_value(config, "fontFamily").and_then(value_to_string),
|
||||
font_size: mapping_value(config, "fontSize").and_then(value_to_string),
|
||||
flowchart: parse_flowchart_config(mapping_value(config, "flowchart")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_theme_variables(value: Option<&Value>) -> MermaidThemeVariables {
|
||||
let mut variables = MermaidThemeVariables::default();
|
||||
let Some(Value::Mapping(mapping)) = value else {
|
||||
return variables;
|
||||
};
|
||||
|
||||
for (key, value) in mapping {
|
||||
let Some(key) = key.as_str() else {
|
||||
continue;
|
||||
};
|
||||
let Some(value) = value_to_string(value) else {
|
||||
continue;
|
||||
};
|
||||
variables.apply_mermaid_alias(key, value);
|
||||
}
|
||||
|
||||
variables
|
||||
}
|
||||
|
||||
fn parse_flowchart_config(value: Option<&Value>) -> FlowchartConfig {
|
||||
let Some(flowchart) = value else {
|
||||
return FlowchartConfig::default();
|
||||
};
|
||||
|
||||
FlowchartConfig {
|
||||
curve: mapping_value(flowchart, "curve").and_then(value_to_string),
|
||||
html_labels: mapping_value(flowchart, "htmlLabels").and_then(value_to_bool),
|
||||
node_spacing: mapping_value(flowchart, "nodeSpacing").and_then(value_to_u32),
|
||||
rank_spacing: mapping_value(flowchart, "rankSpacing").and_then(value_to_u32),
|
||||
padding: mapping_value(flowchart, "padding").and_then(value_to_u32),
|
||||
diagram_padding: mapping_value(flowchart, "diagramPadding").and_then(value_to_u32),
|
||||
wrapping_width: mapping_value(flowchart, "wrappingWidth").and_then(value_to_u32),
|
||||
use_max_width: mapping_value(flowchart, "useMaxWidth").and_then(value_to_bool),
|
||||
default_renderer: mapping_value(flowchart, "defaultRenderer").and_then(value_to_string),
|
||||
}
|
||||
}
|
||||
|
||||
fn mapping_value<'a>(value: &'a Value, key: &str) -> Option<&'a Value> {
|
||||
let Value::Mapping(mapping) = value else {
|
||||
return None;
|
||||
};
|
||||
|
||||
mapping.get(&Value::String(key.to_string()))
|
||||
}
|
||||
|
||||
fn value_to_string(value: &Value) -> Option<String> {
|
||||
match value {
|
||||
Value::String(value) => Some(value.clone()),
|
||||
Value::Number(value) => Some(value.to_string()),
|
||||
Value::Bool(value) => Some(value.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_to_bool(value: &Value) -> Option<bool> {
|
||||
match value {
|
||||
Value::Bool(value) => Some(*value),
|
||||
Value::String(value) => match value.as_str() {
|
||||
"true" => Some(true),
|
||||
"false" => Some(false),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_to_u32(value: &Value) -> Option<u32> {
|
||||
match value {
|
||||
Value::Number(value) => value.as_u64().and_then(|value| u32::try_from(value).ok()),
|
||||
Value::String(value) => value.parse().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_font_size(value: &str) -> Option<f64> {
|
||||
let trimmed = value.trim();
|
||||
let numeric = trimmed.strip_suffix("px").unwrap_or(trimmed).trim();
|
||||
numeric
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
.filter(|size| size.is_finite() && *size > 0.0)
|
||||
}
|
||||
|
||||
fn frontmatter_bounds(source: &str) -> Option<(usize, usize, usize)> {
|
||||
let mut cursor = 0;
|
||||
while cursor < source.len() {
|
||||
let end = next_line_end(source, cursor);
|
||||
let line = source[cursor..end].trim();
|
||||
if line.is_empty() {
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
if line != "---" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let yaml_start = end;
|
||||
let mut scan = end;
|
||||
while scan < source.len() {
|
||||
let scan_end = next_line_end(source, scan);
|
||||
if source[scan..scan_end].trim() == "---" {
|
||||
return Some((yaml_start, scan, scan_end));
|
||||
}
|
||||
scan = scan_end;
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn next_line_end(source: &str, start: usize) -> usize {
|
||||
source[start..]
|
||||
.find('\n')
|
||||
.map(|position| start + position + 1)
|
||||
.unwrap_or(source.len())
|
||||
}
|
||||
Reference in New Issue
Block a user