codemaxxing 0.1.8 → 0.1.10
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 +43 -0
- package/dist/agent.d.ts +31 -1
- package/dist/agent.js +357 -5
- package/dist/config.d.ts +3 -5
- package/dist/config.js +23 -0
- package/dist/index.js +139 -27
- package/dist/tools/files.d.ts +8 -0
- package/dist/tools/files.js +137 -0
- package/dist/utils/sessions.d.ts +7 -0
- package/dist/utils/sessions.js +26 -1
- package/package.json +2 -1
- package/src/agent.ts +380 -6
- package/src/config.ts +28 -1
- package/src/index.tsx +177 -24
- package/src/tools/files.ts +127 -0
- package/src/utils/sessions.ts +33 -1
package/dist/index.js
CHANGED
|
@@ -6,12 +6,12 @@ import { EventEmitter } from "events";
|
|
|
6
6
|
import TextInput from "ink-text-input";
|
|
7
7
|
import { CodingAgent } from "./agent.js";
|
|
8
8
|
import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
|
|
9
|
-
import { listSessions, getSession, loadMessages } from "./utils/sessions.js";
|
|
9
|
+
import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
|
|
10
10
|
import { execSync } from "child_process";
|
|
11
11
|
import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
|
|
12
12
|
import { getTheme, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
|
|
13
13
|
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
|
|
14
|
-
const VERSION = "0.1.
|
|
14
|
+
const VERSION = "0.1.9";
|
|
15
15
|
// ── Helpers ──
|
|
16
16
|
function formatTimeAgo(date) {
|
|
17
17
|
const secs = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
@@ -43,6 +43,7 @@ const SLASH_COMMANDS = [
|
|
|
43
43
|
{ cmd: "/theme", desc: "switch color theme" },
|
|
44
44
|
{ cmd: "/model", desc: "switch model mid-session" },
|
|
45
45
|
{ cmd: "/sessions", desc: "list past sessions" },
|
|
46
|
+
{ cmd: "/session delete", desc: "delete a session" },
|
|
46
47
|
{ cmd: "/resume", desc: "resume a past session" },
|
|
47
48
|
{ cmd: "/quit", desc: "exit" },
|
|
48
49
|
];
|
|
@@ -106,6 +107,9 @@ function App() {
|
|
|
106
107
|
const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
|
|
107
108
|
const [themePicker, setThemePicker] = useState(false);
|
|
108
109
|
const [themePickerIndex, setThemePickerIndex] = useState(0);
|
|
110
|
+
const [deleteSessionPicker, setDeleteSessionPicker] = useState(null);
|
|
111
|
+
const [deleteSessionPickerIndex, setDeleteSessionPickerIndex] = useState(0);
|
|
112
|
+
const [deleteSessionConfirm, setDeleteSessionConfirm] = useState(null);
|
|
109
113
|
const [loginPicker, setLoginPicker] = useState(false);
|
|
110
114
|
const [loginPickerIndex, setLoginPickerIndex] = useState(0);
|
|
111
115
|
const [loginMethodPicker, setLoginMethodPicker] = useState(null);
|
|
@@ -146,7 +150,9 @@ function App() {
|
|
|
146
150
|
else {
|
|
147
151
|
info.push("✗ No local LLM server found. Start LM Studio or Ollama.");
|
|
148
152
|
info.push(" Use --base-url and --api-key to connect to a remote provider.");
|
|
153
|
+
info.push(" Type /login to authenticate with a cloud provider.");
|
|
149
154
|
setConnectionInfo([...info]);
|
|
155
|
+
setReady(true);
|
|
150
156
|
return;
|
|
151
157
|
}
|
|
152
158
|
}
|
|
@@ -210,9 +216,15 @@ function App() {
|
|
|
210
216
|
onGitCommit: (message) => {
|
|
211
217
|
addMsg("info", `📝 Auto-committed: ${message}`);
|
|
212
218
|
},
|
|
213
|
-
|
|
219
|
+
onContextCompressed: (oldTokens, newTokens) => {
|
|
220
|
+
const saved = oldTokens - newTokens;
|
|
221
|
+
const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
|
|
222
|
+
addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
|
|
223
|
+
},
|
|
224
|
+
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
225
|
+
onToolApproval: (name, args, diff) => {
|
|
214
226
|
return new Promise((resolve) => {
|
|
215
|
-
setApproval({ tool: name, args, resolve });
|
|
227
|
+
setApproval({ tool: name, args, diff, resolve });
|
|
216
228
|
setLoading(false);
|
|
217
229
|
});
|
|
218
230
|
},
|
|
@@ -252,7 +264,7 @@ function App() {
|
|
|
252
264
|
const selected = matches[idx];
|
|
253
265
|
if (selected) {
|
|
254
266
|
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
255
|
-
if (selected.cmd === "/commit" || selected.cmd === "/model") {
|
|
267
|
+
if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete") {
|
|
256
268
|
setInput(selected.cmd + " ");
|
|
257
269
|
setCmdIndex(0);
|
|
258
270
|
setInputKey((k) => k + 1);
|
|
@@ -274,7 +286,7 @@ function App() {
|
|
|
274
286
|
setInput("");
|
|
275
287
|
setPastedChunks([]);
|
|
276
288
|
setPasteCount(0);
|
|
277
|
-
if (!trimmed
|
|
289
|
+
if (!trimmed)
|
|
278
290
|
return;
|
|
279
291
|
addMsg("user", trimmed);
|
|
280
292
|
if (trimmed === "/quit" || trimmed === "/exit") {
|
|
@@ -295,6 +307,7 @@ function App() {
|
|
|
295
307
|
" /models — list available models",
|
|
296
308
|
" /map — show repository map",
|
|
297
309
|
" /sessions — list past sessions",
|
|
310
|
+
" /session delete — delete a session",
|
|
298
311
|
" /resume — resume a past session",
|
|
299
312
|
" /reset — clear conversation",
|
|
300
313
|
" /context — show message count",
|
|
@@ -308,6 +321,28 @@ function App() {
|
|
|
308
321
|
].join("\n"));
|
|
309
322
|
return;
|
|
310
323
|
}
|
|
324
|
+
if (trimmed.startsWith("/theme")) {
|
|
325
|
+
const themeName = trimmed.replace("/theme", "").trim();
|
|
326
|
+
if (!themeName) {
|
|
327
|
+
const themeKeys = listThemes();
|
|
328
|
+
const currentIdx = themeKeys.indexOf(theme.name.toLowerCase());
|
|
329
|
+
setThemePicker(true);
|
|
330
|
+
setThemePickerIndex(currentIdx >= 0 ? currentIdx : 0);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!THEMES[themeName]) {
|
|
334
|
+
addMsg("error", `Theme "${themeName}" not found. Use /theme to see available themes.`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
setTheme(getTheme(themeName));
|
|
338
|
+
addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Commands below require an active LLM connection
|
|
342
|
+
if (!agent) {
|
|
343
|
+
addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
311
346
|
if (trimmed === "/reset") {
|
|
312
347
|
agent.reset();
|
|
313
348
|
addMsg("info", "✅ Conversation reset.");
|
|
@@ -340,23 +375,6 @@ function App() {
|
|
|
340
375
|
addMsg("info", `✅ Switched to model: ${newModel}`);
|
|
341
376
|
return;
|
|
342
377
|
}
|
|
343
|
-
if (trimmed.startsWith("/theme")) {
|
|
344
|
-
const themeName = trimmed.replace("/theme", "").trim();
|
|
345
|
-
if (!themeName) {
|
|
346
|
-
const themeKeys = listThemes();
|
|
347
|
-
const currentIdx = themeKeys.indexOf(theme.name.toLowerCase());
|
|
348
|
-
setThemePicker(true);
|
|
349
|
-
setThemePickerIndex(currentIdx >= 0 ? currentIdx : 0);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (!THEMES[themeName]) {
|
|
353
|
-
addMsg("error", `Theme "${themeName}" not found. Use /theme to see available themes.`);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
setTheme(getTheme(themeName));
|
|
357
|
-
addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
378
|
if (trimmed === "/map") {
|
|
361
379
|
const map = agent.getRepoMap();
|
|
362
380
|
if (map) {
|
|
@@ -409,12 +427,50 @@ function App() {
|
|
|
409
427
|
const tokens = s.token_estimate >= 1000
|
|
410
428
|
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
411
429
|
: String(s.token_estimate);
|
|
412
|
-
|
|
430
|
+
const cost = s.estimated_cost > 0
|
|
431
|
+
? `$${s.estimated_cost < 0.01 ? s.estimated_cost.toFixed(4) : s.estimated_cost.toFixed(2)}`
|
|
432
|
+
: "";
|
|
433
|
+
return ` ${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok${cost ? ` ${cost}` : ""} ${ago} ${s.model}`;
|
|
413
434
|
});
|
|
414
435
|
addMsg("info", "Recent sessions:\n" + lines.join("\n") + "\n\n Use /resume <id> to continue a session");
|
|
415
436
|
}
|
|
416
437
|
return;
|
|
417
438
|
}
|
|
439
|
+
if (trimmed.startsWith("/session delete")) {
|
|
440
|
+
const idArg = trimmed.replace("/session delete", "").trim();
|
|
441
|
+
if (idArg) {
|
|
442
|
+
// Direct delete by ID
|
|
443
|
+
const session = getSession(idArg);
|
|
444
|
+
if (!session) {
|
|
445
|
+
addMsg("error", `Session "${idArg}" not found.`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const dir = session.cwd.split("/").pop() || session.cwd;
|
|
449
|
+
setDeleteSessionConfirm({ id: idArg, display: `${idArg} ${dir}/ ${session.message_count} msgs ${session.model}` });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Show picker
|
|
453
|
+
const sessions = listSessions(10);
|
|
454
|
+
if (sessions.length === 0) {
|
|
455
|
+
addMsg("info", "No sessions to delete.");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const items = sessions.map((s) => {
|
|
459
|
+
const date = new Date(s.updated_at + "Z");
|
|
460
|
+
const ago = formatTimeAgo(date);
|
|
461
|
+
const dir = s.cwd.split("/").pop() || s.cwd;
|
|
462
|
+
const tokens = s.token_estimate >= 1000
|
|
463
|
+
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
464
|
+
: String(s.token_estimate);
|
|
465
|
+
return {
|
|
466
|
+
id: s.id,
|
|
467
|
+
display: `${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`,
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
setDeleteSessionPicker(items);
|
|
471
|
+
setDeleteSessionPickerIndex(0);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
418
474
|
if (trimmed === "/resume") {
|
|
419
475
|
const sessions = listSessions(10);
|
|
420
476
|
if (sessions.length === 0) {
|
|
@@ -697,6 +753,53 @@ function App() {
|
|
|
697
753
|
}
|
|
698
754
|
return; // Ignore other keys during session picker
|
|
699
755
|
}
|
|
756
|
+
// Delete session confirmation (y/n)
|
|
757
|
+
if (deleteSessionConfirm) {
|
|
758
|
+
if (inputChar === "y" || inputChar === "Y") {
|
|
759
|
+
const deleted = deleteSession(deleteSessionConfirm.id);
|
|
760
|
+
if (deleted) {
|
|
761
|
+
addMsg("info", `✅ Deleted session ${deleteSessionConfirm.id}`);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
addMsg("error", `Failed to delete session ${deleteSessionConfirm.id}`);
|
|
765
|
+
}
|
|
766
|
+
setDeleteSessionConfirm(null);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (inputChar === "n" || inputChar === "N" || key.escape) {
|
|
770
|
+
addMsg("info", "Delete cancelled.");
|
|
771
|
+
setDeleteSessionConfirm(null);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
// Delete session picker navigation
|
|
777
|
+
if (deleteSessionPicker) {
|
|
778
|
+
if (key.upArrow) {
|
|
779
|
+
setDeleteSessionPickerIndex((prev) => (prev - 1 + deleteSessionPicker.length) % deleteSessionPicker.length);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (key.downArrow) {
|
|
783
|
+
setDeleteSessionPickerIndex((prev) => (prev + 1) % deleteSessionPicker.length);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (key.return) {
|
|
787
|
+
const selected = deleteSessionPicker[deleteSessionPickerIndex];
|
|
788
|
+
if (selected) {
|
|
789
|
+
setDeleteSessionPicker(null);
|
|
790
|
+
setDeleteSessionPickerIndex(0);
|
|
791
|
+
setDeleteSessionConfirm(selected);
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (key.escape) {
|
|
796
|
+
setDeleteSessionPicker(null);
|
|
797
|
+
setDeleteSessionPickerIndex(0);
|
|
798
|
+
addMsg("info", "Delete cancelled.");
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
700
803
|
// Backspace with empty input → remove last paste chunk
|
|
701
804
|
if (key.backspace || key.delete) {
|
|
702
805
|
if (input === "" && pastedChunksRef.current.length > 0) {
|
|
@@ -784,7 +887,10 @@ function App() {
|
|
|
784
887
|
default:
|
|
785
888
|
return _jsx(Text, { children: msg.text }, msg.id);
|
|
786
889
|
}
|
|
787
|
-
}), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: theme.colors.muted, children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: theme.colors.muted, children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.
|
|
890
|
+
}), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: theme.colors.muted, children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: theme.colors.muted, children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.diff ? (_jsxs(Box, { flexDirection: "column", marginTop: 0, marginLeft: 2, children: [approval.diff.split("\n").slice(0, 40).map((line, i) => (_jsx(Text, { color: line.startsWith("+") ? theme.colors.success :
|
|
891
|
+
line.startsWith("-") ? theme.colors.error :
|
|
892
|
+
line.startsWith("@@") ? theme.colors.primary :
|
|
893
|
+
theme.colors.muted, children: line }, i))), approval.diff.split("\n").length > 40 ? (_jsxs(Text, { color: theme.colors.muted, children: ["... (", approval.diff.split("\n").length - 40, " more lines)"] })) : null] })) : null, approval.tool === "run_command" && approval.args.command ? (_jsxs(Text, { color: theme.colors.muted, children: [" $ ", String(approval.args.command)] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), loginPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "\uD83D\uDCAA Choose a provider:" }), PROVIDERS.filter((p) => p.id !== "local").map((p, i) => (_jsxs(Text, { children: [i === loginPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginPickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: p.name }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", p.description] }), getCredentials().some((c) => c.provider === p.id) ? _jsx(Text, { color: theme.colors.success, children: " \u2713" }) : null] }, p.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), loginMethodPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "How do you want to authenticate?" }), loginMethodPicker.methods.map((method, i) => {
|
|
788
894
|
const labels = {
|
|
789
895
|
"oauth": "🌐 Browser login (OAuth)",
|
|
790
896
|
"setup-token": "🔑 Link subscription (via Claude Code CLI)",
|
|
@@ -793,10 +899,16 @@ function App() {
|
|
|
793
899
|
"device-flow": "📱 Device flow (GitHub)",
|
|
794
900
|
};
|
|
795
901
|
return (_jsxs(Text, { children: [i === loginMethodIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginMethodIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: labels[method] ?? method })] }, method));
|
|
796
|
-
}), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc back" })] })), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: c.cmd }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, 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 · ~", (() => {
|
|
902
|
+
}), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc back" })] })), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Delete a session:" }), deleteSessionPicker.map((s, i) => (_jsxs(Text, { children: [i === deleteSessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete session ", deleteSessionConfirm.id, "?"] }), _jsxs(Text, { color: theme.colors.muted, children: [" ", deleteSessionConfirm.display] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: c.cmd }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, 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 · ~", (() => {
|
|
797
903
|
const tokens = agent.estimateTokens();
|
|
798
904
|
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
799
|
-
})(), " tokens",
|
|
905
|
+
})(), " tokens", (() => {
|
|
906
|
+
const { totalCost } = agent.getCostInfo();
|
|
907
|
+
if (totalCost > 0) {
|
|
908
|
+
return ` · 💰 $${totalCost < 0.01 ? totalCost.toFixed(4) : totalCost.toFixed(2)}`;
|
|
909
|
+
}
|
|
910
|
+
return "";
|
|
911
|
+
})(), modelName ? ` · 🤖 ${modelName}` : ""] }) }))] }));
|
|
800
912
|
}
|
|
801
913
|
// Clear screen before render
|
|
802
914
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
package/dist/tools/files.d.ts
CHANGED
|
@@ -7,3 +7,11 @@ export declare const FILE_TOOLS: ChatCompletionTool[];
|
|
|
7
7
|
* Execute a tool call and return the result
|
|
8
8
|
*/
|
|
9
9
|
export declare function executeTool(name: string, args: Record<string, unknown>, cwd: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Generate a simple unified diff between two strings
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateDiff(oldContent: string, newContent: string, filePath: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Get existing file content for diff preview (returns null if file doesn't exist)
|
|
16
|
+
*/
|
|
17
|
+
export declare function getExistingContent(filePath: string, cwd: string): string | null;
|
package/dist/tools/files.js
CHANGED
|
@@ -167,6 +167,143 @@ export async function executeTool(name, args, cwd) {
|
|
|
167
167
|
return `Unknown tool: ${name}`;
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Generate a simple unified diff between two strings
|
|
172
|
+
*/
|
|
173
|
+
export function generateDiff(oldContent, newContent, filePath) {
|
|
174
|
+
const oldLines = oldContent.split("\n");
|
|
175
|
+
const newLines = newContent.split("\n");
|
|
176
|
+
const output = [`--- a/${filePath}`, `+++ b/${filePath}`];
|
|
177
|
+
const lcs = computeLCS(oldLines, newLines);
|
|
178
|
+
let oi = 0, ni = 0, li = 0;
|
|
179
|
+
let hunkLines = [];
|
|
180
|
+
let hunkOldCount = 0;
|
|
181
|
+
let hunkNewCount = 0;
|
|
182
|
+
let hunkStartOld = 1;
|
|
183
|
+
let hunkStartNew = 1;
|
|
184
|
+
let pendingContext = [];
|
|
185
|
+
let hasHunk = false;
|
|
186
|
+
function flushHunk() {
|
|
187
|
+
if (hasHunk && hunkLines.length > 0) {
|
|
188
|
+
output.push(`@@ -${hunkStartOld},${hunkOldCount} +${hunkStartNew},${hunkNewCount} @@`);
|
|
189
|
+
output.push(...hunkLines);
|
|
190
|
+
}
|
|
191
|
+
hunkLines = [];
|
|
192
|
+
hunkOldCount = 0;
|
|
193
|
+
hunkNewCount = 0;
|
|
194
|
+
hasHunk = false;
|
|
195
|
+
pendingContext = [];
|
|
196
|
+
}
|
|
197
|
+
function startHunk() {
|
|
198
|
+
if (!hasHunk) {
|
|
199
|
+
hasHunk = true;
|
|
200
|
+
hunkStartOld = Math.max(1, oi + 1 - 3);
|
|
201
|
+
hunkStartNew = Math.max(1, ni + 1 - 3);
|
|
202
|
+
const contextStart = Math.max(0, oi - 3);
|
|
203
|
+
for (let c = contextStart; c < oi; c++) {
|
|
204
|
+
hunkLines.push(` ${oldLines[c]}`);
|
|
205
|
+
hunkOldCount++;
|
|
206
|
+
hunkNewCount++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (pendingContext.length > 0) {
|
|
210
|
+
hunkLines.push(...pendingContext);
|
|
211
|
+
pendingContext = [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
while (oi < oldLines.length || ni < newLines.length) {
|
|
215
|
+
if (li < lcs.length && oi < oldLines.length && ni < newLines.length &&
|
|
216
|
+
oldLines[oi] === lcs[li] && newLines[ni] === lcs[li]) {
|
|
217
|
+
// Matching line
|
|
218
|
+
if (hasHunk) {
|
|
219
|
+
pendingContext.push(` ${oldLines[oi]}`);
|
|
220
|
+
hunkOldCount++;
|
|
221
|
+
hunkNewCount++;
|
|
222
|
+
if (pendingContext.length > 6)
|
|
223
|
+
flushHunk();
|
|
224
|
+
}
|
|
225
|
+
oi++;
|
|
226
|
+
ni++;
|
|
227
|
+
li++;
|
|
228
|
+
}
|
|
229
|
+
else if (oi < oldLines.length && (li >= lcs.length || oldLines[oi] !== lcs[li])) {
|
|
230
|
+
startHunk();
|
|
231
|
+
hunkLines.push(`-${oldLines[oi]}`);
|
|
232
|
+
hunkOldCount++;
|
|
233
|
+
oi++;
|
|
234
|
+
}
|
|
235
|
+
else if (ni < newLines.length && (li >= lcs.length || newLines[ni] !== lcs[li])) {
|
|
236
|
+
startHunk();
|
|
237
|
+
hunkLines.push(`+${newLines[ni]}`);
|
|
238
|
+
hunkNewCount++;
|
|
239
|
+
ni++;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
flushHunk();
|
|
246
|
+
if (output.length <= 2)
|
|
247
|
+
return "(no changes)";
|
|
248
|
+
const maxDiffLines = 60;
|
|
249
|
+
if (output.length > maxDiffLines + 2) {
|
|
250
|
+
return output.slice(0, maxDiffLines + 2).join("\n") + `\n... (${output.length - maxDiffLines - 2} more lines)`;
|
|
251
|
+
}
|
|
252
|
+
return output.join("\n");
|
|
253
|
+
}
|
|
254
|
+
function computeLCS(a, b) {
|
|
255
|
+
if (a.length > 500 || b.length > 500) {
|
|
256
|
+
// For large files, just return common lines in order
|
|
257
|
+
const result = [];
|
|
258
|
+
let bi = 0;
|
|
259
|
+
for (const line of a) {
|
|
260
|
+
while (bi < b.length && b[bi] !== line)
|
|
261
|
+
bi++;
|
|
262
|
+
if (bi < b.length) {
|
|
263
|
+
result.push(line);
|
|
264
|
+
bi++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
const m = a.length, n = b.length;
|
|
270
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
271
|
+
for (let i = 1; i <= m; i++) {
|
|
272
|
+
for (let j = 1; j <= n; j++) {
|
|
273
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const result = [];
|
|
277
|
+
let i = m, j = n;
|
|
278
|
+
while (i > 0 && j > 0) {
|
|
279
|
+
if (a[i - 1] === b[j - 1]) {
|
|
280
|
+
result.unshift(a[i - 1]);
|
|
281
|
+
i--;
|
|
282
|
+
j--;
|
|
283
|
+
}
|
|
284
|
+
else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
285
|
+
i--;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
j--;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get existing file content for diff preview (returns null if file doesn't exist)
|
|
295
|
+
*/
|
|
296
|
+
export function getExistingContent(filePath, cwd) {
|
|
297
|
+
const fullPath = join(cwd, filePath);
|
|
298
|
+
if (!existsSync(fullPath))
|
|
299
|
+
return null;
|
|
300
|
+
try {
|
|
301
|
+
return readFileSync(fullPath, "utf-8");
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
170
307
|
function listDir(dirPath, cwd, recursive = false, depth = 0) {
|
|
171
308
|
const entries = [];
|
|
172
309
|
const IGNORE = ["node_modules", ".git", "dist", ".next", "__pycache__"];
|
package/dist/utils/sessions.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface SessionInfo {
|
|
|
23
23
|
message_count: number;
|
|
24
24
|
token_estimate: number;
|
|
25
25
|
summary: string | null;
|
|
26
|
+
prompt_tokens: number;
|
|
27
|
+
completion_tokens: number;
|
|
28
|
+
estimated_cost: number;
|
|
26
29
|
}
|
|
27
30
|
export declare function listSessions(limit?: number): SessionInfo[];
|
|
28
31
|
/**
|
|
@@ -37,6 +40,10 @@ export declare function getSession(sessionId: string): SessionInfo | null;
|
|
|
37
40
|
* Delete a session and its messages
|
|
38
41
|
*/
|
|
39
42
|
export declare function deleteSession(sessionId: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Update cost tracking for a session
|
|
45
|
+
*/
|
|
46
|
+
export declare function updateSessionCost(sessionId: string, promptTokens: number, completionTokens: number, estimatedCost: number): void;
|
|
40
47
|
/**
|
|
41
48
|
* Update session summary (for context compression)
|
|
42
49
|
*/
|
package/dist/utils/sessions.js
CHANGED
|
@@ -27,7 +27,10 @@ function getDb() {
|
|
|
27
27
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
28
28
|
message_count INTEGER NOT NULL DEFAULT 0,
|
|
29
29
|
token_estimate INTEGER NOT NULL DEFAULT 0,
|
|
30
|
-
summary TEXT
|
|
30
|
+
summary TEXT,
|
|
31
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
estimated_cost REAL NOT NULL DEFAULT 0
|
|
31
34
|
);
|
|
32
35
|
|
|
33
36
|
CREATE TABLE IF NOT EXISTS messages (
|
|
@@ -43,6 +46,19 @@ function getDb() {
|
|
|
43
46
|
|
|
44
47
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
45
48
|
`);
|
|
49
|
+
// Migrate: add cost columns if missing (for existing DBs)
|
|
50
|
+
try {
|
|
51
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN prompt_tokens INTEGER NOT NULL DEFAULT 0`);
|
|
52
|
+
}
|
|
53
|
+
catch { /* column already exists */ }
|
|
54
|
+
try {
|
|
55
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN completion_tokens INTEGER NOT NULL DEFAULT 0`);
|
|
56
|
+
}
|
|
57
|
+
catch { /* column already exists */ }
|
|
58
|
+
try {
|
|
59
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN estimated_cost REAL NOT NULL DEFAULT 0`);
|
|
60
|
+
}
|
|
61
|
+
catch { /* column already exists */ }
|
|
46
62
|
return db;
|
|
47
63
|
}
|
|
48
64
|
/**
|
|
@@ -145,6 +161,15 @@ export function deleteSession(sessionId) {
|
|
|
145
161
|
const result = db.prepare(`DELETE FROM sessions WHERE id = ?`).run(sessionId);
|
|
146
162
|
return result.changes > 0;
|
|
147
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Update cost tracking for a session
|
|
166
|
+
*/
|
|
167
|
+
export function updateSessionCost(sessionId, promptTokens, completionTokens, estimatedCost) {
|
|
168
|
+
const db = getDb();
|
|
169
|
+
db.prepare(`
|
|
170
|
+
UPDATE sessions SET prompt_tokens = ?, completion_tokens = ?, estimated_cost = ? WHERE id = ?
|
|
171
|
+
`).run(promptTokens, completionTokens, estimatedCost, sessionId);
|
|
172
|
+
}
|
|
148
173
|
/**
|
|
149
174
|
* Update session summary (for context compression)
|
|
150
175
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemaxxing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"author": "Marcos Vallejo",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
29
30
|
"@types/react": "^19.2.14",
|
|
30
31
|
"better-sqlite3": "^12.6.2",
|
|
31
32
|
"chalk": "^5.3.0",
|