Initial commit on Forgejo
Some checks failed
CI and releases / Test and vet (push) Failing after 1s
CI and releases / Build release binaries (push) Has been skipped
CI and releases / Publish GitHub release (push) Has been skipped

Fresh repository history for elektrine/tarakan-client hosted at
https://git.elektrine.com/elektrine/tarakan-client.
This commit is contained in:
Maxfield Luke 2026-07-29 04:56:36 -04:00
commit 58b29a7f84
70 changed files with 12994 additions and 0 deletions

View file

@ -0,0 +1,219 @@
package agent
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"sync"
)
// runKimi runs Kimi Code headless in print mode.
//
// With --final-message-only the CLI emits only the assistant answer (best for
// Review Format JSON). When stream-json is available and Progress is set, we
// use streaming so the TUI can show tool activity.
func runKimi(ctx context.Context, provider Provider, request Request) (string, error) {
if provider.Path == "" {
return "", ErrUnavailable
}
prompt := securityPrompt(request.Prompt)
cwd := request.Directory
if cwd == "" {
cwd, _ = os.Getwd()
}
// Prefer stream-json when the UI wants progress; otherwise quiet text.
if request.Progress != nil {
return runKimiStreaming(ctx, provider, prompt, cwd, request.Progress)
}
return runKimiFinal(ctx, provider, prompt, cwd)
}
func runKimiFinal(ctx context.Context, provider Provider, prompt, cwd string) (string, error) {
args := []string{
"--print",
"--prompt", prompt,
"--yolo",
"--final-message-only",
"--work-dir", cwd,
}
command := exec.CommandContext(ctx, provider.Path, args...)
command.Dir = cwd
command.Env = subprocessEnvironment(os.Environ())
output, err := command.CombinedOutput()
if err != nil {
return string(output), fmt.Errorf("%s failed: %w\n%s", provider.Description, err, strings.TrimSpace(string(output)))
}
return strings.TrimSpace(string(output)), nil
}
func runKimiStreaming(ctx context.Context, provider Provider, prompt, cwd string, progress func(string)) (string, error) {
args := []string{
"--print",
"--prompt", prompt,
"--yolo",
"--output-format", "stream-json",
"--work-dir", cwd,
}
command := exec.CommandContext(ctx, provider.Path, args...)
command.Dir = cwd
command.Env = subprocessEnvironment(os.Environ())
stdout, err := command.StdoutPipe()
if err != nil {
return "", err
}
var stderrBuf bytes.Buffer
command.Stderr = &stderrBuf
if err := command.Start(); err != nil {
return "", fmt.Errorf("%s failed to start: %w", provider.Description, err)
}
report := func(line string) {
if progress == nil {
return
}
if line = strings.TrimSpace(line); line != "" {
progress(line)
}
}
var (
textBuf strings.Builder
mu sync.Mutex
final string
)
scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
ev := parseKimiStreamLine(line)
if !ev.ok {
continue
}
if ev.activity != "" {
report(ev.activity)
}
if ev.text != "" {
mu.Lock()
textBuf.WriteString(ev.text)
mu.Unlock()
}
if ev.final != "" {
final = ev.final
}
}
waitErr := command.Wait()
out := strings.TrimSpace(final)
if out == "" {
mu.Lock()
out = strings.TrimSpace(textBuf.String())
mu.Unlock()
}
if waitErr != nil {
detail := strings.TrimSpace(stderrBuf.String())
if detail != "" {
return out, fmt.Errorf("%s failed: %w\n%s", provider.Description, waitErr, detail)
}
return out, fmt.Errorf("%s failed: %w", provider.Description, waitErr)
}
return out, nil
}
type kimiStreamEvent struct {
ok bool
activity string
text string
final string
}
// parseKimiStreamLine maps Kimi print stream-json lines into progress + text.
// The wire format is evolving; we accept common role/type shapes and ignore the rest.
func parseKimiStreamLine(line string) kimiStreamEvent {
line = strings.TrimSpace(line)
if line == "" || !strings.HasPrefix(line, "{") {
return kimiStreamEvent{}
}
var raw map[string]any
if err := json.Unmarshal([]byte(line), &raw); err != nil {
return kimiStreamEvent{}
}
// OpenAI-ish chat chunk: {"choices":[{"delta":{"content":"..."}}]}
if choices, ok := raw["choices"].([]any); ok && len(choices) > 0 {
if choice, ok := choices[0].(map[string]any); ok {
if delta, ok := choice["delta"].(map[string]any); ok {
if content, ok := delta["content"].(string); ok && content != "" {
return kimiStreamEvent{ok: true, text: content}
}
}
if msg, ok := choice["message"].(map[string]any); ok {
if content, ok := msg["content"].(string); ok && content != "" {
return kimiStreamEvent{ok: true, text: content, final: content}
}
}
}
}
// role/content messages
if role, _ := raw["role"].(string); role == "assistant" {
if content, ok := raw["content"].(string); ok && content != "" {
return kimiStreamEvent{ok: true, text: content, final: content}
}
}
// type-tagged events (tool use, assistant text)
typ, _ := raw["type"].(string)
switch typ {
case "assistant", "message", "agent_message", "text":
if content := kimiFirstString(raw, "text", "content", "message"); content != "" {
return kimiStreamEvent{ok: true, text: content, final: content}
}
case "tool_use", "tool_call", "tool":
name := kimiFirstString(raw, "name", "tool", "tool_name")
if name == "" {
if input, ok := raw["input"].(map[string]any); ok {
name = kimiFirstString(input, "name", "command", "path")
}
}
if name != "" {
return kimiStreamEvent{ok: true, activity: "Kimi: " + name}
}
return kimiStreamEvent{ok: true, activity: "Kimi … (tool)"}
case "result", "final", "final_message":
if content := kimiFirstString(raw, "result", "text", "content", "message"); content != "" {
return kimiStreamEvent{ok: true, text: content, final: content}
}
}
// Tool name at top level without type
if name := kimiFirstString(raw, "tool_name", "name"); name != "" && raw["type"] == nil {
if _, isMsg := raw["role"]; !isMsg {
return kimiStreamEvent{ok: true, activity: "Kimi: " + name}
}
}
return kimiStreamEvent{}
}
func kimiFirstString(m map[string]any, keys ...string) string {
for _, key := range keys {
if v, ok := m[key].(string); ok {
if s := strings.TrimSpace(v); s != "" {
return s
}
}
}
return ""
}