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,407 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use super::shell_token::{CurrentToken, build_insert_token, parse_current_token};
|
||||
use super::{RankedSuggestion, SuggestContext, SuggestionSource, splice_token_into_line};
|
||||
|
||||
const CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
const MAX_RESULTS: usize = 10;
|
||||
|
||||
pub(crate) struct PathProvider;
|
||||
|
||||
impl PathProvider {
|
||||
pub async fn suggest(&self, ctx: &SuggestContext) -> Vec<RankedSuggestion> {
|
||||
// shell_token quoting is POSIX-only: cmd/pwsh would misparse the
|
||||
// escaped line, so Windows serves no deterministic completions.
|
||||
if cfg!(windows) {
|
||||
return Vec::new();
|
||||
}
|
||||
let tok = match extract_command_token(ctx.prefix()) {
|
||||
Some(t) => t,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let token_range = (tok.start, ctx.prefix().len());
|
||||
|
||||
let cache = get_or_refresh_path_cache().await;
|
||||
let mut results = filter_executables(&tok, token_range, &cache.executables);
|
||||
splice_token_into_line(&mut results, &ctx.text, token_range);
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
/// The command token being typed, via the canonical tokenizer: quotes hide
|
||||
/// separators (`echo "a | gr` is quoted data, not a command position), the
|
||||
/// cursor must sit in the segment's first word, and — mirroring the file
|
||||
/// provider — flag-looking tokens never complete.
|
||||
fn extract_command_token(prefix: &str) -> Option<CurrentToken> {
|
||||
let tok = parse_current_token(prefix);
|
||||
if tok.tokens_before != 0 || tok.after_redirect || tok.value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if tok.value.starts_with('-') {
|
||||
return None;
|
||||
}
|
||||
Some(tok)
|
||||
}
|
||||
|
||||
fn filter_executables(
|
||||
tok: &CurrentToken,
|
||||
token_range: (usize, usize),
|
||||
executables: &[String],
|
||||
) -> Vec<RankedSuggestion> {
|
||||
let prefix = tok.value.as_str();
|
||||
// Binary search to the first entry >= prefix, then take while starts_with.
|
||||
let start = executables.partition_point(|e| e.as_str() < prefix);
|
||||
|
||||
let mut results: Vec<RankedSuggestion> = Vec::new();
|
||||
let mut truncated = false;
|
||||
for exe in executables[start..]
|
||||
.iter()
|
||||
.take_while(|exe| exe.starts_with(prefix))
|
||||
{
|
||||
if results.len() == MAX_RESULTS {
|
||||
// An uncapped match remains: the set is not exhaustive.
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
results.push(RankedSuggestion {
|
||||
display: exe.clone(),
|
||||
// Re-quoted like filenames: an executable named `zz;echo PWNED`
|
||||
// must insert as ONE word, never a second command.
|
||||
insert_text: build_insert_token(tok, "", exe, false),
|
||||
description: String::new(),
|
||||
source: SuggestionSource::Path,
|
||||
priority: 0,
|
||||
is_ghost_candidate: false,
|
||||
replace_range: Some(token_range),
|
||||
token_text: None,
|
||||
truncated: false,
|
||||
});
|
||||
}
|
||||
if truncated {
|
||||
results.iter_mut().for_each(|s| s.truncated = true);
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
// --- PATH cache ---
|
||||
|
||||
struct PathCacheInner {
|
||||
executables: Vec<String>,
|
||||
updated_at: Instant,
|
||||
path_env: String,
|
||||
}
|
||||
|
||||
static PATH_CACHE: OnceLock<ArcSwap<PathCacheInner>> = OnceLock::new();
|
||||
static PATH_REFRESHING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
async fn get_or_refresh_path_cache() -> Arc<PathCacheInner> {
|
||||
let swap = PATH_CACHE.get_or_init(|| {
|
||||
ArcSwap::from_pointee(PathCacheInner {
|
||||
executables: Vec::new(),
|
||||
updated_at: Instant::now() - CACHE_TTL - Duration::from_secs(1),
|
||||
path_env: String::new(),
|
||||
})
|
||||
});
|
||||
|
||||
let current = swap.load_full();
|
||||
let current_path = std::env::var("PATH").unwrap_or_default();
|
||||
|
||||
if current.updated_at.elapsed() < CACHE_TTL && current.path_env == current_path {
|
||||
return current;
|
||||
}
|
||||
|
||||
if PATH_REFRESHING
|
||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
let result = match tokio::task::spawn_blocking(scan_path_dirs).await {
|
||||
Ok(executables) => {
|
||||
let new = Arc::new(PathCacheInner {
|
||||
executables,
|
||||
updated_at: Instant::now(),
|
||||
path_env: current_path,
|
||||
});
|
||||
swap.store(Arc::clone(&new));
|
||||
new
|
||||
}
|
||||
Err(_) => current,
|
||||
};
|
||||
|
||||
PATH_REFRESHING.store(false, Ordering::Release);
|
||||
result
|
||||
}
|
||||
|
||||
fn scan_path_dirs() -> Vec<String> {
|
||||
let path_var = std::env::var("PATH").unwrap_or_default();
|
||||
scan_path_from(&path_var)
|
||||
}
|
||||
|
||||
fn scan_path_from(path_var: &str) -> Vec<String> {
|
||||
let mut executables = Vec::new();
|
||||
|
||||
for dir in std::env::split_paths(path_var) {
|
||||
let entries = match std::fs::read_dir(&dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
for entry in entries.filter_map(Result::ok) {
|
||||
let meta = match entry.metadata() {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if !meta.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if meta.permissions().mode() & 0o111 == 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(name) = entry.file_name().into_string() {
|
||||
executables.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executables.sort_unstable();
|
||||
executables.dedup();
|
||||
executables
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// --- extract_command_token ---
|
||||
|
||||
fn cmd(prefix: &str) -> Option<(usize, String)> {
|
||||
extract_command_token(prefix).map(|t| (t.start, t.value))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_at_start_of_line() {
|
||||
assert_eq!(cmd("git"), Some((0, "git".into())));
|
||||
assert_eq!(cmd("gi"), Some((0, "gi".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_prefix_at_argument_position() {
|
||||
assert_eq!(cmd("git com"), None);
|
||||
assert_eq!(cmd("ls | grep foo"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_after_separators() {
|
||||
assert_eq!(cmd("ls | gr"), Some((5, "gr".into())));
|
||||
assert_eq!(cmd("make && gi"), Some((8, "gi".into())));
|
||||
assert_eq!(cmd("cd /tmp; ls"), Some((9, "ls".into())));
|
||||
assert_eq!(cmd("false || tr"), Some((9, "tr".into())));
|
||||
assert_eq!(cmd("sleep 10 & ls"), Some((11, "ls".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_when_empty_after_separator() {
|
||||
assert_eq!(cmd("ls | "), None);
|
||||
assert_eq!(cmd("sleep 10 & "), None);
|
||||
assert_eq!(cmd("&&"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_for_empty_or_whitespace_input() {
|
||||
assert_eq!(cmd(""), None);
|
||||
assert_eq!(cmd(" "), None);
|
||||
}
|
||||
|
||||
/// A separator inside quotes is data, not a command position — the old
|
||||
/// naive segment scan offered executables inside quoted strings.
|
||||
#[test]
|
||||
fn none_inside_quoted_data() {
|
||||
assert_eq!(cmd("echo \"x | gr"), None);
|
||||
}
|
||||
|
||||
/// Flag-looking and redirect-target tokens never command-complete.
|
||||
#[test]
|
||||
fn none_for_flags_and_redirect_targets() {
|
||||
assert_eq!(cmd("-gr"), None);
|
||||
assert_eq!(cmd("> lo"), None);
|
||||
}
|
||||
|
||||
// --- filter_executables ---
|
||||
|
||||
fn tok(prefix: &str) -> CurrentToken {
|
||||
parse_current_token(prefix)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_matches_prefix() {
|
||||
let exes = vec![
|
||||
"gcc".into(),
|
||||
"git".into(),
|
||||
"grep".into(),
|
||||
"less".into(),
|
||||
"ls".into(),
|
||||
];
|
||||
let results = filter_executables(&tok("gr"), (0, 2), &exes);
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].insert_text, "grep");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_multiple_matches() {
|
||||
let exes = vec!["git".into(), "grep".into(), "gzip".into()];
|
||||
let results = filter_executables(&tok("g"), (0, 1), &exes);
|
||||
assert_eq!(results.len(), 3);
|
||||
assert!(results.iter().all(|s| !s.truncated));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_no_match() {
|
||||
let exes = vec!["git".into(), "grep".into()];
|
||||
assert!(filter_executables(&tok("docker"), (0, 6), &exes).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_path_suggestions_are_not_ghost() {
|
||||
let exes = vec!["git".into()];
|
||||
let results = filter_executables(&tok("g"), (0, 1), &exes);
|
||||
assert!(!results[0].is_ghost_candidate);
|
||||
assert_eq!(results[0].source, SuggestionSource::Path);
|
||||
assert_eq!(results[0].priority, 0);
|
||||
}
|
||||
|
||||
/// Capped sets mark every row truncated so the pager keeps
|
||||
/// dropdown-only semantics (an unshown match could disprove an LCP).
|
||||
#[test]
|
||||
fn filter_caps_at_max_and_marks_truncated() {
|
||||
let exes: Vec<String> = (0..20).map(|i| format!("test_{i:03}")).collect();
|
||||
let results = filter_executables(&tok("test"), (0, 4), &exes);
|
||||
assert_eq!(results.len(), MAX_RESULTS);
|
||||
assert!(results.iter().all(|s| s.truncated));
|
||||
|
||||
let exes: Vec<String> = (0..MAX_RESULTS).map(|i| format!("test_{i:03}")).collect();
|
||||
let results = filter_executables(&tok("test"), (0, 4), &exes);
|
||||
assert_eq!(results.len(), MAX_RESULTS);
|
||||
assert!(results.iter().all(|s| !s.truncated));
|
||||
}
|
||||
|
||||
/// The segment-after-pipe token range: accepting `grep` for `ls | gr`
|
||||
/// must target only the `gr` token, never the whole line.
|
||||
#[test]
|
||||
fn filter_stamps_segment_token_range() {
|
||||
let t = extract_command_token("ls | gr").unwrap();
|
||||
let exes = vec!["grep".into()];
|
||||
let results = filter_executables(&t, (t.start, 7), &exes);
|
||||
assert_eq!(results[0].replace_range, Some((5, 7)));
|
||||
assert_eq!(results[0].insert_text, "grep");
|
||||
}
|
||||
|
||||
/// Metacharacter executable names insert as ONE word — accepting
|
||||
/// `zz;echo PWNED` must never put a second command on the line.
|
||||
#[test]
|
||||
fn filter_escapes_metacharacter_executable_names() {
|
||||
let exes = vec!["zz;echo PWNED".into()];
|
||||
let results = filter_executables(&tok("zz"), (0, 2), &exes);
|
||||
assert_eq!(results[0].display, "zz;echo PWNED");
|
||||
assert_eq!(results[0].insert_text, "zz\\;echo\\ PWNED");
|
||||
}
|
||||
|
||||
/// A quoted command prefix completes inside its quote style.
|
||||
#[test]
|
||||
fn filter_requotes_quoted_command_prefix() {
|
||||
let exes = vec!["grep".into()];
|
||||
let results = filter_executables(&tok("\"gr"), (0, 3), &exes);
|
||||
assert_eq!(results[0].insert_text, "\"grep\"");
|
||||
}
|
||||
|
||||
// --- scan_path_from ---
|
||||
|
||||
#[test]
|
||||
fn scan_nonexistent_dir() {
|
||||
assert!(scan_path_from("/nonexistent/path/that/doesnt/exist").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_creates_sorted_deduped_list() {
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let bin = dir.path().join("bin");
|
||||
fs::create_dir(&bin).unwrap();
|
||||
|
||||
for name in &["zzz_cmd", "aaa_cmd", "mmm_cmd"] {
|
||||
let path = bin.join(name);
|
||||
fs::write(&path, "").unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let result = scan_path_from(bin.to_str().unwrap());
|
||||
assert_eq!(result, vec!["aaa_cmd", "mmm_cmd", "zzz_cmd"]);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn scan_skips_nonexecutable_files() {
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::TempDir;
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let bin = dir.path().join("bin");
|
||||
fs::create_dir(&bin).unwrap();
|
||||
|
||||
let exec_path = bin.join("my_exec");
|
||||
fs::write(&exec_path, "").unwrap();
|
||||
fs::set_permissions(&exec_path, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
let data_path = bin.join("my_data");
|
||||
fs::write(&data_path, "").unwrap();
|
||||
fs::set_permissions(&data_path, fs::Permissions::from_mode(0o644)).unwrap();
|
||||
|
||||
let result = scan_path_from(bin.to_str().unwrap());
|
||||
assert_eq!(result, vec!["my_exec"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_deduplicates_across_dirs() {
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
let dir = TempDir::new().unwrap();
|
||||
let bin1 = dir.path().join("bin1");
|
||||
let bin2 = dir.path().join("bin2");
|
||||
fs::create_dir(&bin1).unwrap();
|
||||
fs::create_dir(&bin2).unwrap();
|
||||
|
||||
for bin in [&bin1, &bin2] {
|
||||
let path = bin.join("shared_cmd");
|
||||
fs::write(&path, "").unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let path_var = format!("{}:{}", bin1.to_str().unwrap(), bin2.to_str().unwrap());
|
||||
let result = scan_path_from(&path_var);
|
||||
assert_eq!(result, vec!["shared_cmd"]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user