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,199 @@
|
||||
use std::path::Path;
|
||||
|
||||
use kigi_workspace::session::git::VcsKind;
|
||||
|
||||
// Re-export from kigi-chat-state — canonical definition lives there.
|
||||
pub use kigi_chat_state::compaction_utils::extract_user_query;
|
||||
|
||||
/// Wraps the user query properly
|
||||
pub fn user_query(user_message: String) -> String {
|
||||
format!(
|
||||
r#"<user_query>
|
||||
{user_message}
|
||||
</user_query>"#
|
||||
)
|
||||
}
|
||||
|
||||
/// Environment info for constructing the `<user_info>` block.
|
||||
///
|
||||
/// When `None`, values are read from the local machine. When `Some`,
|
||||
/// the provided values are used (e.g. from a remote workspace via
|
||||
/// `workspace.info` RPC).
|
||||
pub struct UserInfoOverride {
|
||||
pub os: String,
|
||||
pub shell: String,
|
||||
pub cwd: String,
|
||||
}
|
||||
|
||||
/// Minimal user message prefix for fast-start / headless contexts.
|
||||
///
|
||||
/// Intentionally excludes workspace snapshot and git status.
|
||||
/// When `override_info` is provided, uses remote workspace info instead
|
||||
/// of local machine introspection.
|
||||
pub fn construct_user_message_minimal(
|
||||
working_directory: &Path,
|
||||
override_info: Option<&UserInfoOverride>,
|
||||
) -> String {
|
||||
let local_shell;
|
||||
let (os, shell, cwd) = match override_info {
|
||||
Some(info) => (info.os.as_str(), info.shell.as_str(), info.cwd.clone()),
|
||||
None => {
|
||||
local_shell = resolve_shell_display();
|
||||
(
|
||||
std::env::consts::OS,
|
||||
local_shell.as_str(),
|
||||
working_directory.to_string_lossy().to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
// Local-timezone date, captured when the prefix is built. Re-stamped on
|
||||
// compaction and on resume (build_user_message_prefix), so it stays current
|
||||
// across long sessions.
|
||||
let today = chrono::Local::now().format("%Y-%m-%d");
|
||||
format!(
|
||||
r#"<user_info>
|
||||
OS Version: {os}
|
||||
Shell: {shell}
|
||||
Workspace Path: {cwd}
|
||||
Today's date: {today}
|
||||
Note: Prefer using relative paths over absolute paths as tool call args when possible.
|
||||
</user_info>"#,
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve a display string for the user's shell.
|
||||
///
|
||||
/// Unix: full path from `$SHELL` (e.g. `/bin/zsh`).
|
||||
/// Windows: detected via `detect_windows_shell` cascade
|
||||
/// (pwsh > powershell.exe > Git Bash > cmd.exe).
|
||||
fn resolve_shell_display() -> String {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
kigi_config::shell::detect_windows_shell()
|
||||
.name()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_vcs_status_block(status: &str, vcs_kind: VcsKind) -> String {
|
||||
let (tag, description) = if vcs_kind.is_jj() {
|
||||
(
|
||||
"jj_status",
|
||||
"This is the Jujutsu (jj) status at the start of the conversation. This is a \
|
||||
jj-managed repository \u{2014} use `jj` commands instead of `git`. There is no staging \
|
||||
area; all changes are part of the working-copy commit (@). Use `jj describe` to \
|
||||
set commit messages and `jj new` to finalize changes.",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"git_status",
|
||||
"This is the git status at the start of the conversation. Note that this status \
|
||||
is a snapshot in time, and will not update during the conversation.",
|
||||
)
|
||||
};
|
||||
format!("\n\n<{tag}>\n{description}\n{status}\n</{tag}>\n")
|
||||
}
|
||||
|
||||
/// Compute the VCS status block (without the `<user_info>` wrapper).
|
||||
pub async fn compute_vcs_status_block(
|
||||
working_directory: &Path,
|
||||
vcs_kind: VcsKind,
|
||||
) -> Option<String> {
|
||||
use kigi_workspace::file_system::{git_status, jj_status};
|
||||
|
||||
if matches!(vcs_kind, VcsKind::None) {
|
||||
return None;
|
||||
}
|
||||
let _timer = crate::instrumentation_timer!("session.user_prefix.vcs_status");
|
||||
let timeout = std::time::Duration::from_secs(2);
|
||||
let result = if vcs_kind.is_jj() {
|
||||
tokio::time::timeout(timeout, jj_status(working_directory)).await
|
||||
} else {
|
||||
tokio::time::timeout(timeout, git_status(working_directory)).await
|
||||
};
|
||||
match result {
|
||||
Ok(Ok(status)) => Some(format_vcs_status_block(&status, vcs_kind)),
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!("user prefix VCS status failed: {e}");
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!(vcs = ?vcs_kind, "user prefix VCS status timed out after 2s");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Full user message prefix: `<user_info>` + VCS status.
|
||||
///
|
||||
/// When `override_info` is provided, uses remote workspace info and
|
||||
/// `vcs_status_override` (pre-fetched from the remote workspace) instead
|
||||
/// of local introspection.
|
||||
pub async fn construct_user_message(
|
||||
working_directory: &Path,
|
||||
vcs_kind: VcsKind,
|
||||
override_info: Option<&UserInfoOverride>,
|
||||
vcs_status_override: Option<String>,
|
||||
) -> String {
|
||||
let cwd = working_directory.to_string_lossy().to_string();
|
||||
let vcs_block = if let Some(status) = vcs_status_override {
|
||||
Some(format_vcs_status_block(&status, vcs_kind))
|
||||
} else {
|
||||
let (block, elapsed_ms) = crate::timed!({
|
||||
let mut timer = crate::instrumentation_timer!("session.user_prefix");
|
||||
timer.with_field("cwd", cwd.as_str());
|
||||
compute_vcs_status_block(working_directory, vcs_kind).await
|
||||
});
|
||||
tracing::debug!(elapsed_ms = elapsed_ms as u64, "startup: user_prefix");
|
||||
block
|
||||
};
|
||||
let mut user_info = construct_user_message_minimal(working_directory, override_info);
|
||||
if let Some(vcs) = vcs_block {
|
||||
user_info.push_str(&vcs);
|
||||
}
|
||||
user_info
|
||||
}
|
||||
|
||||
// Tests for extract_user_query now live in kigi_chat_state::compaction_utils.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use kigi_workspace::file_system::FsError;
|
||||
|
||||
/// Verify that construct_user_message completes within the 2s git_status
|
||||
/// timeout even when pointed at a non-existent directory (git commands
|
||||
/// fail instantly → no timeout path exercised, but validates the happy
|
||||
/// path doesn't regress).
|
||||
#[tokio::test]
|
||||
async fn construct_user_message_returns_without_git_status_on_bad_dir() {
|
||||
let dir = std::path::Path::new("/tmp/nonexistent-grok-test-dir");
|
||||
let msg = construct_user_message(dir, VcsKind::Git, None, None).await;
|
||||
// user_info block is always present
|
||||
assert!(
|
||||
msg.contains("<user_info>"),
|
||||
"must contain user_info section"
|
||||
);
|
||||
// git_status block should be absent (git commands fail on non-repo)
|
||||
assert!(
|
||||
!msg.contains("<git_status>"),
|
||||
"must not contain git_status for non-repo directory"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_status_error_omits_block() {
|
||||
// Simulate what construct_user_message does when git_status returns Err
|
||||
let git_status_res: Result<String, FsError> = Err(FsError::Other("timed out".into()));
|
||||
let mut user_info = "<user_info>test</user_info>".to_string();
|
||||
if let Ok(git_status) = git_status_res {
|
||||
user_info = format!("{user_info}\n<git_status>{git_status}</git_status>");
|
||||
}
|
||||
assert!(!user_info.contains("<git_status>"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user