codemaxxing 0.1.8 → 0.1.9

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 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.5";
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);
@@ -210,9 +214,15 @@ function App() {
210
214
  onGitCommit: (message) => {
211
215
  addMsg("info", `📝 Auto-committed: ${message}`);
212
216
  },
213
- onToolApproval: (name, args) => {
217
+ onContextCompressed: (oldTokens, newTokens) => {
218
+ const saved = oldTokens - newTokens;
219
+ const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
220
+ addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
221
+ },
222
+ contextCompressionThreshold: config.defaults.contextCompressionThreshold,
223
+ onToolApproval: (name, args, diff) => {
214
224
  return new Promise((resolve) => {
215
- setApproval({ tool: name, args, resolve });
225
+ setApproval({ tool: name, args, diff, resolve });
216
226
  setLoading(false);
217
227
  });
218
228
  },
@@ -252,7 +262,7 @@ function App() {
252
262
  const selected = matches[idx];
253
263
  if (selected) {
254
264
  // Commands that need args (like /commit, /model) — fill input instead of executing
255
- if (selected.cmd === "/commit" || selected.cmd === "/model") {
265
+ if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete") {
256
266
  setInput(selected.cmd + " ");
257
267
  setCmdIndex(0);
258
268
  setInputKey((k) => k + 1);
@@ -295,6 +305,7 @@ function App() {
295
305
  " /models — list available models",
296
306
  " /map — show repository map",
297
307
  " /sessions — list past sessions",
308
+ " /session delete — delete a session",
298
309
  " /resume — resume a past session",
299
310
  " /reset — clear conversation",
300
311
  " /context — show message count",
@@ -409,12 +420,50 @@ function App() {
409
420
  const tokens = s.token_estimate >= 1000
410
421
  ? `${(s.token_estimate / 1000).toFixed(1)}k`
411
422
  : String(s.token_estimate);
412
- return ` ${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`;
423
+ const cost = s.estimated_cost > 0
424
+ ? `$${s.estimated_cost < 0.01 ? s.estimated_cost.toFixed(4) : s.estimated_cost.toFixed(2)}`
425
+ : "";
426
+ return ` ${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok${cost ? ` ${cost}` : ""} ${ago} ${s.model}`;
413
427
  });
414
428
  addMsg("info", "Recent sessions:\n" + lines.join("\n") + "\n\n Use /resume <id> to continue a session");
415
429
  }
416
430
  return;
417
431
  }
432
+ if (trimmed.startsWith("/session delete")) {
433
+ const idArg = trimmed.replace("/session delete", "").trim();
434
+ if (idArg) {
435
+ // Direct delete by ID
436
+ const session = getSession(idArg);
437
+ if (!session) {
438
+ addMsg("error", `Session "${idArg}" not found.`);
439
+ return;
440
+ }
441
+ const dir = session.cwd.split("/").pop() || session.cwd;
442
+ setDeleteSessionConfirm({ id: idArg, display: `${idArg} ${dir}/ ${session.message_count} msgs ${session.model}` });
443
+ return;
444
+ }
445
+ // Show picker
446
+ const sessions = listSessions(10);
447
+ if (sessions.length === 0) {
448
+ addMsg("info", "No sessions to delete.");
449
+ return;
450
+ }
451
+ const items = sessions.map((s) => {
452
+ const date = new Date(s.updated_at + "Z");
453
+ const ago = formatTimeAgo(date);
454
+ const dir = s.cwd.split("/").pop() || s.cwd;
455
+ const tokens = s.token_estimate >= 1000
456
+ ? `${(s.token_estimate / 1000).toFixed(1)}k`
457
+ : String(s.token_estimate);
458
+ return {
459
+ id: s.id,
460
+ display: `${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`,
461
+ };
462
+ });
463
+ setDeleteSessionPicker(items);
464
+ setDeleteSessionPickerIndex(0);
465
+ return;
466
+ }
418
467
  if (trimmed === "/resume") {
419
468
  const sessions = listSessions(10);
420
469
  if (sessions.length === 0) {
@@ -697,6 +746,53 @@ function App() {
697
746
  }
698
747
  return; // Ignore other keys during session picker
699
748
  }
749
+ // Delete session confirmation (y/n)
750
+ if (deleteSessionConfirm) {
751
+ if (inputChar === "y" || inputChar === "Y") {
752
+ const deleted = deleteSession(deleteSessionConfirm.id);
753
+ if (deleted) {
754
+ addMsg("info", `✅ Deleted session ${deleteSessionConfirm.id}`);
755
+ }
756
+ else {
757
+ addMsg("error", `Failed to delete session ${deleteSessionConfirm.id}`);
758
+ }
759
+ setDeleteSessionConfirm(null);
760
+ return;
761
+ }
762
+ if (inputChar === "n" || inputChar === "N" || key.escape) {
763
+ addMsg("info", "Delete cancelled.");
764
+ setDeleteSessionConfirm(null);
765
+ return;
766
+ }
767
+ return;
768
+ }
769
+ // Delete session picker navigation
770
+ if (deleteSessionPicker) {
771
+ if (key.upArrow) {
772
+ setDeleteSessionPickerIndex((prev) => (prev - 1 + deleteSessionPicker.length) % deleteSessionPicker.length);
773
+ return;
774
+ }
775
+ if (key.downArrow) {
776
+ setDeleteSessionPickerIndex((prev) => (prev + 1) % deleteSessionPicker.length);
777
+ return;
778
+ }
779
+ if (key.return) {
780
+ const selected = deleteSessionPicker[deleteSessionPickerIndex];
781
+ if (selected) {
782
+ setDeleteSessionPicker(null);
783
+ setDeleteSessionPickerIndex(0);
784
+ setDeleteSessionConfirm(selected);
785
+ }
786
+ return;
787
+ }
788
+ if (key.escape) {
789
+ setDeleteSessionPicker(null);
790
+ setDeleteSessionPickerIndex(0);
791
+ addMsg("info", "Delete cancelled.");
792
+ return;
793
+ }
794
+ return;
795
+ }
700
796
  // Backspace with empty input → remove last paste chunk
701
797
  if (key.backspace || key.delete) {
702
798
  if (input === "" && pastedChunksRef.current.length > 0) {
@@ -784,7 +880,10 @@ function App() {
784
880
  default:
785
881
  return _jsx(Text, { children: msg.text }, msg.id);
786
882
  }
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.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) => {
883
+ }), 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 :
884
+ line.startsWith("-") ? theme.colors.error :
885
+ line.startsWith("@@") ? theme.colors.primary :
886
+ 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
887
  const labels = {
789
888
  "oauth": "🌐 Browser login (OAuth)",
790
889
  "setup-token": "🔑 Link subscription (via Claude Code CLI)",
@@ -793,10 +892,16 @@ function App() {
793
892
  "device-flow": "📱 Device flow (GitHub)",
794
893
  };
795
894
  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 · ~", (() => {
895
+ }), _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
896
  const tokens = agent.estimateTokens();
798
897
  return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
799
- })(), " tokens", modelName ? ` · 🤖 ${modelName}` : ""] }) }))] }));
898
+ })(), " tokens", (() => {
899
+ const { totalCost } = agent.getCostInfo();
900
+ if (totalCost > 0) {
901
+ return ` · 💰 $${totalCost < 0.01 ? totalCost.toFixed(4) : totalCost.toFixed(2)}`;
902
+ }
903
+ return "";
904
+ })(), modelName ? ` · 🤖 ${modelName}` : ""] }) }))] }));
800
905
  }
801
906
  // Clear screen before render
802
907
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
@@ -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;
@@ -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__"];
@@ -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
  */
@@ -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.8",
3
+ "version": "0.1.9",
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",