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,369 @@
//! `x.ai/feedback`, `x.ai/feedback/dismiss`, `x.ai/btw`, and `x.ai/review/*`
//! extension handlers.
//!
//! - `feedback`/`feedback/dismiss`: persist user ratings/text locally and
//! forward to cli-chat-proxy.
//! - `btw`: dispatch a side question to the active session via
//! `SessionCommand::SideQuestion` and return the answer.
//! - `review/comment` and `review/comment/delete`: record inline code review
//! events to cloud storage.
use std::sync::Arc;
use agent_client_protocol as acp;
use tokio::sync::oneshot;
use super::{ExtResult, parse_params};
use crate::agent::MvpAgent;
use crate::session::persistence::{LocalFeedbackEntry, UserFeedbackEntry};
use crate::session::{
ClientFeedbackInput, CommentDeleteRequest, CommentDeleteResponse, CommentRequest,
CommentResponse, FeedbackRequestDismiss, FeedbackResponse, SessionCommand,
};
#[tracing::instrument(skip_all, fields(method = %args.method))]
pub async fn handle(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/btw" => {
tracing::info!("handling /btw side question");
handle_btw(agent, args).await
}
"x.ai/feedback" | "x.ai/feedback/dismiss" => {
tracing::info!("handling user feedback");
handle_feedback(agent, args).await
}
m if m.starts_with("x.ai/review") => {
tracing::info!("handling review comment");
handle_review(agent, args).await
}
_ => Err(acp::Error::method_not_found()),
}
}
/// Handle `x.ai/btw` -- a side question that doesn't interrupt the current turn.
async fn handle_btw(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct BtwRequest {
session_id: String,
question: String,
}
let req: BtwRequest = parse_params(args)?;
let sid: acp::SessionId = req.session_id.clone().into();
let session_handle = {
let sessions = agent.sessions.borrow();
sessions.get(&sid).cloned()
};
let Some(session) = session_handle else {
return Err(
acp::Error::invalid_params().data(format!("session not found: {}", req.session_id))
);
};
let (tx, rx) = oneshot::channel();
let _ = session.cmd_tx.send(SessionCommand::SideQuestion {
question: req.question,
respond_to: tx,
});
let result = rx
.await
.map_err(|_| acp::Error::internal_error().data("session failed to respond"))?;
match result {
Ok(answer) => super::to_ext_response(Ok(serde_json::json!({
"answer": answer,
}))),
Err(e) => Err(acp::Error::internal_error().data(e)),
}
}
async fn handle_feedback(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
if !agent.cfg.borrow().is_feedback_enabled() {
return Err(acp::Error::internal_error().data(
"Feedback is disabled. To enable, set KIGI_FEEDBACK_ENABLED=true or \
[features] feedback = true in config.toml.",
));
}
match args.method.as_ref() {
"x.ai/feedback" => {
// Parse the input -- try the full ClientFeedbackInput first,
// then fall back to the simple FeedbackRequest (from /feedback slash command)
// which only has {session_id, feedback_text} and no client_type.
let feedback_input: ClientFeedbackInput =
match serde_json::from_str::<ClientFeedbackInput>(args.params.get()) {
Ok(input) => input,
Err(_) => {
// Fallback: parse simple FeedbackRequest from /feedback command
let simple: crate::session::FeedbackRequest = parse_params(args)?;
ClientFeedbackInput {
session_id: simple.session_id,
client_type:
prod_mc_cli_chat_proxy_types::feedback_types::ClientType::Tui,
rating_type: None,
rating_value: None,
feedback_text: Some(simple.feedback_text),
feedback_categories: vec![],
context_type: None,
turn_number: None,
request_id: None,
client_version: None,
metadata: None,
terminal_info: None,
}
}
};
let session_id = acp::SessionId::new(feedback_input.session_id.clone());
let session_handle = agent.sessions.borrow().get(&session_id).cloned();
let (model_id, model_metadata) = if let Some(ref session) = session_handle {
let (tx1, rx1) = tokio::sync::oneshot::channel();
let _ = session
.cmd_tx
.send(SessionCommand::GetCurrentModel { responds_to: tx1 });
let model_id = rx1.await.ok();
let model_metadata = session.get_model_metadata().await;
(model_id, model_metadata)
} else {
let sampling_config = agent.sampling_config.borrow().clone();
(Some(sampling_config.model.clone()), Default::default())
};
let turn_number = feedback_input.turn_number.or_else(|| {
agent
.session_turn_number(&session_id)
.map(|t| t.saturating_sub(1) as i64)
});
let mut submission = feedback_input.to_submission(
model_id.clone(),
model_metadata.resolved_model_id,
model_metadata.model_fingerprint,
turn_number,
);
let turn_number = submission.turn_number;
if let Some(user_meta) =
crate::agent::mvp_agent::parse_json_object_env("KIGI_USER_METADATA")
{
submission.merge_metadata(user_meta);
}
// Enrich with session context for Slack notifications (best-effort).
if let Some(ref session_handle) = session_handle {
let (tx, rx) = tokio::sync::oneshot::channel();
let _ = session_handle
.cmd_tx
.send(SessionCommand::GetFeedbackContext {
turn_number,
responds_to: tx,
});
if let Ok(ctx) = rx.await {
submission.tool_outcomes = ctx.tool_outcomes;
submission.session_cwd = Some(ctx.session_cwd);
submission.compaction_count = Some(ctx.compaction_count);
submission.context_window_usage = Some(ctx.context_window_usage);
submission.context_tokens_used = Some(ctx.context_tokens_used);
submission.context_window_tokens = Some(ctx.context_window_tokens);
}
}
// Track rating in session signals
if let (Some(session_handle), Some(rating_value)) =
(&session_handle, feedback_input.rating_value)
{
use prod_mc_cli_chat_proxy_types::feedback_types::RatingType;
let (is_positive, is_negative) = match feedback_input.rating_type {
// Thumbs: -1 = down, 0 = neutral, 1 = up
Some(RatingType::Thumbs) | None => (rating_value > 0, rating_value < 0),
// Stars (1-5): >= 4 positive, <= 2 negative, 3 neutral
Some(RatingType::Stars) => (rating_value >= 4, rating_value <= 2),
// NPS (0-10): 9-10 promoter, 0-6 detractor, 7-8 passive
Some(RatingType::Nps) => (rating_value >= 9, rating_value <= 6),
};
if is_positive {
session_handle.signals_handle.record_positive_rating();
} else if is_negative {
session_handle.signals_handle.record_negative_rating();
}
}
// Log feedback type for debugging
if feedback_input.is_solicited() {
tracing::info!(
session_id = %feedback_input.session_id,
request_id = ?feedback_input.request_id(),
turn_number = ?turn_number,
"Solicited feedback received (response to feedback request)"
);
} else {
tracing::info!(
session_id = %feedback_input.session_id,
turn_number = ?turn_number,
"Spontaneous user feedback received"
);
}
let client = agent.feedback_client();
if client.is_none() {
tracing::warn!(
"no feedback client available (missing proxy credentials); feedback saved locally only"
);
}
let outcome = crate::session::feedback_manager::submit_feedback_workflow(
&mut submission,
client.as_ref(),
session_handle.as_ref().map(|h| &h.persistence_tx),
feedback_input.is_solicited(),
)
.await;
match &outcome {
crate::session::feedback_manager::SubmitOutcome::Submitted => {
tracing::info!("feedback submitted to proxy successfully");
}
crate::session::feedback_manager::SubmitOutcome::LocalOnly => {
tracing::warn!("feedback saved locally only (no proxy client)");
}
crate::session::feedback_manager::SubmitOutcome::Failed(e) => {
tracing::error!(error = %e, "feedback submission to proxy failed");
return Err(acp::Error::internal_error()
.data(format!("Feedback submission failed: {e}")));
}
}
let value = serde_json::to_value(FeedbackResponse { success: true })
.map(|value| serde_json::value::to_raw_value(&value).map(Arc::from))
.expect("to work")
.expect("to work");
Ok(acp::ExtResponse::new(value))
}
"x.ai/feedback/dismiss" => {
let dismiss_input: FeedbackRequestDismiss = parse_params(args)?;
tracing::info!(
session_id = %dismiss_input.session_id,
request_id = %dismiss_input.request_id,
"Feedback request dismissed by user"
);
// Count dismissals too (else event_type is always "responded" and
// response-rate is unknowable).
{
tracing::info_span!(
"feedback.survey",
survey_type = "session",
event_type = "dismissed",
appearance_id = %dismiss_input.request_id,
has_feedback_text = false,
is_solicited = true,
)
.in_scope(|| {});
}
// Persist dismiss locally; flushed before storage CopyFile by the persistence actor.
{
let session_id = acp::SessionId::new(dismiss_input.session_id.clone());
if let Some(session_handle) = agent.sessions.borrow().get(&session_id) {
session_handle.persist_feedback(LocalFeedbackEntry::UserFeedback(
UserFeedbackEntry {
submitted_at: chrono::Utc::now(),
session_id: dismiss_input.session_id.clone(),
turn_number: None,
solicited: true,
request_id: Some(dismiss_input.request_id.clone()),
dismissed: true,
submission: None,
},
));
}
}
let request_id = dismiss_input.request_id.clone();
let client = agent
.feedback_client()
.ok_or_else(|| acp::Error::internal_error().data("No credentials for feedback"))?;
let feedback_base_url = agent.cfg.borrow().endpoints.resolve_feedback_base_url();
match client.dismiss_request(&request_id).await {
Ok(response) => {
tracing::info!(
request_id = %response.request_id,
status = %response.status,
feedback_url = %feedback_base_url,
"Feedback request dismissed"
);
let value = serde_json::to_value(&response)
.map(|value| serde_json::value::to_raw_value(&value).map(Arc::from))
.expect("to work")
.expect("to work");
Ok(acp::ExtResponse::new(value))
}
Err(e) => {
tracing::warn!(
error = %e,
request_id = %request_id,
feedback_url = %feedback_base_url,
"Failed to dismiss feedback request"
);
Err(acp::Error::internal_error()
.data(format!("Failed to dismiss feedback request: {e}")))
}
}
}
_ => Err(acp::Error::method_not_found()),
}
}
/// Record inline code review events.
///
/// Methods:
/// - `x.ai/review/comment`: record a new inline code comment to cloud storage
/// - `x.ai/review/comment/delete`: record a tombstone event for a deleted comment
async fn handle_review(agent: &MvpAgent, args: &acp::ExtRequest) -> ExtResult {
match args.method.as_ref() {
"x.ai/review/comment" => {
let request: CommentRequest = parse_params(args)?;
let comment_id = uuid::Uuid::now_v7().to_string();
tracing::info!(
comment_id = %comment_id,
session_id = %request.session_id,
prompt_index = request.prompt_index,
path = %request.citation.path,
lines = %format!("{}-{}", request.citation.start_line, request.citation.end_line),
"Comment received"
);
let value = serde_json::to_value(CommentResponse {
comment_id,
recorded: true,
})
.map(|value| serde_json::value::to_raw_value(&value).map(Arc::from))
.expect("to work")
.expect("to work");
Ok(acp::ExtResponse::new(value))
}
"x.ai/review/comment/delete" => {
let request: CommentDeleteRequest = parse_params(args)?;
tracing::info!(
comment_id = %request.comment_id,
session_id = %request.session_id,
"Comment delete received"
);
let value = serde_json::to_value(CommentDeleteResponse {
comment_id: request.comment_id,
deleted: true,
})
.map(|value| serde_json::value::to_raw_value(&value).map(Arc::from))
.expect("to work")
.expect("to work");
Ok(acp::ExtResponse::new(value))
}
_ => Err(acp::Error::method_not_found()),
}
}