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
@@ -0,0 +1,189 @@
// todo: add support for signal handling
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
use tokio::time;
use crate::terminal::runner::{
AsyncTerminalRunner, TerminalError, TerminalRunRequest, TerminalRunResult,
};
pub struct LocalTerminalRunner;
async fn read_stream(mut stream: impl AsyncReadExt + Unpin) -> Vec<u8> {
let mut buffer = Vec::new();
let _ = stream.read_to_end(&mut buffer).await;
buffer
}
/// Truncate buffer to keep only the last `limit` bytes (drops oldest bytes).
/// Returns true if truncation occurred.
///
/// This function ensures we don't split UTF-8 characters when truncating
/// by using char_indices to find a valid character boundary.
fn truncate_buffer(buf: &mut Vec<u8>, limit: usize) -> bool {
if buf.len() > limit {
// Convert to string to work with character boundaries
let s = String::from_utf8_lossy(buf);
let excess = buf.len().saturating_sub(limit);
// Find the first char boundary at or after `excess` bytes
let start_idx = s
.char_indices()
.find(|(i, _)| *i >= excess)
.map(|(i, _)| i)
.unwrap_or(s.len());
// Slice from that boundary and update buffer
*buf = s[start_idx..].as_bytes().to_vec();
true
} else {
false
}
}
#[async_trait::async_trait]
impl AsyncTerminalRunner for LocalTerminalRunner {
async fn run(&self, request: TerminalRunRequest) -> Result<TerminalRunResult, TerminalError> {
// Build and spawn the command via the platform shell.
#[cfg(unix)]
let mut cmd = {
let mut c = Command::new(crate::terminal::default_shell_path());
c.arg("-lc").arg(&request.command);
c
};
#[cfg(not(unix))]
let mut cmd = {
let inv = kigi_config::shell::shell_command_argv(&request.command);
let mut c = Command::new(inv.program);
c.args(&inv.args).envs(inv.env);
c
};
cmd.current_dir(&request.cwd)
.envs(&request.env)
.envs(crate::terminal::pager_env())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
// Detach from the controlling terminal so child processes
// (e.g. GPG pinentry) cannot open /dev/tty and corrupt the TUI.
kigi_tools::util::detach_command(&mut cmd);
let mut child = cmd
.spawn()
.map_err(|e| TerminalError::Other(format!("Failed to start shell: {e}")))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| TerminalError::Other("Failed to capture stdout".into()))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| TerminalError::Other("Failed to capture stderr".into()))?;
let stdout_task = tokio::spawn(read_stream(stdout));
let stderr_task = tokio::spawn(read_stream(stderr));
let mut timed_out = false;
let wait_result = time::timeout(request.timeout, child.wait()).await;
let exit_status = match wait_result {
Ok(status_res) => status_res
.map_err(|e| TerminalError::Other(format!("Failed to wait for process: {e}")))?,
Err(_) => {
timed_out = true;
if let Err(e) = child.start_kill() {
tracing::warn!("Failed to kill timed-out process: {e}");
}
child.wait().await.map_err(|e| {
TerminalError::Other(format!("Failed to wait for killed process: {e}"))
})?
}
};
let stdout_result = stdout_task.await.unwrap_or_else(|_| Vec::new());
let stderr_result = stderr_task.await.unwrap_or_else(|_| Vec::new());
// Combine stdout and stderr, then truncate if needed
let mut combined = stdout_result;
combined.extend(stderr_result);
let truncated = truncate_buffer(&mut combined, request.output_byte_limit);
let combined_output = String::from_utf8_lossy(&combined).into_owned();
Ok(TerminalRunResult {
combined_output,
exit_code: exit_status.code(),
truncated,
signal: None,
timed_out,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::terminal::DEFAULT_OUTPUT_BYTE_LIMIT;
use crate::terminal::runner::TerminalRunRequest;
use kigi_paths::AbsPathBuf;
use std::collections::HashMap;
fn make_request(command: &str) -> TerminalRunRequest {
TerminalRunRequest {
tool_call_id: agent_client_protocol::ToolCallId::new("test"),
command: command.to_string(),
cwd: AbsPathBuf::new(std::env::current_dir().unwrap()).unwrap(),
env: HashMap::new(),
timeout: std::time::Duration::from_secs(10),
output_byte_limit: DEFAULT_OUTPUT_BYTE_LIMIT,
stream: false,
output_file: None,
}
}
/// Verify that `detach_from_tty` prevents child processes from opening
/// `/dev/tty`. After setsid(), the child has no controlling terminal.
#[tokio::test]
#[cfg(unix)]
async fn test_child_cannot_open_dev_tty() {
// Skip in CI / environments without a controlling terminal.
if std::fs::OpenOptions::new()
.write(true)
.open("/dev/tty")
.is_err()
{
eprintln!("skipping: no controlling terminal");
return;
}
let result = LocalTerminalRunner
.run(make_request(
"(exec 3>/dev/tty && echo ATTACHED || echo DETACHED) 2>/dev/null",
))
.await
.unwrap();
assert_eq!(
result.combined_output.trim(),
"DETACHED",
"child process should not be able to open /dev/tty after detach_from_tty()"
);
}
/// Basic regression: commands still produce output and exit normally.
#[tokio::test]
async fn test_basic_command_output() {
let result = LocalTerminalRunner
.run(make_request("echo hello"))
.await
.unwrap();
assert_eq!(result.combined_output.trim(), "hello");
assert_eq!(result.exit_code, Some(0));
}
}