codemaxxing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,659 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useState, useEffect, useCallback } from "react";
4
+ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
5
+ import { EventEmitter } from "events";
6
+ import TextInput from "ink-text-input";
7
+ import { CodingAgent } from "./agent.js";
8
+ import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
9
+ import { listSessions, getSession, loadMessages } from "./utils/sessions.js";
10
+ import { execSync } from "child_process";
11
+ import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
12
+ const VERSION = "0.1.0";
13
+ // ── Helpers ──
14
+ function formatTimeAgo(date) {
15
+ const secs = Math.floor((Date.now() - date.getTime()) / 1000);
16
+ if (secs < 60)
17
+ return `${secs}s ago`;
18
+ const mins = Math.floor(secs / 60);
19
+ if (mins < 60)
20
+ return `${mins}m ago`;
21
+ const hours = Math.floor(mins / 60);
22
+ if (hours < 24)
23
+ return `${hours}h ago`;
24
+ const days = Math.floor(hours / 24);
25
+ return `${days}d ago`;
26
+ }
27
+ // ── Slash Commands ──
28
+ const SLASH_COMMANDS = [
29
+ { cmd: "/help", desc: "show commands" },
30
+ { cmd: "/map", desc: "show repository map" },
31
+ { cmd: "/reset", desc: "clear conversation" },
32
+ { cmd: "/context", desc: "show message count" },
33
+ { cmd: "/diff", desc: "show git changes" },
34
+ { cmd: "/undo", desc: "revert last codemaxxing commit" },
35
+ { cmd: "/commit", desc: "commit all changes" },
36
+ { cmd: "/push", desc: "push to remote" },
37
+ { cmd: "/git on", desc: "enable auto-commits" },
38
+ { cmd: "/git off", desc: "disable auto-commits" },
39
+ { cmd: "/models", desc: "list available models" },
40
+ { cmd: "/model", desc: "switch model mid-session" },
41
+ { cmd: "/sessions", desc: "list past sessions" },
42
+ { cmd: "/resume", desc: "resume a past session" },
43
+ { cmd: "/quit", desc: "exit" },
44
+ ];
45
+ const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
46
+ const SPINNER_MESSAGES = [
47
+ "Locking in...", "Cooking...", "Maxxing...", "In the zone...",
48
+ "Yapping...", "Frame mogging...", "Jester gooning...", "Gooning...",
49
+ "Doing back flips...", "Jester maxxing...", "Getting baked...",
50
+ "Blasting tren...", "Pumping...", "Wondering if I should actually do this...",
51
+ "Hacking the main frame...", "Codemaxxing...", "Vibe coding...", "Running a marathon...",
52
+ ];
53
+ // ── Neon Spinner ──
54
+ function NeonSpinner({ message }) {
55
+ const [frame, setFrame] = useState(0);
56
+ const [elapsed, setElapsed] = useState(0);
57
+ useEffect(() => {
58
+ const start = Date.now();
59
+ const interval = setInterval(() => {
60
+ setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
61
+ setElapsed(Math.floor((Date.now() - start) / 1000));
62
+ }, 80);
63
+ return () => clearInterval(interval);
64
+ }, []);
65
+ return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "#00FFFF", children: SPINNER_FRAMES[frame] }), " ", _jsx(Text, { bold: true, color: "#FF00FF", children: message }), " ", _jsxs(Text, { color: "#008B8B", children: ["[", elapsed, "s]"] })] }));
66
+ }
67
+ // ── Streaming Indicator (subtle, shows model is still working) ──
68
+ const STREAM_DOTS = ["· ", "·· ", "···", " ··", " ·", " "];
69
+ function StreamingIndicator() {
70
+ const [frame, setFrame] = useState(0);
71
+ useEffect(() => {
72
+ const interval = setInterval(() => {
73
+ setFrame((f) => (f + 1) % STREAM_DOTS.length);
74
+ }, 300);
75
+ return () => clearInterval(interval);
76
+ }, []);
77
+ return (_jsxs(Text, { dimColor: true, children: [" ", _jsx(Text, { color: "#008B8B", children: STREAM_DOTS[frame] }), " ", _jsx(Text, { color: "#008B8B", children: "streaming" })] }));
78
+ }
79
+ let msgId = 0;
80
+ // ── Main App ──
81
+ function App() {
82
+ const { exit } = useApp();
83
+ const { stdout } = useStdout();
84
+ const termWidth = stdout?.columns ?? 80;
85
+ const [input, setInput] = useState("");
86
+ const [pastedChunks, setPastedChunks] = useState([]);
87
+ const [pasteCount, setPasteCount] = useState(0);
88
+ const [messages, setMessages] = useState([]);
89
+ const [loading, setLoading] = useState(false);
90
+ const [streaming, setStreaming] = useState(false);
91
+ const [spinnerMsg, setSpinnerMsg] = useState("");
92
+ const [agent, setAgent] = useState(null);
93
+ const [modelName, setModelName] = useState("");
94
+ const providerRef = React.useRef({ baseUrl: "", apiKey: "" });
95
+ const [ready, setReady] = useState(false);
96
+ const [connectionInfo, setConnectionInfo] = useState([]);
97
+ const [ctrlCPressed, setCtrlCPressed] = useState(false);
98
+ const [cmdIndex, setCmdIndex] = useState(0);
99
+ const [inputKey, setInputKey] = useState(0);
100
+ const [sessionPicker, setSessionPicker] = useState(null);
101
+ const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
102
+ const [approval, setApproval] = useState(null);
103
+ // Listen for paste events from stdin interceptor
104
+ useEffect(() => {
105
+ const handler = ({ content, lines }) => {
106
+ setPasteCount((c) => {
107
+ const newId = c + 1;
108
+ setPastedChunks((prev) => [...prev, { id: newId, lines, content }]);
109
+ return newId;
110
+ });
111
+ };
112
+ pasteEvents.on("paste", handler);
113
+ return () => { pasteEvents.off("paste", handler); };
114
+ }, []);
115
+ // Initialize agent
116
+ useEffect(() => {
117
+ (async () => {
118
+ const cliArgs = parseCLIArgs();
119
+ const rawConfig = loadConfig();
120
+ const config = applyOverrides(rawConfig, cliArgs);
121
+ let provider = config.provider;
122
+ const info = [];
123
+ if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !cliArgs.baseUrl)) {
124
+ info.push("Detecting local LLM server...");
125
+ setConnectionInfo([...info]);
126
+ const detected = await detectLocalProvider();
127
+ if (detected) {
128
+ // Keep CLI model override if specified
129
+ if (cliArgs.model)
130
+ detected.model = cliArgs.model;
131
+ provider = detected;
132
+ info.push(`✔ Connected to ${provider.baseUrl} → ${provider.model}`);
133
+ setConnectionInfo([...info]);
134
+ }
135
+ else {
136
+ info.push("✗ No local LLM server found. Start LM Studio or Ollama.");
137
+ info.push(" Use --base-url and --api-key to connect to a remote provider.");
138
+ setConnectionInfo([...info]);
139
+ return;
140
+ }
141
+ }
142
+ else {
143
+ info.push(`Provider: ${provider.baseUrl}`);
144
+ info.push(`Model: ${provider.model}`);
145
+ setConnectionInfo([...info]);
146
+ }
147
+ const cwd = process.cwd();
148
+ // Git info
149
+ if (isGitRepo(cwd)) {
150
+ const branch = getBranch(cwd);
151
+ const status = getStatus(cwd);
152
+ info.push(`Git: ${branch} (${status})`);
153
+ setConnectionInfo([...info]);
154
+ }
155
+ const a = new CodingAgent({
156
+ provider,
157
+ cwd,
158
+ maxTokens: config.defaults.maxTokens,
159
+ autoApprove: config.defaults.autoApprove,
160
+ onToken: (token) => {
161
+ // Switch from big spinner to streaming mode
162
+ setLoading(false);
163
+ setStreaming(true);
164
+ // Update the current streaming response in-place
165
+ setMessages((prev) => {
166
+ const lastIdx = prev.length - 1;
167
+ const last = prev[lastIdx];
168
+ if (last && last.type === "response" && last._streaming) {
169
+ return [
170
+ ...prev.slice(0, lastIdx),
171
+ { ...last, text: last.text + token },
172
+ ];
173
+ }
174
+ // First token of a new response
175
+ return [...prev, { id: msgId++, type: "response", text: token, _streaming: true }];
176
+ });
177
+ },
178
+ onToolCall: (name, args) => {
179
+ setLoading(true);
180
+ setSpinnerMsg("Executing tools...");
181
+ const argStr = Object.entries(args)
182
+ .map(([k, v]) => {
183
+ const val = String(v);
184
+ return val.length > 60 ? val.slice(0, 60) + "..." : val;
185
+ })
186
+ .join(", ");
187
+ addMsg("tool", `${name}(${argStr})`);
188
+ },
189
+ onToolResult: (_name, result) => {
190
+ const numLines = result.split("\n").length;
191
+ const size = result.length > 1024 ? `${(result.length / 1024).toFixed(1)}KB` : `${result.length}B`;
192
+ addMsg("tool-result", `└ ${numLines} lines (${size})`);
193
+ },
194
+ onThinking: (text) => {
195
+ if (text.length > 0) {
196
+ addMsg("info", `💭 Thought for ${text.split(/\s+/).length} words`);
197
+ }
198
+ },
199
+ onGitCommit: (message) => {
200
+ addMsg("info", `📝 Auto-committed: ${message}`);
201
+ },
202
+ onToolApproval: (name, args) => {
203
+ return new Promise((resolve) => {
204
+ setApproval({ tool: name, args, resolve });
205
+ setLoading(false);
206
+ });
207
+ },
208
+ });
209
+ // Initialize async context (repo map)
210
+ await a.init();
211
+ setAgent(a);
212
+ setModelName(provider.model);
213
+ providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
214
+ setReady(true);
215
+ })();
216
+ }, []);
217
+ function addMsg(type, text) {
218
+ setMessages((prev) => [...prev, { id: msgId++, type, text }]);
219
+ }
220
+ // Compute matching commands for suggestions
221
+ const cmdMatches = input.startsWith("/")
222
+ ? SLASH_COMMANDS.filter(c => c.cmd.startsWith(input.toLowerCase()))
223
+ : [];
224
+ const showSuggestions = cmdMatches.length > 0 && !loading && !approval && input !== cmdMatches[0]?.cmd;
225
+ // Refs to avoid stale closures in handleSubmit
226
+ const cmdIndexRef = React.useRef(cmdIndex);
227
+ cmdIndexRef.current = cmdIndex;
228
+ const cmdMatchesRef = React.useRef(cmdMatches);
229
+ cmdMatchesRef.current = cmdMatches;
230
+ const showSuggestionsRef = React.useRef(showSuggestions);
231
+ showSuggestionsRef.current = showSuggestions;
232
+ const pastedChunksRef = React.useRef(pastedChunks);
233
+ pastedChunksRef.current = pastedChunks;
234
+ const handleSubmit = useCallback(async (value) => {
235
+ // Skip autocomplete if input exactly matches a command (e.g. /models vs /model)
236
+ const isExactCommand = SLASH_COMMANDS.some(c => c.cmd === value.trim());
237
+ // If suggestions are showing and input isn't already an exact command, use autocomplete
238
+ if (showSuggestionsRef.current && !isExactCommand) {
239
+ const matches = cmdMatchesRef.current;
240
+ const idx = cmdIndexRef.current;
241
+ const selected = matches[idx];
242
+ if (selected) {
243
+ // Commands that need args (like /commit, /model) — fill input instead of executing
244
+ if (selected.cmd === "/commit" || selected.cmd === "/model") {
245
+ setInput(selected.cmd + " ");
246
+ setCmdIndex(0);
247
+ setInputKey((k) => k + 1);
248
+ return;
249
+ }
250
+ // Execute the selected command directly
251
+ value = selected.cmd;
252
+ setCmdIndex(0);
253
+ }
254
+ }
255
+ // Combine typed text with any pasted chunks
256
+ const chunks = pastedChunksRef.current;
257
+ let fullValue = value;
258
+ if (chunks.length > 0) {
259
+ const pasteText = chunks.map(p => p.content).join("\n\n");
260
+ fullValue = value ? `${value}\n\n${pasteText}` : pasteText;
261
+ }
262
+ const trimmed = fullValue.trim();
263
+ setInput("");
264
+ setPastedChunks([]);
265
+ setPasteCount(0);
266
+ if (!trimmed || !agent)
267
+ return;
268
+ addMsg("user", trimmed);
269
+ if (trimmed === "/quit" || trimmed === "/exit") {
270
+ exit();
271
+ return;
272
+ }
273
+ if (trimmed === "/help") {
274
+ addMsg("info", [
275
+ "Commands:",
276
+ " /help — show this",
277
+ " /model — switch model mid-session",
278
+ " /models — list available models",
279
+ " /map — show repository map",
280
+ " /sessions — list past sessions",
281
+ " /resume — resume a past session",
282
+ " /reset — clear conversation",
283
+ " /context — show message count",
284
+ " /diff — show git changes",
285
+ " /undo — revert last codemaxxing commit",
286
+ " /commit — commit all changes",
287
+ " /push — push to remote",
288
+ " /git on — enable auto-commits",
289
+ " /git off — disable auto-commits",
290
+ " /quit — exit",
291
+ ].join("\n"));
292
+ return;
293
+ }
294
+ if (trimmed === "/reset") {
295
+ agent.reset();
296
+ addMsg("info", "✅ Conversation reset.");
297
+ return;
298
+ }
299
+ if (trimmed === "/context") {
300
+ addMsg("info", `Messages in context: ${agent.getContextLength()}`);
301
+ return;
302
+ }
303
+ if (trimmed === "/models") {
304
+ addMsg("info", "Fetching available models...");
305
+ const { baseUrl, apiKey } = providerRef.current;
306
+ const models = await listModels(baseUrl, apiKey);
307
+ if (models.length === 0) {
308
+ addMsg("info", "No models found or couldn't reach provider.");
309
+ }
310
+ else {
311
+ addMsg("info", "Available models:\n" + models.map(m => ` ${m}`).join("\n"));
312
+ }
313
+ return;
314
+ }
315
+ if (trimmed.startsWith("/model")) {
316
+ const newModel = trimmed.replace("/model", "").trim();
317
+ if (!newModel) {
318
+ addMsg("info", `Current model: ${agent.getModel()}\n Usage: /model <model-name>`);
319
+ return;
320
+ }
321
+ agent.switchModel(newModel);
322
+ setModelName(newModel);
323
+ addMsg("info", `✅ Switched to model: ${newModel}`);
324
+ return;
325
+ }
326
+ if (trimmed === "/map") {
327
+ const map = agent.getRepoMap();
328
+ if (map) {
329
+ addMsg("info", map);
330
+ }
331
+ else {
332
+ // Map hasn't been built yet, refresh it
333
+ setLoading(true);
334
+ const newMap = await agent.refreshRepoMap();
335
+ addMsg("info", newMap || "No repository map available.");
336
+ setLoading(false);
337
+ }
338
+ return;
339
+ }
340
+ if (trimmed === "/diff") {
341
+ const diff = getDiff(process.cwd());
342
+ addMsg("info", diff);
343
+ return;
344
+ }
345
+ if (trimmed === "/undo") {
346
+ const result = undoLastCommit(process.cwd());
347
+ addMsg("info", result.success ? `✅ ${result.message}` : `✗ ${result.message}`);
348
+ return;
349
+ }
350
+ if (trimmed === "/git on") {
351
+ if (!agent.isGitEnabled()) {
352
+ addMsg("info", "✗ Not a git repository");
353
+ }
354
+ else {
355
+ agent.setAutoCommit(true);
356
+ addMsg("info", "✅ Auto-commits enabled for this session");
357
+ }
358
+ return;
359
+ }
360
+ if (trimmed === "/git off") {
361
+ agent.setAutoCommit(false);
362
+ addMsg("info", "✅ Auto-commits disabled");
363
+ return;
364
+ }
365
+ if (trimmed === "/sessions") {
366
+ const sessions = listSessions(10);
367
+ if (sessions.length === 0) {
368
+ addMsg("info", "No past sessions found.");
369
+ }
370
+ else {
371
+ const lines = sessions.map((s, i) => {
372
+ const date = new Date(s.updated_at + "Z");
373
+ const ago = formatTimeAgo(date);
374
+ const dir = s.cwd.split("/").pop() || s.cwd;
375
+ const tokens = s.token_estimate >= 1000
376
+ ? `${(s.token_estimate / 1000).toFixed(1)}k`
377
+ : String(s.token_estimate);
378
+ return ` ${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`;
379
+ });
380
+ addMsg("info", "Recent sessions:\n" + lines.join("\n") + "\n\n Use /resume <id> to continue a session");
381
+ }
382
+ return;
383
+ }
384
+ if (trimmed === "/resume") {
385
+ const sessions = listSessions(10);
386
+ if (sessions.length === 0) {
387
+ addMsg("info", "No past sessions to resume.");
388
+ return;
389
+ }
390
+ const items = sessions.map((s) => {
391
+ const date = new Date(s.updated_at + "Z");
392
+ const ago = formatTimeAgo(date);
393
+ const dir = s.cwd.split("/").pop() || s.cwd;
394
+ const tokens = s.token_estimate >= 1000
395
+ ? `${(s.token_estimate / 1000).toFixed(1)}k`
396
+ : String(s.token_estimate);
397
+ return {
398
+ id: s.id,
399
+ display: `${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`,
400
+ };
401
+ });
402
+ setSessionPicker(items);
403
+ setSessionPickerIndex(0);
404
+ return;
405
+ }
406
+ if (trimmed === "/push") {
407
+ try {
408
+ const output = execSync("git push", { cwd: process.cwd(), encoding: "utf-8", stdio: "pipe" });
409
+ addMsg("info", `✅ Pushed to remote${output.trim() ? "\n" + output.trim() : ""}`);
410
+ }
411
+ catch (e) {
412
+ addMsg("error", `Push failed: ${e.stderr || e.message}`);
413
+ }
414
+ return;
415
+ }
416
+ if (trimmed.startsWith("/commit")) {
417
+ const msg = trimmed.replace("/commit", "").trim();
418
+ if (!msg) {
419
+ addMsg("info", "Usage: /commit your commit message here");
420
+ return;
421
+ }
422
+ try {
423
+ execSync("git add -A", { cwd: process.cwd(), stdio: "pipe" });
424
+ execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: process.cwd(), stdio: "pipe" });
425
+ addMsg("info", `✅ Committed: ${msg}`);
426
+ }
427
+ catch (e) {
428
+ addMsg("error", `Commit failed: ${e.stderr || e.message}`);
429
+ }
430
+ return;
431
+ }
432
+ setLoading(true);
433
+ setStreaming(false);
434
+ setSpinnerMsg(SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)]);
435
+ try {
436
+ // Response is built incrementally via onToken callback
437
+ // chat() returns the final text but we don't need to add it again
438
+ await agent.chat(trimmed);
439
+ }
440
+ catch (err) {
441
+ addMsg("error", `Error: ${err.message}`);
442
+ }
443
+ setLoading(false);
444
+ setStreaming(false);
445
+ }, [agent, exit]);
446
+ useInput((inputChar, key) => {
447
+ // Handle slash command navigation
448
+ if (showSuggestionsRef.current) {
449
+ const matches = cmdMatchesRef.current;
450
+ if (key.upArrow) {
451
+ setCmdIndex((prev) => (prev - 1 + matches.length) % matches.length);
452
+ return;
453
+ }
454
+ if (key.downArrow) {
455
+ setCmdIndex((prev) => (prev + 1) % matches.length);
456
+ return;
457
+ }
458
+ if (key.tab) {
459
+ const selected = matches[cmdIndexRef.current];
460
+ if (selected) {
461
+ setInput(selected.cmd + (selected.cmd === "/commit" ? " " : ""));
462
+ setCmdIndex(0);
463
+ setInputKey((k) => k + 1);
464
+ }
465
+ return;
466
+ }
467
+ }
468
+ // Session picker navigation
469
+ if (sessionPicker) {
470
+ if (key.upArrow) {
471
+ setSessionPickerIndex((prev) => (prev - 1 + sessionPicker.length) % sessionPicker.length);
472
+ return;
473
+ }
474
+ if (key.downArrow) {
475
+ setSessionPickerIndex((prev) => (prev + 1) % sessionPicker.length);
476
+ return;
477
+ }
478
+ if (key.return) {
479
+ const selected = sessionPicker[sessionPickerIndex];
480
+ if (selected && agent) {
481
+ const session = getSession(selected.id);
482
+ if (session) {
483
+ agent.resume(selected.id).then(() => {
484
+ const dir = session.cwd.split("/").pop() || session.cwd;
485
+ // Find last user message for context
486
+ const msgs = loadMessages(selected.id);
487
+ const lastUserMsg = [...msgs].reverse().find(m => m.role === "user");
488
+ const lastText = lastUserMsg && typeof lastUserMsg.content === "string"
489
+ ? lastUserMsg.content.slice(0, 80) + (lastUserMsg.content.length > 80 ? "..." : "")
490
+ : null;
491
+ let info = `✅ Resumed session ${selected.id} (${dir}/, ${session.message_count} messages)`;
492
+ if (lastText)
493
+ info += `\n Last: "${lastText}"`;
494
+ addMsg("info", info);
495
+ }).catch((e) => {
496
+ addMsg("error", `Failed to resume: ${e.message}`);
497
+ });
498
+ }
499
+ }
500
+ setSessionPicker(null);
501
+ setSessionPickerIndex(0);
502
+ return;
503
+ }
504
+ if (key.escape) {
505
+ setSessionPicker(null);
506
+ setSessionPickerIndex(0);
507
+ addMsg("info", "Resume cancelled.");
508
+ return;
509
+ }
510
+ return; // Ignore other keys during session picker
511
+ }
512
+ // Backspace with empty input → remove last paste chunk
513
+ if (key.backspace || key.delete) {
514
+ if (input === "" && pastedChunksRef.current.length > 0) {
515
+ setPastedChunks((prev) => prev.slice(0, -1));
516
+ return;
517
+ }
518
+ }
519
+ // Handle approval prompts
520
+ if (approval) {
521
+ if (inputChar === "y" || inputChar === "Y") {
522
+ const r = approval.resolve;
523
+ setApproval(null);
524
+ setLoading(true);
525
+ setSpinnerMsg("Executing...");
526
+ r("yes");
527
+ return;
528
+ }
529
+ if (inputChar === "n" || inputChar === "N") {
530
+ const r = approval.resolve;
531
+ setApproval(null);
532
+ addMsg("info", "✗ Denied");
533
+ r("no");
534
+ return;
535
+ }
536
+ if (inputChar === "a" || inputChar === "A") {
537
+ const r = approval.resolve;
538
+ setApproval(null);
539
+ setLoading(true);
540
+ setSpinnerMsg("Executing...");
541
+ addMsg("info", `✔ Always allow ${approval.tool} for this session`);
542
+ r("always");
543
+ return;
544
+ }
545
+ return; // Ignore other keys during approval
546
+ }
547
+ if (key.ctrl && inputChar === "c") {
548
+ if (ctrlCPressed) {
549
+ exit();
550
+ }
551
+ else {
552
+ setCtrlCPressed(true);
553
+ addMsg("info", "Press Ctrl+C again to exit.");
554
+ setTimeout(() => setCtrlCPressed(false), 3000);
555
+ }
556
+ }
557
+ });
558
+ // CODE banner lines
559
+ const codeLines = [
560
+ " _(`-') (`-') _ ",
561
+ " _ .-> ( (OO ).-> ( OO).-/ ",
562
+ " \\-,-----.(`-')----. \\ .'_ (,------. ",
563
+ " | .--./( OO).-. ''`'-..__) | .---' ",
564
+ " /_) (`-')( _) | | || | ' |(| '--. ",
565
+ " || |OO ) \\| |)| || | / : | .--' ",
566
+ "(_' '--'\\ ' '-' '| '-' / | `---. ",
567
+ " `-----' `-----' `------' `------' ",
568
+ ];
569
+ const maxxingLines = [
570
+ "<-. (`-') (`-') _ (`-') (`-') _ <-. (`-')_ ",
571
+ " \\(OO )_ (OO ).-/ (OO )_.-> (OO )_.-> (_) \\( OO) ) .-> ",
572
+ ",--./ ,-.) / ,---. (_| \\_)--. (_| \\_)--.,-(`-'),--./ ,--/ ,---(`-') ",
573
+ "| `.' | | \\ /`.\\ \\ `.' / \\ `.' / | ( OO)| \\ | | ' .-(OO ) ",
574
+ "| |'.'| | '-'|_.' | \\ .') \\ .') | | )| . '| |)| | .-, \\ ",
575
+ "| | | |(| .-. | .' \\ .' \\ (| |_/ | |\\ | | | '.(_/ ",
576
+ "| | | | | | | | / .'. \\ / .'. \\ | |'->| | \\ | | '-' | ",
577
+ "`--' `--' `--' `--'`--' '--'`--' '--'`--' `--' `--' `-----' ",
578
+ ];
579
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00FFFF", paddingX: 1, children: [codeLines.map((line, i) => (_jsx(Text, { color: "#00FFFF", children: line }, `c${i}`))), maxxingLines.map((line, i) => (_jsx(Text, { color: i === maxxingLines.length - 1 ? "#CC00CC" : "#FF00FF", children: line }, `m${i}`))), _jsxs(Text, { children: [_jsx(Text, { color: "#008B8B", children: " v" + VERSION }), " ", _jsx(Text, { color: "#00FFFF", children: "\uD83D\uDCAA" }), " ", _jsx(Text, { dimColor: true, children: "your code. your model. no excuses." })] }), _jsxs(Text, { dimColor: true, children: [" Type ", _jsx(Text, { color: "#008B8B", children: "/help" }), " for commands · ", _jsx(Text, { color: "#008B8B", children: "Ctrl+C" }), " twice to exit"] })] }), connectionInfo.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#008B8B", paddingX: 1, marginBottom: 1, children: connectionInfo.map((line, i) => (_jsx(Text, { color: line.startsWith("✔") ? "#00FFFF" : line.startsWith("✗") ? "red" : "#008B8B", children: line }, i))) })), messages.map((msg) => {
580
+ switch (msg.type) {
581
+ case "user":
582
+ return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "#008B8B", children: [" > ", msg.text] }) }, msg.id));
583
+ case "response":
584
+ return (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: msg.text.split("\n").map((l, i) => (_jsxs(Text, { wrap: "wrap", children: [i === 0 ? _jsx(Text, { color: "#00FFFF", children: "\u25CF " }) : _jsx(Text, { children: " " }), l.startsWith("```") ? _jsx(Text, { color: "#008B8B", children: l }) :
585
+ l.startsWith("# ") || l.startsWith("## ") ? _jsx(Text, { bold: true, color: "#FF00FF", children: l }) :
586
+ l.startsWith("**") ? _jsx(Text, { bold: true, children: l }) :
587
+ _jsx(Text, { children: l })] }, i))) }, msg.id));
588
+ case "tool":
589
+ return (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "#00FFFF", children: " \u25CF " }), _jsx(Text, { bold: true, color: "#FF00FF", children: msg.text })] }) }, msg.id));
590
+ case "tool-result":
591
+ return _jsxs(Text, { color: "#008B8B", children: [" ", msg.text] }, msg.id);
592
+ case "error":
593
+ return _jsxs(Text, { color: "red", children: [" ", msg.text] }, msg.id);
594
+ case "info":
595
+ return _jsxs(Text, { color: "#008B8B", children: [" ", msg.text] }, msg.id);
596
+ default:
597
+ return _jsx(Text, { children: msg.text }, msg.id);
598
+ }
599
+ }), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg }), streaming && !loading && _jsx(StreamingIndicator, {}), approval && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#FF8C00", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: "#FF8C00", children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: "#008B8B", children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: "#008B8B", children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.tool === "run_command" && approval.args.command ? (_jsxs(Text, { color: "#008B8B", children: [" $ ", String(approval.args.command)] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: "#00FF00", bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: "#FF0000", bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: "#00FFFF", bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#FF00FF", paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: "#FF00FF", children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: "#FF00FF", bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? "#FF00FF" : "#008B8B", children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "#008B8B", paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: "#FF00FF", bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? "#FF00FF" : "#00FFFF", bold: true, children: c.cmd }), _jsxs(Text, { color: "#008B8B", children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? "#FF8C00" : "#00FFFF", paddingX: 1, children: [_jsx(Text, { color: "#FF00FF", bold: true, children: "> " }), approval ? (_jsx(Text, { color: "#FF8C00", children: "waiting for approval..." })) : ready && !loading ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: "#008B8B", children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(v); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: ["💬 ", agent.getContextLength(), " messages · ~", (() => {
600
+ const tokens = agent.estimateTokens();
601
+ return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
602
+ })(), " tokens", modelName ? ` · 🤖 ${modelName}` : ""] }) }))] }));
603
+ }
604
+ // Clear screen before render
605
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
606
+ // Paste event bus — communicates between stdin interceptor and React
607
+ const pasteEvents = new EventEmitter();
608
+ // Enable bracketed paste mode — terminal wraps pastes in escape sequences
609
+ process.stdout.write("\x1b[?2004h");
610
+ // Intercept stdin to handle pasted content
611
+ // Bracketed paste: \x1b[200~ ... \x1b[201~
612
+ let pasteBuffer = "";
613
+ let inPaste = false;
614
+ const origPush = process.stdin.push.bind(process.stdin);
615
+ process.stdin.push = function (chunk, encoding) {
616
+ if (chunk === null)
617
+ return origPush(chunk, encoding);
618
+ let data = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf-8") : String(chunk);
619
+ const hasStart = data.includes("\x1b[200~");
620
+ const hasEnd = data.includes("\x1b[201~");
621
+ if (hasStart) {
622
+ inPaste = true;
623
+ data = data.replace(/\x1b\[200~/g, "");
624
+ }
625
+ if (hasEnd) {
626
+ data = data.replace(/\x1b\[201~/g, "");
627
+ pasteBuffer += data;
628
+ inPaste = false;
629
+ const content = pasteBuffer.trim();
630
+ pasteBuffer = "";
631
+ const lineCount = content.split("\n").length;
632
+ if (lineCount > 2) {
633
+ // Multi-line paste → store as chunk, don't send to input
634
+ pasteEvents.emit("paste", { content, lines: lineCount });
635
+ return true;
636
+ }
637
+ // Short paste (1-2 lines) — send as normal input
638
+ const sanitized = content.replace(/\r?\n/g, " ");
639
+ if (sanitized) {
640
+ return origPush(sanitized, "utf-8");
641
+ }
642
+ return true;
643
+ }
644
+ if (inPaste) {
645
+ pasteBuffer += data;
646
+ return true;
647
+ }
648
+ data = data.replace(/\x1b\[20[01]~/g, "");
649
+ return origPush(typeof chunk === "string" ? data : Buffer.from(data), encoding);
650
+ };
651
+ // Disable bracketed paste on exit
652
+ process.on("exit", () => {
653
+ process.stdout.write("\x1b[?2004l");
654
+ });
655
+ // Handle terminal resize — clear ghost artifacts
656
+ process.stdout.on("resize", () => {
657
+ process.stdout.write("\x1B[2J\x1B[H");
658
+ });
659
+ render(_jsx(App, {}), { exitOnCtrlC: false });
@@ -0,0 +1,9 @@
1
+ import type { ChatCompletionTool } from "openai/resources/chat/completions";
2
+ /**
3
+ * Tool definitions for the OpenAI function calling API
4
+ */
5
+ export declare const FILE_TOOLS: ChatCompletionTool[];
6
+ /**
7
+ * Execute a tool call and return the result
8
+ */
9
+ export declare function executeTool(name: string, args: Record<string, unknown>, cwd: string): Promise<string>;