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/README.md +209 -0
- package/dist/agent.d.ts +65 -0
- package/dist/agent.js +269 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +174 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +659 -0
- package/dist/tools/files.d.ts +9 -0
- package/dist/tools/files.js +227 -0
- package/dist/utils/context.d.ts +13 -0
- package/dist/utils/context.js +135 -0
- package/dist/utils/git.d.ts +27 -0
- package/dist/utils/git.js +113 -0
- package/dist/utils/repomap.d.ts +18 -0
- package/dist/utils/repomap.js +195 -0
- package/dist/utils/sessions.d.ts +51 -0
- package/dist/utils/sessions.js +174 -0
- package/dist/utils/treesitter.d.ts +20 -0
- package/dist/utils/treesitter.js +710 -0
- package/package.json +51 -0
- package/src/agent.ts +322 -0
- package/src/config.ts +211 -0
- package/src/index.tsx +858 -0
- package/src/tools/files.ts +247 -0
- package/src/utils/context.ts +146 -0
- package/src/utils/git.ts +117 -0
- package/src/utils/repomap.ts +220 -0
- package/src/utils/sessions.ts +222 -0
- package/tsconfig.json +16 -0
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>;
|