Skip to content

Commit a4c0aee

Browse files
committed
v0.6.0: interactive REPL mode
Add 'kode repl' — a zero-dependency interactive Read-Eval-Print Loop that provides a persistent session chat experience without any external TUI or readline libraries. Usage: kode repl # Start a fresh interactive session kode repl --id <id> # Resume an existing session Features: - Auto-saves session after every turn - Full agent loop (thinking, tool calls, results) visible between turns - Slash commands: /exit, /quit, /help, /info - Resume any session with kode repl --id <id> - Zero external dependencies — pure stdlib (bufio.Scanner) Implementation: ~130 lines in cmd/kode/repl.go, reuses existing kode.New() + RunWithMessages() + session Store.
1 parent 305d55f commit a4c0aee

4 files changed

Lines changed: 194 additions & 0 deletions

File tree

cmd/kode/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ func main() {
100100
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
101101
os.Exit(1)
102102
}
103+
case "repl":
104+
if err := replCmd(os.Args[2:]); err != nil {
105+
fmt.Fprintf(os.Stderr, "kode: %v\n", err)
106+
os.Exit(1)
107+
}
103108
default:
104109
fmt.Fprintf(os.Stderr, "kode: unknown command %q\n", os.Args[1])
105110
printUsage()
@@ -215,13 +220,15 @@ func printUsage() {
215220
kode run --session [flags] <task>
216221
kode continue [--id <id>] <task>
217222
kode session <list|show [id]|trim <id> <n>|delete <id>|cleanup <days>>
223+
kode repl [--id <id>]
218224
kode init [--global | -g] [--force | -f]
219225
kode version
220226
221227
Commands:
222228
run Execute a task with the agent loop
223229
run --session Execute and save conversation as a session
224230
continue Continue the most recent session (or by --id)
231+
repl Interactive REPL mode (multi-turn session)
225232
session Manage sessions: list, show, delete, trim, cleanup
226233
init Create a config file (default: ./kode.json)
227234
version Print version and exit

cmd/kode/main_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ func TestPrintUsage(t *testing.T) {
224224
required := []string{
225225
"kode run",
226226
"kode continue",
227+
"kode repl",
227228
"kode session",
228229
"trim",
229230
"cleanup",

cmd/kode/repl.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"os"
8+
"os/signal"
9+
"strings"
10+
11+
"github.com/BackendStack21/kode"
12+
"github.com/BackendStack21/kode/internal/config"
13+
"github.com/BackendStack21/kode/internal/llm"
14+
"github.com/BackendStack21/kode/internal/render"
15+
"github.com/BackendStack21/kode/internal/session"
16+
)
17+
18+
// ── REPL ──────────────────────────────────────────────────────────────
19+
20+
// replCmd handles `kode repl [--id <id>]`.
21+
// It starts (or resumes) an interactive multi-turn session.
22+
func replCmd(args []string) error {
23+
sessionID := ""
24+
for i := 0; i < len(args)-1 && args[i] == "--id"; {
25+
sessionID = args[i+1]
26+
i += 2
27+
}
28+
29+
store, err := session.NewStore()
30+
if err != nil {
31+
return fmt.Errorf("session store: %w", err)
32+
}
33+
34+
// Resolve config
35+
resolved := config.LoadConfig(config.CLIFlags{})
36+
systemMessage := resolved.System
37+
if systemMessage == "" {
38+
systemMessage = defaultSystem
39+
}
40+
41+
// Build tools
42+
tools := builtinTools()
43+
var sandboxCleanup func() error
44+
45+
if resolved.Sandbox {
46+
sbCfg := sandboxConfig{
47+
Image: resolved.SandboxImage,
48+
Network: resolved.SandboxNetwork,
49+
Readonly: resolved.SandboxReadonly,
50+
Memory: resolved.SandboxMemory,
51+
CPUs: resolved.SandboxCPUs,
52+
User: resolved.SandboxUser,
53+
Env: resolved.SandboxEnv,
54+
Volumes: resolved.SandboxVolumes,
55+
}
56+
cleanup, err := setupSandbox(tools, sbCfg)
57+
if err != nil {
58+
return fmt.Errorf("sandbox: %w", err)
59+
}
60+
sandboxCleanup = cleanup
61+
}
62+
63+
// Renderer
64+
modelLabel := kode.ProfileLabel(resolved.Model)
65+
if modelLabel == "" {
66+
modelLabel = "deepseek-chat"
67+
}
68+
color := !resolved.NoColor && render.ColorEnabled()
69+
rend := render.New(os.Stderr, color).WithModel(modelLabel)
70+
71+
agent, err := kode.New(kode.Config{
72+
Model: resolved.Model,
73+
BaseURL: resolved.BaseURL,
74+
APIKey: resolved.APIKey,
75+
MaxIterations: resolved.MaxIter,
76+
SystemMessage: systemMessage,
77+
NoProjectFile: resolved.NoAgents,
78+
Thinking: resolved.Thinking,
79+
Tools: tools,
80+
SandboxCleanup: sandboxCleanup,
81+
Renderer: rend,
82+
})
83+
if err != nil {
84+
return err
85+
}
86+
defer agent.Close()
87+
88+
// Load or create session
89+
var sess *session.Session
90+
if sessionID != "" {
91+
sess, err = store.Load(sessionID)
92+
if err != nil {
93+
return fmt.Errorf("load session %q: %w", sessionID, err)
94+
}
95+
} else {
96+
// Start fresh session with just the system message
97+
sess, err = store.Create(
98+
[]llm.Message{{Role: "system", Content: systemMessage}},
99+
resolved.Model,
100+
"interactive session",
101+
)
102+
if err != nil {
103+
return fmt.Errorf("create session: %w", err)
104+
}
105+
}
106+
107+
fmt.Fprintf(os.Stderr, "\nkode ⚡ %s · session %s\n\n", modelLabel, sess.ID)
108+
fmt.Fprintf(os.Stderr, " Type /help for commands, /exit to quit.\n\n")
109+
110+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
111+
defer cancel()
112+
113+
scanner := bufio.NewScanner(os.Stdin)
114+
turn := 0
115+
for {
116+
fmt.Fprintf(os.Stderr, "─── Turn %d ───\n", turn+1)
117+
fmt.Fprint(os.Stderr, "> ")
118+
119+
if !scanner.Scan() {
120+
break
121+
}
122+
input := strings.TrimSpace(scanner.Text())
123+
124+
if input == "" {
125+
continue
126+
}
127+
128+
if strings.HasPrefix(input, "/") {
129+
if handleREPLCommand(input, sess) {
130+
break
131+
}
132+
continue
133+
}
134+
135+
// Build message history: session messages + new user input
136+
messages := sess.GetMessages()
137+
origLen := len(messages)
138+
messages = append(messages, llm.Message{Role: "user", Content: input})
139+
140+
// Run agent with full history
141+
rend.Start(input)
142+
_, allMessages, err := agent.RunWithMessages(ctx, messages)
143+
if err != nil {
144+
fmt.Fprintf(os.Stderr, "kode: agent error: %v\n", err)
145+
continue
146+
}
147+
148+
// Save new messages to session
149+
newMsgs := allMessages[origLen:]
150+
if err := store.Append(sess.ID, newMsgs); err != nil {
151+
fmt.Fprintf(os.Stderr, "kode: save error: %v\n", err)
152+
}
153+
154+
// Reload session to get updated turn count
155+
sess, _ = store.Load(sess.ID)
156+
turn++
157+
158+
fmt.Fprintln(os.Stderr)
159+
}
160+
return nil
161+
}
162+
163+
// handleREPLCommand processes a REPL slash command.
164+
// Returns true if the session should exit.
165+
func handleREPLCommand(input string, sess *session.Session) bool {
166+
switch strings.ToLower(input) {
167+
case "/exit", "/quit":
168+
fmt.Fprintf(os.Stderr, "Session %s saved. Continue later with: kode repl --id %s\n", sess.ID, sess.ID)
169+
return true
170+
case "/help":
171+
fmt.Fprint(os.Stderr, `Commands:
172+
/exit, /quit Exit REPL (session is saved)
173+
/help Show this help
174+
/info Show session info
175+
176+
`)
177+
case "/info":
178+
fmt.Fprintf(os.Stderr, "Session: %s\n", sess.ID)
179+
fmt.Fprintf(os.Stderr, "Model: %s\n", sess.Model)
180+
fmt.Fprintf(os.Stderr, "Turns: %d\n", sess.Turns)
181+
default:
182+
fmt.Fprintf(os.Stderr, "Unknown command: %s (/help for commands)\n", input)
183+
}
184+
return false
185+
}

docs/CLI.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
| `kode run [flags] <task>` | Execute a task with the agent loop (single-shot by default) |
88
| `kode run --session [flags] <task>` | Execute and save conversation as a multi-turn session |
99
| `kode continue [--id <id>] <task>` | Continue the most recent session (or by `--id`) |
10+
| `kode repl [--id <id>]` | Interactive REPL mode (persistent multi-turn session) |
1011
| `kode session list` | List sessions |
1112
| `kode session show [id]` | Show session details (default: latest) |
1213
| `kode session delete <id>` | Delete a session |

0 commit comments

Comments
 (0)