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,194 @@
//! `memory_get` tool — new architecture (`Tool` trait).
use std::sync::Arc;
use super::types::MemoryGetInput;
use crate::types::memory_backend::MemoryBackend;
use crate::types::output::ToolOutput;
use crate::types::tool::{ToolKind, ToolNamespace};
/// Format content with line numbers: `{line_num}→{line}`.
///
/// Extracted as a free function so it can be unit-tested independently of
/// the async tool infrastructure. `first_line_num` is the 1-based number
/// for the first line of `content` (accounts for `from` offset).
///
/// Uses `split('\n')` rather than `lines()` so that content ending with a
/// newline (`"a\n"`) emits a trailing blank numbered line, matching the
/// behavior of the standard `read_file` tool. `lines()` would silently drop
/// that trailing element, causing off-by-one line references for files
/// (virtually all Markdown memory files) that end with a newline.
pub(crate) fn format_with_line_numbers(content: &str, first_line_num: usize) -> String {
if content.is_empty() {
return String::new();
}
content
.split('\n')
.enumerate()
.map(|(i, line)| format!("{}{}", first_line_num + i, line))
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Debug, Default)]
pub struct MemoryGetImpl;
impl crate::types::tool_metadata::ToolMetadata for MemoryGetImpl {
fn kind(&self) -> ToolKind {
ToolKind::MemoryGet
}
fn tool_namespace(&self) -> ToolNamespace {
ToolNamespace::GrokBuild
}
fn description_template(&self) -> &str {
"Read a memory file by path. Returns the file content with line numbers, optionally \
limited to a range of lines.\n\n\
Use after `memory_search` returns a relevant result and you need the full context \
around a snippet, or to read a specific MEMORY.md file in full.\n\n\
Line numbers are 1-based and match the line offsets accepted by the `from` parameter, \
so targeted follow-up reads or edits can reference exact positions."
}
}
impl kigi_tool_runtime::Tool for MemoryGetImpl {
type Args = MemoryGetInput;
type Output = ToolOutput;
fn id(&self) -> kigi_tool_protocol::ToolId {
kigi_tool_protocol::ToolId::new("memory_get").expect("valid tool id")
}
fn description(
&self,
_ctx: &::kigi_tool_runtime::ListToolsContext,
) -> kigi_tool_types::ToolDescription {
kigi_tool_types::ToolDescription::new(
"memory_get",
crate::types::tool_metadata::ToolMetadata::description_template(self),
)
}
fn capabilities(&self) -> kigi_tool_protocol::ToolCapabilities {
kigi_tool_protocol::ToolCapabilities {
is_read_only: true,
tool_scope: Some(kigi_tool_protocol::ToolScope::Read),
..Default::default()
}
}
async fn run(
&self,
ctx: kigi_tool_runtime::ToolCallContext,
input: MemoryGetInput,
) -> Result<ToolOutput, kigi_tool_runtime::ToolError> {
use crate::types::tool_metadata::shared_resources;
let resources = shared_resources(&ctx)?;
let Some(memory) = resources
.lock()
.await
.get::<Arc<dyn MemoryBackend>>()
.cloned()
else {
return Ok(ToolOutput::Text(
"Memory is not enabled. Use --experimental-memory to enable.".into(),
));
};
let memory = memory.clone();
tracing::info!(target: crate::types::memory_backend::MEMORY_LOG_TARGET,"MEMORY_GET: invoked");
let content = memory
.get(&input.path, input.from, input.lines)
.map_err(|e| {
kigi_tool_runtime::ToolError::execution(
kigi_tool_protocol::ToolId::new("memory_get").expect("valid"),
format!("memory get failed: {e}"),
)
})?;
let total_lines = content.lines().count();
let first_line_num = input.from.unwrap_or(0) + 1;
let numbered = format_with_line_numbers(&content, first_line_num);
let output = format!(
"**File:** {}\n**Lines:** {} (from: {}, limit: {})\n\n{}",
input.path,
total_lines,
input.from.map_or("start".to_string(), |f| f.to_string()),
input.lines.map_or("all".to_string(), |l| l.to_string()),
numbered,
);
Ok(ToolOutput::Text(output.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// format_with_line_numbers produces 1-based unpadded output.
#[test]
fn test_format_basic_line_numbers() {
let out = format_with_line_numbers("alpha\nbeta\ngamma", 1);
assert_eq!(out, "1→alpha\n2→beta\n3→gamma");
}
/// The `from` offset shifts the first line number so numbers reflect the
/// actual position in the source file, not the slice position.
#[test]
fn test_format_offset_adjusts_line_numbers() {
// Simulates memory_get called with from=4 (0-based) — first displayed
// line should be labelled "5" (1-based).
let out = format_with_line_numbers("line five\nline six", 5);
assert!(out.starts_with("5→line five"), "got: {out}");
assert!(out.ends_with("6→line six"), "got: {out}");
}
/// Empty content produces empty output (no panic).
#[test]
fn test_format_empty_content() {
let out = format_with_line_numbers("", 1);
assert!(out.is_empty(), "empty input must produce empty output");
}
/// Single-line content produces one numbered line.
#[test]
fn test_format_single_line() {
let out = format_with_line_numbers("only line", 1);
assert_eq!(out, "1→only line");
}
/// Wide line numbers (>= 7 digits) are not truncated.
#[test]
fn test_format_large_line_numbers() {
let out = format_with_line_numbers("x", 1_000_000);
assert!(out.starts_with("1000000→"), "got: {out}");
}
/// Content ending with `\n` emits a trailing blank numbered line.
///
/// Regression test for the `lines()` vs `split('\n')` difference.
/// Virtually all Markdown memory files end with a trailing newline, so
/// without this fix `memory_get` line numbers are off-by-one relative to
/// `read_file` for any file that ends with a newline.
#[test]
fn test_format_trailing_newline_emits_blank_line() {
let out = format_with_line_numbers("alpha\n", 1);
assert_eq!(
out, "1→alpha\n2→",
"trailing newline must produce a numbered blank final line"
);
}
/// Two trailing newlines produce two extra blank lines.
#[test]
fn test_format_double_trailing_newline() {
let out = format_with_line_numbers("a\n\n", 1);
assert_eq!(out, "1→a\n2→\n3→");
}
/// Content without a trailing newline does NOT produce a spurious blank line.
#[test]
fn test_format_no_trailing_newline_no_blank_line() {
let out = format_with_line_numbers("alpha", 1);
assert_eq!(out, "1→alpha", "no trailing newline → no extra line");
}
}
@@ -0,0 +1,43 @@
//! Memory tools for cross-session knowledge retrieval.
//!
//! - `memory_search` — search indexed memory for relevant chunks
//! - `memory_get` — read a specific memory file by path
pub mod get_tool;
pub mod search_tool;
pub mod types;
pub use get_tool::MemoryGetImpl;
pub use search_tool::MemorySearchImpl;
/// Registered name of the `memory_search` tool.
///
/// Single source of truth shared between the tool definition and any
/// gating callers (e.g. shell-side slash-command availability checks).
pub const MEMORY_SEARCH_TOOL_NAME: &str = "memory_search";
/// Registered name of the `memory_get` tool.
pub const MEMORY_GET_TOOL_NAME: &str = "memory_get";
#[cfg(test)]
mod tests {
use super::*;
/// The constants are the wire identifier embedded in
/// `AvailableCommandsUpdate._meta.tools` and matched by the shell's
/// memory-gate predicate. A typo in either site silently disables
/// `/flush` and `/dream`. Pin both halves.
#[test]
fn memory_tool_constants_match_registered_ids() {
assert_eq!(MEMORY_SEARCH_TOOL_NAME, "memory_search");
assert_eq!(MEMORY_GET_TOOL_NAME, "memory_get");
assert_eq!(
kigi_tool_runtime::Tool::id(&MemorySearchImpl).to_string(),
MEMORY_SEARCH_TOOL_NAME
);
assert_eq!(
kigi_tool_runtime::Tool::id(&MemoryGetImpl).to_string(),
MEMORY_GET_TOOL_NAME
);
}
}
@@ -0,0 +1,109 @@
//! `memory_search` tool — new architecture (`Tool` trait).
use std::sync::Arc;
use super::types::MemorySearchInput;
use crate::types::memory_backend::{MemoryBackend, format_staleness_note};
use crate::types::output::ToolOutput;
use crate::types::tool::{ToolKind, ToolNamespace};
#[derive(Debug, Default)]
pub struct MemorySearchImpl;
impl crate::types::tool_metadata::ToolMetadata for MemorySearchImpl {
fn kind(&self) -> ToolKind {
ToolKind::MemorySearch
}
fn tool_namespace(&self) -> ToolNamespace {
ToolNamespace::GrokBuild
}
fn description_template(&self) -> &str {
"Search cross-session memory for relevant knowledge chunks. Returns ranked results \
from global, workspace, and session memory files.\n\n\
Use this proactively when:\n\
- A question references prior work, decisions, or context you don't have\n\
- You need project conventions, coding patterns, or user preferences\n\
- The user mentions something discussed or decided in a previous session\n\
- Starting work in an unfamiliar part of the codebase\n\
- After compaction when prior context may have been lost"
}
}
impl kigi_tool_runtime::Tool for MemorySearchImpl {
type Args = MemorySearchInput;
type Output = ToolOutput;
fn id(&self) -> kigi_tool_protocol::ToolId {
kigi_tool_protocol::ToolId::new("memory_search").expect("valid tool id")
}
fn description(
&self,
_ctx: &::kigi_tool_runtime::ListToolsContext,
) -> kigi_tool_types::ToolDescription {
kigi_tool_types::ToolDescription::new(
"memory_search",
crate::types::tool_metadata::ToolMetadata::description_template(self),
)
}
fn capabilities(&self) -> kigi_tool_protocol::ToolCapabilities {
kigi_tool_protocol::ToolCapabilities {
is_read_only: true,
tool_scope: Some(kigi_tool_protocol::ToolScope::Read),
..Default::default()
}
}
async fn run(
&self,
ctx: kigi_tool_runtime::ToolCallContext,
input: MemorySearchInput,
) -> Result<ToolOutput, kigi_tool_runtime::ToolError> {
use crate::types::tool_metadata::shared_resources;
let resources = shared_resources(&ctx)?;
let Some(memory) = resources
.lock()
.await
.get::<Arc<dyn MemoryBackend>>()
.cloned()
else {
return Ok(ToolOutput::Text(
"Memory is not enabled. Use --experimental-memory to enable.".into(),
));
};
let max_results = input
.max_results
.unwrap_or_else(|| memory.default_search_max_results());
let min_score = input
.min_score
.unwrap_or_else(|| memory.default_search_min_score());
tracing::info!(target: crate::types::memory_backend::MEMORY_LOG_TARGET, max_results, "MEMORY_SEARCH: invoked");
let results = memory
.search(&input.query, max_results, min_score)
.await
.map_err(|e| {
kigi_tool_runtime::ToolError::execution(
kigi_tool_protocol::ToolId::new("memory_search").expect("valid"),
format!("memory search failed: {e}"),
)
})?;
tracing::info!(target: crate::types::memory_backend::MEMORY_LOG_TARGET, results = results.len(), "MEMORY_SEARCH: complete");
if results.is_empty() {
return Ok(ToolOutput::Text(
"No memory results found for query.".into(),
));
}
let mut output = format!("Found {} memory result(s):\n", results.len());
for (i, r) in results.iter().enumerate() {
let staleness = format_staleness_note(&r.source, r.created_at);
output.push_str(&format!(
"\n### Result {} (score: {:.2}, source: {})\n**File:** {} (lines {}-{})\n{}```\n{}\n```\n",
i + 1, r.score, r.source, r.path, r.start_line, r.end_line, staleness, r.snippet,
));
}
Ok(ToolOutput::Text(output.into()))
}
}
@@ -0,0 +1,53 @@
//! Input/output types for memory tools.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Input for the `memory_search` tool.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct MemorySearchInput {
/// The search query string. Use specific technical terms rather than
/// conversational language. Good: "authentication middleware patterns".
/// Bad: "that thing we discussed about auth".
pub query: String,
/// Maximum number of results to return.
///
/// When omitted the backend-configured value is used (typically 6 from
/// `[memory.search].max_results`), so leaving this unset is preferred
/// for normal queries.
#[serde(default)]
pub max_results: Option<usize>,
/// Minimum relevance score threshold.
///
/// When omitted the backend-configured value is used (typically 0.0 from
/// `[memory.search].min_score`).
#[serde(default)]
pub min_score: Option<f64>,
}
/// Output schema for `memory_search` (used for JSON Schema generation only).
#[derive(Debug, JsonSchema)]
pub struct MemorySearchOutput {
/// Formatted search results as markdown text.
pub results: String,
}
/// Input for the `memory_get` tool.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct MemoryGetInput {
/// Path to the memory file to read.
pub path: String,
/// 0-based start line (default: beginning of file).
#[serde(default)]
pub from: Option<usize>,
/// Maximum number of lines to return (default: all).
#[serde(default)]
pub lines: Option<usize>,
}
/// Output schema for `memory_get` (used for JSON Schema generation only).
#[derive(Debug, JsonSchema)]
pub struct MemoryGetOutput {
/// File content (optionally line-limited).
pub content: String,
}