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,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,
|
||||
}
|
||||
Reference in New Issue
Block a user