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:
@@ -0,0 +1,327 @@
|
||||
//! Filesystem locations for Kigi config files and binaries.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static KIGI_SHARE_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const CLAUDE_MANAGED_SETTINGS_PATH: &str =
|
||||
"/Library/Application Support/ClaudeCode/managed-settings.json";
|
||||
#[cfg(target_os = "linux")]
|
||||
const CLAUDE_MANAGED_SETTINGS_PATH: &str = "/etc/claude-code/managed-settings.json";
|
||||
|
||||
/// The default user Kigi directory (`~/.kigi`, canonicalized) used when
|
||||
/// `KIGI_SHARE_DIR` is unset. Exposed so callers (e.g. display helpers) can detect
|
||||
/// whether [`kigi_home()`] is the default without duplicating the computation.
|
||||
///
|
||||
/// Uses [`dunce::canonicalize`] instead of [`std::fs::canonicalize`]: on
|
||||
/// Windows, std returns a verbatim path (`\\?\C:\Users\...`) which external
|
||||
/// tools choke on — e.g. `git clone` rejects `\\?\` destinations with
|
||||
/// "Invalid argument", breaking marketplace cache clones under
|
||||
/// `~/.kigi/marketplace-cache`. `dunce` strips the prefix whenever the path
|
||||
/// is safely representable in legacy form; on non-Windows it is identical to
|
||||
/// `std::fs::canonicalize`.
|
||||
///
|
||||
/// Keep the dunce canonicalization in sync with the hand-rolled duplicate in
|
||||
/// `kigi_fast_worktree::db::resolve_kigi_home` (deliberately standalone crate).
|
||||
pub fn default_kigi_home() -> PathBuf {
|
||||
#[allow(deprecated)]
|
||||
let home = std::env::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
dunce::canonicalize(&home).unwrap_or(home).join(".kigi")
|
||||
}
|
||||
|
||||
/// Per-user config directory: `$KIGI_SHARE_DIR` or `~/.kigi`. Created if needed.
|
||||
pub fn kigi_home() -> PathBuf {
|
||||
KIGI_SHARE_DIR
|
||||
.get_or_init(|| {
|
||||
let kigi_home = if let Ok(v) = std::env::var("KIGI_SHARE_DIR") {
|
||||
PathBuf::from(v)
|
||||
} else {
|
||||
default_kigi_home()
|
||||
};
|
||||
let _ = std::fs::create_dir_all(&kigi_home);
|
||||
kigi_home
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The user-global Kigi home, but only when one genuinely resolves: `Some` when
|
||||
/// `$KIGI_SHARE_DIR` is set or a home directory is found, `None` otherwise. Unlike
|
||||
/// [`kigi_home()`], this never falls back to a cwd-relative `.kigi`, so callers
|
||||
/// that *scan* user-global Kigi resources (hooks, marketplace sources, ...) don't
|
||||
/// mistake a project's `.kigi` tree for the user-global one when no home resolves.
|
||||
pub fn user_kigi_home() -> Option<PathBuf> {
|
||||
#[allow(deprecated)]
|
||||
let resolvable = std::env::var_os("KIGI_SHARE_DIR").is_some() || std::env::home_dir().is_some();
|
||||
resolvable.then(kigi_home)
|
||||
}
|
||||
|
||||
/// Canonical kigi application path: `$KIGI_SHARE_DIR/bin/kigi` (Unix) or `kigi.exe` (Windows).
|
||||
pub fn kigi_application() -> PathBuf {
|
||||
let name = if cfg!(windows) { "kigi.exe" } else { "kigi" };
|
||||
kigi_home().join("bin").join(name)
|
||||
}
|
||||
|
||||
/// System-wide config directory: `/etc/kigi/` on Unix, `None` on Windows.
|
||||
pub fn system_config_dir() -> Option<PathBuf> {
|
||||
if cfg!(unix) {
|
||||
Some(PathBuf::from("/etc/kigi"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// System path for the managed-settings.json used for settings compat, if it exists.
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
pub fn claude_managed_settings_path() -> Option<PathBuf> {
|
||||
let path = PathBuf::from(CLAUDE_MANAGED_SETTINGS_PATH);
|
||||
path.exists().then_some(path)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
pub fn claude_managed_settings_path() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The platform path where managed-settings.json would live for settings
|
||||
/// compat, whether or not it exists. `None` on unsupported platforms.
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
pub fn claude_managed_settings_probe_path() -> Option<PathBuf> {
|
||||
Some(PathBuf::from(CLAUDE_MANAGED_SETTINGS_PATH))
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
pub fn claude_managed_settings_probe_path() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Max bytes for a single directory name component (macOS APFS, Linux ext4,
|
||||
/// NTFS all enforce 255 bytes).
|
||||
const MAX_DIRNAME_BYTES: usize = 255;
|
||||
|
||||
/// Encode a CWD string into a filesystem-safe directory name component.
|
||||
///
|
||||
/// Short CWDs (URL-encoded form <= 255 bytes) use URL-encoding for backward
|
||||
/// compatibility and human readability on disk.
|
||||
///
|
||||
/// Long CWDs (> 255 bytes encoded) use a compact `{slug}-{blake3_hex16}`
|
||||
/// form that is always <= 57 bytes. Callers must write a `.cwd` metadata
|
||||
/// file via [`ensure_sessions_cwd_dir`] so the original CWD can be
|
||||
/// recovered by [`decode_cwd_from_dirname`].
|
||||
pub fn encode_cwd_dirname(cwd: &str) -> String {
|
||||
let url_encoded = urlencoding::encode(cwd);
|
||||
if url_encoded.len() <= MAX_DIRNAME_BYTES {
|
||||
return url_encoded.into_owned();
|
||||
}
|
||||
let hash = blake3::hash(cwd.as_bytes());
|
||||
let hash16 = &hash.to_hex()[..16];
|
||||
let leaf = std::path::Path::new(cwd)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("workspace");
|
||||
let slug = slugify(leaf, 40);
|
||||
let slug = if slug.is_empty() { "workspace" } else { &slug };
|
||||
format!("{slug}-{hash16}")
|
||||
}
|
||||
|
||||
/// Recover the original CWD from a sessions CWD directory.
|
||||
///
|
||||
/// Tries URL-decoding the directory name first (works for short/legacy dirs).
|
||||
/// Falls back to reading a `.cwd` metadata file inside the directory (written
|
||||
/// by [`ensure_sessions_cwd_dir`] for hash-based dirs).
|
||||
pub fn decode_cwd_from_dirname(dir: &std::path::Path) -> Option<String> {
|
||||
let name = dir.file_name()?.to_str()?;
|
||||
if let Ok(decoded) = urlencoding::decode(name) {
|
||||
let s = decoded.into_owned();
|
||||
// URL-decoded absolute CWDs always start with `/` (Unix) or a drive
|
||||
// letter (Windows). The slug-hash form never does, so this
|
||||
// distinguishes the two encodings unambiguously.
|
||||
if s.starts_with('/') || (cfg!(windows) && s.chars().nth(1) == Some(':')) {
|
||||
return Some(s);
|
||||
}
|
||||
}
|
||||
std::fs::read_to_string(dir.join(".cwd"))
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
/// Build the CWD-level session directory path:
|
||||
/// `kigi_home()/sessions/{encode_cwd_dirname(cwd)}`.
|
||||
///
|
||||
/// Does **not** create the directory on disk — use [`ensure_sessions_cwd_dir`]
|
||||
/// when the directory must exist.
|
||||
pub fn sessions_cwd_dir(cwd: &str) -> PathBuf {
|
||||
kigi_home().join("sessions").join(encode_cwd_dirname(cwd))
|
||||
}
|
||||
|
||||
/// Create the CWD-level session directory and write a `.cwd` metadata file
|
||||
/// when hash-based encoding is used (long paths).
|
||||
///
|
||||
/// For short paths the `.cwd` file is not written because the directory name
|
||||
/// itself is reversible via URL-decoding.
|
||||
pub fn ensure_sessions_cwd_dir(cwd: &str) -> std::io::Result<PathBuf> {
|
||||
let encoded_name = encode_cwd_dirname(cwd);
|
||||
let dir = kigi_home().join("sessions").join(&encoded_name);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
// Hash-based encoding is in use when the dirname differs from the
|
||||
// plain URL-encoded form. Write a `.cwd` file so decode can recover
|
||||
// the original path. O_CREAT|O_EXCL via create_new avoids TOCTOU
|
||||
// races with parallel session starts.
|
||||
if encoded_name != urlencoding::encode(cwd).as_ref() {
|
||||
let cwd_file = dir.join(".cwd");
|
||||
match std::fs::File::create_new(&cwd_file) {
|
||||
Ok(mut f) => {
|
||||
std::io::Write::write_all(&mut f, cwd.as_bytes())?;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Generate a URL-safe slug from a string.
|
||||
///
|
||||
/// Lowercases, replaces non-alphanumeric chars with `-`, collapses
|
||||
/// consecutive dashes, and truncates to `max_len` characters.
|
||||
fn slugify(input: &str, max_len: usize) -> String {
|
||||
let mut result = String::with_capacity(input.len());
|
||||
let mut prev_dash = false;
|
||||
for c in input.to_lowercase().chars() {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
result.push(c);
|
||||
prev_dash = false;
|
||||
} else if !prev_dash {
|
||||
result.push('-');
|
||||
prev_dash = true;
|
||||
}
|
||||
}
|
||||
let trimmed = result.trim_matches('-');
|
||||
trimmed.chars().take(max_len).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Realistic CWDs that trigger the bug (URL-encoded > 255 bytes).
|
||||
const LONG_CWDS: &[&str] = &[
|
||||
"/Users/dev/Documents/開発プロジェクト/機能追加/テスト環境/ソースコード/main-branch",
|
||||
"/Users/user/Library/Mobile Documents/com~apple~CloudDocs/项目文件/深层嵌套目录/更深层次的/工作区域/project",
|
||||
"/Users/user/Library/CloudStorage/OneDrive-대한민국회사/프로젝트/개발환경/소스코드/백엔드/서비스/my-app",
|
||||
"/Users/user/Documents/工作文件夹/二零二六年项目/子目录一/子目录二/子目录三/源代码/code",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn long_cwd_uses_hash_fallback_within_name_max() {
|
||||
let long_cwd = format!("/Users/test/{}", "中".repeat(30));
|
||||
let encoded = encode_cwd_dirname(&long_cwd);
|
||||
assert!(encoded.len() <= MAX_DIRNAME_BYTES);
|
||||
assert!(!encoded.starts_with("%2F"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_long_paths_produce_different_hashes() {
|
||||
let a = format!("/Users/test/{}", "中".repeat(30));
|
||||
let b = format!("/Users/test/{}", "日".repeat(30));
|
||||
assert_ne!(encode_cwd_dirname(&a), encode_cwd_dirname(&b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_reads_cwd_file_for_hash_dirs() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().join("some-slug-abcdef0123456789");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join(".cwd"), "/original/long/path").unwrap();
|
||||
assert_eq!(
|
||||
decode_cwd_from_dirname(&dir),
|
||||
Some("/original/long/path".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_returns_none_without_cwd_file() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().join("some-slug-abcdef0123456789");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
assert_eq!(decode_cwd_from_dirname(&dir), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cwd_file_write_is_idempotent_via_excl() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let long_cwd = format!("/Users/test/{}", "中".repeat(30));
|
||||
let dir = tmp.path().join(encode_cwd_dirname(&long_cwd));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let cwd_file = dir.join(".cwd");
|
||||
std::fs::write(&cwd_file, &long_cwd).unwrap();
|
||||
match std::fs::File::create_new(&cwd_file) {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
other => panic!("expected AlreadyExists, got: {other:?}"),
|
||||
}
|
||||
assert_eq!(std::fs::read_to_string(&cwd_file).unwrap(), long_cwd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_encoded_long_cwd_fails_on_real_filesystem() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let url_encoded = urlencoding::encode(LONG_CWDS[0]).into_owned();
|
||||
let result = std::fs::create_dir_all(tmp.path().join(&url_encoded));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_roundtrip_on_real_filesystem_for_long_cwds() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
for cwd in LONG_CWDS {
|
||||
let encoded = encode_cwd_dirname(cwd);
|
||||
let dir = tmp.path().join(&encoded);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join(".cwd"), cwd).unwrap();
|
||||
assert_eq!(decode_cwd_from_dirname(&dir).as_deref(), Some(*cwd));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_cwds_use_url_encoding_and_roundtrip_on_real_filesystem() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
for cwd in [
|
||||
"/Users/foo/project",
|
||||
"/tmp",
|
||||
"/Users/user/Documents/project-名前",
|
||||
] {
|
||||
let encoded = encode_cwd_dirname(cwd);
|
||||
assert_eq!(encoded, urlencoding::encode(cwd).into_owned());
|
||||
let dir = tmp.path().join(&encoded);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
assert_eq!(decode_cwd_from_dirname(&dir).as_deref(), Some(cwd));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_kigi_home_has_no_verbatim_prefix() {
|
||||
// On Windows, std::fs::canonicalize returns `\\?\C:\...` verbatim
|
||||
// paths that external tools (notably `git clone`) reject. The dunce
|
||||
// canonicalization must yield a plain path. No-op assertion on Unix.
|
||||
let home = default_kigi_home();
|
||||
assert!(!home.to_string_lossy().starts_with(r"\\?\"));
|
||||
assert!(home.ends_with(".kigi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_basic() {
|
||||
assert_eq!(slugify("Hello World!", 40), "hello-world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_cjk_produces_empty() {
|
||||
assert_eq!(slugify("深层目录", 40), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slugify_truncates() {
|
||||
assert_eq!(slugify(&"a".repeat(100), 10).len(), 10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user