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,234 @@
|
||||
//! `grok wrap` — run any command in a local PTY that forwards its clipboard.
|
||||
//!
|
||||
//! Generalizes the `grok ssh` wrapper: spawns an arbitrary command inside a
|
||||
//! local pseudo-terminal, intercepts OSC 52 clipboard escape sequences from
|
||||
//! its output, and writes their payload to the local system clipboard. Useful
|
||||
//! for containerized or remote shells (`docker exec`, `kubectl exec`, ...)
|
||||
//! whose clipboard cannot otherwise reach the user — especially in terminals
|
||||
//! that do not handle OSC 52 themselves (for example Apple Terminal).
|
||||
//!
|
||||
//! Resolvable programs spawn directly. On Unix, commands a direct spawn cannot
|
||||
//! run — a single shell-quoted string (`grok wrap "mycli ssh host"`) or a shell
|
||||
//! alias — are handed to `$SHELL -i -c` instead, so the user's own shell does
|
||||
//! the word-splitting and alias expansion. The exec fallback (a non-TTY
|
||||
//! session, or PTY setup failure) keeps the same route but drops `-i` to
|
||||
//! avoid job-control noise without our PTY.
|
||||
//!
|
||||
//! The PTY/OSC 52/resize engine lives in [`crate::pty_wrap::run_wrapped_command`].
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::app::WrapArgs;
|
||||
|
||||
/// Run the `grok wrap` command.
|
||||
///
|
||||
/// On Unix interactive sessions the command runs inside a local PTY so its
|
||||
/// OSC 52 clipboard sequences can be intercepted and written to the local
|
||||
/// clipboard. Otherwise the command is executed directly (no wrapping).
|
||||
pub fn run(args: &WrapArgs) -> Result<()> {
|
||||
// `command` is `required` in clap, so it always has at least one element.
|
||||
let program = args
|
||||
.command
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("grok wrap: no command given"))?;
|
||||
|
||||
// Unix: derive both spawn plans up front from one env snapshot so the PTY
|
||||
// attempt and its fallback route consistently. The wrapped run uses
|
||||
// `$SHELL -i` when routing through the shell (rc files load, aliases
|
||||
// expand — safe because it runs inside our PTY); the exec fallback drops
|
||||
// `-i` because an interactive shell without our PTY risks job-control
|
||||
// noise.
|
||||
#[cfg(unix)]
|
||||
let (wrapped, fallback) = {
|
||||
let shell = user_shell();
|
||||
let in_path = kigi_config::shell::is_command_available(program);
|
||||
(
|
||||
derive_spawn(&args.command, &shell, in_path, ShellMode::Interactive),
|
||||
derive_spawn(&args.command, &shell, in_path, ShellMode::Plain),
|
||||
)
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let (wrapped, fallback) = {
|
||||
let direct = SpawnPlan {
|
||||
program: program.clone(),
|
||||
args: args.command[1..].to_vec(),
|
||||
};
|
||||
(direct.clone(), direct)
|
||||
};
|
||||
|
||||
if should_wrap() {
|
||||
match crate::pty_wrap::run_wrapped_command(&wrapped.program, &wrapped.args) {
|
||||
Ok(code) => std::process::exit(code),
|
||||
Err(e) => {
|
||||
// PTY setup failed; keep the chosen route without our PTY so
|
||||
// the command still works (just without clipboard forwarding).
|
||||
eprintln!("grok wrap: wrapped mode failed, running without PTY wrapping: {e}");
|
||||
exec_command(&fallback.program, &fallback.args)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
exec_command(&fallback.program, &fallback.args)
|
||||
}
|
||||
}
|
||||
|
||||
/// The program and argv `grok wrap` will actually spawn.
|
||||
#[derive(Clone)]
|
||||
struct SpawnPlan {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
/// Whether a shell-routed command runs `$SHELL -i -c` or plain `$SHELL -c`.
|
||||
#[cfg(unix)]
|
||||
#[derive(Clone, Copy)]
|
||||
enum ShellMode {
|
||||
/// PTY-wrapped run: rc files source and aliases expand.
|
||||
Interactive,
|
||||
/// Exec fallback without our PTY: plain `-c` avoids job-control noise.
|
||||
Plain,
|
||||
}
|
||||
|
||||
/// Decide how to launch the wrapped command: direct spawn, or via the user's
|
||||
/// shell when a direct spawn cannot work.
|
||||
///
|
||||
/// Pure so it is testable without the real environment: `program_in_path` is
|
||||
/// the caller's `PATH` lookup result for `command[0]`.
|
||||
#[cfg(unix)]
|
||||
fn derive_spawn(
|
||||
command: &[String],
|
||||
shell: &str,
|
||||
program_in_path: bool,
|
||||
mode: ShellMode,
|
||||
) -> SpawnPlan {
|
||||
let via_shell = |cmdline: String| {
|
||||
let args = match mode {
|
||||
ShellMode::Interactive => vec!["-i".to_string(), "-c".to_string(), cmdline],
|
||||
ShellMode::Plain => vec!["-c".to_string(), cmdline],
|
||||
};
|
||||
SpawnPlan {
|
||||
program: shell.to_string(),
|
||||
args,
|
||||
}
|
||||
};
|
||||
|
||||
// A single argument containing whitespace is a shell-quoted command line
|
||||
// (`grok wrap "mycli ssh host"`), not a program name: hand it to the shell
|
||||
// verbatim so it does word-splitting, alias expansion, pipes, etc.
|
||||
if command.len() == 1 && command[0].contains(char::is_whitespace) {
|
||||
return via_shell(command[0].clone());
|
||||
}
|
||||
|
||||
// A bare program name that PATH cannot resolve is usually a shell alias
|
||||
// (`alias mycli=remote`); only a shell can expand it. Explicit paths
|
||||
// (containing `/`) spawn directly so their errors stay precise, as do
|
||||
// empty and whitespace-containing first words — neither can be an alias
|
||||
// name, and an empty one (`grok wrap "$PROG" ...` with `$PROG` unset)
|
||||
// must keep failing fast instead of silently running the tail.
|
||||
if !command[0].is_empty()
|
||||
&& !command[0].contains('/')
|
||||
&& !command[0].contains(char::is_whitespace)
|
||||
&& !program_in_path
|
||||
{
|
||||
return via_shell(join_command_line(command));
|
||||
}
|
||||
|
||||
SpawnPlan {
|
||||
program: command[0].clone(),
|
||||
args: command[1..].to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Join argv back into one shell command line, leaving the first word bare:
|
||||
/// shells only expand aliases on unquoted words, so quoting it would break
|
||||
/// the very case this route exists for. Every following word is quoted.
|
||||
#[cfg(unix)]
|
||||
fn join_command_line(command: &[String]) -> String {
|
||||
let mut line = command[0].clone();
|
||||
for word in &command[1..] {
|
||||
line.push(' ');
|
||||
line.push_str("e_word(word));
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
/// Single-quote `word` for a POSIX shell. Always quotes: a hand-maintained
|
||||
/// bare-word safe set rots per shell (zsh alone expands bare `=word`), and
|
||||
/// only the shell ever reads the composed line.
|
||||
#[cfg(unix)]
|
||||
fn quote_word(word: &str) -> String {
|
||||
// Single quotes are literal in POSIX shells except `'` itself, which must
|
||||
// close the quote, escape, and reopen: `'` → `'\''`.
|
||||
format!("'{}'", word.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
/// The user's shell from `$SHELL` when it points at an existing file,
|
||||
/// otherwise `/bin/sh`. `$SHELL` is preferred because the user typed the
|
||||
/// wrapped command in their own shell's dialect (aliases live there);
|
||||
/// deliberately not `kigi_config::shell::unix_shell_path`, which coerces
|
||||
/// to bash/zsh and would drop fish/other-shell aliases.
|
||||
#[cfg(unix)]
|
||||
fn user_shell() -> String {
|
||||
resolve_shell(std::env::var("SHELL").ok().as_deref())
|
||||
}
|
||||
|
||||
/// Pure core of [`user_shell`], split from the env read so tests never touch
|
||||
/// the process environment. `shell` is the raw `$SHELL` value, if any.
|
||||
#[cfg(unix)]
|
||||
fn resolve_shell(shell: Option<&str>) -> String {
|
||||
match shell {
|
||||
Some(s) if !s.is_empty() && std::path::Path::new(s).is_file() => s.to_string(),
|
||||
_ => "/bin/sh".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when the command should be wrapped in a local PTY.
|
||||
///
|
||||
/// Unlike `grok ssh`, `grok wrap` does not gate on the terminal brand: the user
|
||||
/// has explicitly asked to forward the clipboard, and interception works
|
||||
/// regardless of whether the outer terminal supports OSC 52 (the payload is
|
||||
/// written to the local clipboard directly).
|
||||
///
|
||||
/// It requires a platform `portable-pty` can drive — Unix (`openpty`) or
|
||||
/// Windows (ConPTY) — and an interactive (TTY) session. Wrapping a
|
||||
/// non-interactive pipe would make the child think it has a terminal and has no
|
||||
/// clipboard destination anyway. On Windows the OSC 52 → clipboard bridge works
|
||||
/// the same; only the live outer→inner resize is not forwarded (see
|
||||
/// [`crate::pty_wrap::run_wrapped_command`]).
|
||||
fn should_wrap() -> bool {
|
||||
// PTY wrapping requires native pseudo-terminal APIs (Unix openpty / Windows
|
||||
// ConPTY), both of which `portable-pty` supports.
|
||||
if !cfg!(any(unix, windows)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
use std::io::IsTerminal;
|
||||
std::io::stdin().is_terminal()
|
||||
&& std::io::stdout().is_terminal()
|
||||
&& std::io::stderr().is_terminal()
|
||||
}
|
||||
|
||||
/// Replace the current process with `program <args...>` (no PTY wrapping).
|
||||
#[cfg(unix)]
|
||||
fn exec_command(program: &str, args: &[String]) -> Result<()> {
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
let err = std::process::Command::new(program).args(args).exec();
|
||||
|
||||
// exec() only returns on error.
|
||||
Err(anyhow::anyhow!("failed to exec {program}: {err}"))
|
||||
}
|
||||
|
||||
/// On non-Unix platforms, spawn and wait.
|
||||
#[cfg(not(unix))]
|
||||
fn exec_command(program: &str, args: &[String]) -> Result<()> {
|
||||
let status = std::process::Command::new(program)
|
||||
.args(args)
|
||||
.status()
|
||||
.map_err(|e| anyhow::anyhow!("failed to run {program}: {e}"))?;
|
||||
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
#[path = "wrap_cmd_tests.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user