codemaxxing 0.1.7 → 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/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 +208 -48
- 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 +278 -53
- 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
|
-
import { PROVIDERS, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
|
|
14
|
-
const VERSION = "0.1.
|
|
13
|
+
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
|
|
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,8 +107,13 @@ 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);
|
|
115
|
+
const [loginMethodPicker, setLoginMethodPicker] = useState(null);
|
|
116
|
+
const [loginMethodIndex, setLoginMethodIndex] = useState(0);
|
|
111
117
|
const [approval, setApproval] = useState(null);
|
|
112
118
|
// Listen for paste events from stdin interceptor
|
|
113
119
|
useEffect(() => {
|
|
@@ -208,9 +214,15 @@ function App() {
|
|
|
208
214
|
onGitCommit: (message) => {
|
|
209
215
|
addMsg("info", `📝 Auto-committed: ${message}`);
|
|
210
216
|
},
|
|
211
|
-
|
|
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) => {
|
|
212
224
|
return new Promise((resolve) => {
|
|
213
|
-
setApproval({ tool: name, args, resolve });
|
|
225
|
+
setApproval({ tool: name, args, diff, resolve });
|
|
214
226
|
setLoading(false);
|
|
215
227
|
});
|
|
216
228
|
},
|
|
@@ -250,7 +262,7 @@ function App() {
|
|
|
250
262
|
const selected = matches[idx];
|
|
251
263
|
if (selected) {
|
|
252
264
|
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
253
|
-
if (selected.cmd === "/commit" || selected.cmd === "/model") {
|
|
265
|
+
if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete") {
|
|
254
266
|
setInput(selected.cmd + " ");
|
|
255
267
|
setCmdIndex(0);
|
|
256
268
|
setInputKey((k) => k + 1);
|
|
@@ -293,6 +305,7 @@ function App() {
|
|
|
293
305
|
" /models — list available models",
|
|
294
306
|
" /map — show repository map",
|
|
295
307
|
" /sessions — list past sessions",
|
|
308
|
+
" /session delete — delete a session",
|
|
296
309
|
" /resume — resume a past session",
|
|
297
310
|
" /reset — clear conversation",
|
|
298
311
|
" /context — show message count",
|
|
@@ -407,12 +420,50 @@ function App() {
|
|
|
407
420
|
const tokens = s.token_estimate >= 1000
|
|
408
421
|
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
409
422
|
: String(s.token_estimate);
|
|
410
|
-
|
|
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}`;
|
|
411
427
|
});
|
|
412
428
|
addMsg("info", "Recent sessions:\n" + lines.join("\n") + "\n\n Use /resume <id> to continue a session");
|
|
413
429
|
}
|
|
414
430
|
return;
|
|
415
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
|
+
}
|
|
416
467
|
if (trimmed === "/resume") {
|
|
417
468
|
const sessions = listSessions(10);
|
|
418
469
|
if (sessions.length === 0) {
|
|
@@ -497,83 +548,127 @@ function App() {
|
|
|
497
548
|
return;
|
|
498
549
|
}
|
|
499
550
|
}
|
|
500
|
-
// Login picker navigation
|
|
501
|
-
if (
|
|
502
|
-
const
|
|
551
|
+
// Login method picker navigation (second level — pick auth method)
|
|
552
|
+
if (loginMethodPicker) {
|
|
553
|
+
const methods = loginMethodPicker.methods;
|
|
503
554
|
if (key.upArrow) {
|
|
504
|
-
|
|
555
|
+
setLoginMethodIndex((prev) => (prev - 1 + methods.length) % methods.length);
|
|
505
556
|
return;
|
|
506
557
|
}
|
|
507
558
|
if (key.downArrow) {
|
|
508
|
-
|
|
559
|
+
setLoginMethodIndex((prev) => (prev + 1) % methods.length);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (key.escape) {
|
|
563
|
+
setLoginMethodPicker(null);
|
|
564
|
+
setLoginPicker(true); // go back to provider picker
|
|
509
565
|
return;
|
|
510
566
|
}
|
|
511
567
|
if (key.return) {
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
568
|
+
const method = methods[loginMethodIndex];
|
|
569
|
+
const providerId = loginMethodPicker.provider;
|
|
570
|
+
setLoginMethodPicker(null);
|
|
571
|
+
if (method === "oauth" && providerId === "openrouter") {
|
|
515
572
|
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
516
573
|
setLoading(true);
|
|
517
574
|
setSpinnerMsg("Waiting for authorization...");
|
|
518
575
|
openRouterOAuth((msg) => addMsg("info", msg))
|
|
519
|
-
.then((
|
|
520
|
-
addMsg("info", `✅ OpenRouter authenticated!
|
|
521
|
-
addMsg("info", `Switch with: /model anthropic/claude-sonnet-4`);
|
|
576
|
+
.then(() => {
|
|
577
|
+
addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`);
|
|
522
578
|
setLoading(false);
|
|
523
579
|
})
|
|
524
|
-
.catch((err) => {
|
|
525
|
-
addMsg("error", `OAuth failed: ${err.message}`);
|
|
526
|
-
setLoading(false);
|
|
527
|
-
});
|
|
580
|
+
.catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
528
581
|
}
|
|
529
|
-
else if (
|
|
530
|
-
addMsg("info", "Starting
|
|
582
|
+
else if (method === "setup-token") {
|
|
583
|
+
addMsg("info", "Starting setup-token flow — browser will open...");
|
|
531
584
|
setLoading(true);
|
|
532
585
|
setSpinnerMsg("Waiting for Claude Code auth...");
|
|
533
586
|
anthropicSetupToken((msg) => addMsg("info", msg))
|
|
534
|
-
.then((cred) => {
|
|
535
|
-
addMsg("
|
|
536
|
-
setLoading(false);
|
|
537
|
-
})
|
|
538
|
-
.catch((err) => {
|
|
539
|
-
addMsg("error", `Anthropic auth failed: ${err.message}`);
|
|
540
|
-
setLoading(false);
|
|
541
|
-
});
|
|
587
|
+
.then((cred) => { addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); setLoading(false); })
|
|
588
|
+
.catch((err) => { addMsg("error", `Auth failed: ${err.message}`); setLoading(false); });
|
|
542
589
|
}
|
|
543
|
-
else if (
|
|
590
|
+
else if (method === "cached-token" && providerId === "openai") {
|
|
544
591
|
const imported = importCodexToken((msg) => addMsg("info", msg));
|
|
545
592
|
if (imported) {
|
|
546
593
|
addMsg("info", `✅ Imported Codex credentials! (${imported.label})`);
|
|
547
594
|
}
|
|
548
595
|
else {
|
|
549
|
-
addMsg("info", "No Codex CLI found.
|
|
596
|
+
addMsg("info", "No Codex CLI found. Install Codex CLI and sign in first.");
|
|
550
597
|
}
|
|
551
598
|
}
|
|
552
|
-
else if (
|
|
599
|
+
else if (method === "cached-token" && providerId === "qwen") {
|
|
553
600
|
const imported = importQwenToken((msg) => addMsg("info", msg));
|
|
554
601
|
if (imported) {
|
|
555
602
|
addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`);
|
|
556
603
|
}
|
|
557
604
|
else {
|
|
558
|
-
addMsg("info", "No Qwen CLI found.
|
|
605
|
+
addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first.");
|
|
559
606
|
}
|
|
560
607
|
}
|
|
561
|
-
else if (
|
|
608
|
+
else if (method === "device-flow") {
|
|
562
609
|
addMsg("info", "Starting GitHub Copilot device flow...");
|
|
563
610
|
setLoading(true);
|
|
564
611
|
setSpinnerMsg("Waiting for GitHub authorization...");
|
|
565
612
|
copilotDeviceFlow((msg) => addMsg("info", msg))
|
|
566
|
-
.then(() => {
|
|
567
|
-
addMsg("
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
613
|
+
.then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
|
|
614
|
+
.catch((err) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
615
|
+
}
|
|
616
|
+
else if (method === "api-key") {
|
|
617
|
+
const provider = PROVIDERS.find((p) => p.id === providerId);
|
|
618
|
+
addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${providerId} <your-key>\n Get key at: ${provider?.consoleUrl ?? "your provider's dashboard"}`);
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// Login picker navigation (first level — pick provider)
|
|
625
|
+
if (loginPicker) {
|
|
626
|
+
const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
|
|
627
|
+
if (key.upArrow) {
|
|
628
|
+
setLoginPickerIndex((prev) => (prev - 1 + loginProviders.length) % loginProviders.length);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (key.downArrow) {
|
|
632
|
+
setLoginPickerIndex((prev) => (prev + 1) % loginProviders.length);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (key.return) {
|
|
636
|
+
const selected = loginProviders[loginPickerIndex];
|
|
637
|
+
setLoginPicker(false);
|
|
638
|
+
// Get available methods for this provider (filter out 'none')
|
|
639
|
+
const methods = selected.methods.filter((m) => m !== "none");
|
|
640
|
+
if (methods.length === 1) {
|
|
641
|
+
// Only one method — execute it directly
|
|
642
|
+
setLoginMethodPicker({ provider: selected.id, methods });
|
|
643
|
+
setLoginMethodIndex(0);
|
|
644
|
+
// Simulate Enter press on the single method
|
|
645
|
+
if (methods[0] === "oauth" && selected.id === "openrouter") {
|
|
646
|
+
setLoginMethodPicker(null);
|
|
647
|
+
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
648
|
+
setLoading(true);
|
|
649
|
+
setSpinnerMsg("Waiting for authorization...");
|
|
650
|
+
openRouterOAuth((msg) => addMsg("info", msg))
|
|
651
|
+
.then(() => { addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`); setLoading(false); })
|
|
652
|
+
.catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
653
|
+
}
|
|
654
|
+
else if (methods[0] === "device-flow") {
|
|
655
|
+
setLoginMethodPicker(null);
|
|
656
|
+
addMsg("info", "Starting GitHub Copilot device flow...");
|
|
657
|
+
setLoading(true);
|
|
658
|
+
setSpinnerMsg("Waiting for GitHub authorization...");
|
|
659
|
+
copilotDeviceFlow((msg) => addMsg("info", msg))
|
|
660
|
+
.then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
|
|
661
|
+
.catch((err) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
662
|
+
}
|
|
663
|
+
else if (methods[0] === "api-key") {
|
|
664
|
+
setLoginMethodPicker(null);
|
|
665
|
+
addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${selected.id} <your-key>\n Get key at: ${selected.consoleUrl ?? "your provider's dashboard"}`);
|
|
666
|
+
}
|
|
574
667
|
}
|
|
575
668
|
else {
|
|
576
|
-
|
|
669
|
+
// Multiple methods — show submenu
|
|
670
|
+
setLoginMethodPicker({ provider: selected.id, methods });
|
|
671
|
+
setLoginMethodIndex(0);
|
|
577
672
|
}
|
|
578
673
|
return;
|
|
579
674
|
}
|
|
@@ -651,6 +746,53 @@ function App() {
|
|
|
651
746
|
}
|
|
652
747
|
return; // Ignore other keys during session picker
|
|
653
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
|
+
}
|
|
654
796
|
// Backspace with empty input → remove last paste chunk
|
|
655
797
|
if (key.backspace || key.delete) {
|
|
656
798
|
if (input === "" && pastedChunksRef.current.length > 0) {
|
|
@@ -738,10 +880,28 @@ function App() {
|
|
|
738
880
|
default:
|
|
739
881
|
return _jsx(Text, { children: msg.text }, msg.id);
|
|
740
882
|
}
|
|
741
|
-
}), 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.
|
|
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) => {
|
|
887
|
+
const labels = {
|
|
888
|
+
"oauth": "🌐 Browser login (OAuth)",
|
|
889
|
+
"setup-token": "🔑 Link subscription (via Claude Code CLI)",
|
|
890
|
+
"cached-token": "📦 Import from existing CLI",
|
|
891
|
+
"api-key": "🔒 Enter API key manually",
|
|
892
|
+
"device-flow": "📱 Device flow (GitHub)",
|
|
893
|
+
};
|
|
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));
|
|
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 · ~", (() => {
|
|
742
896
|
const tokens = agent.estimateTokens();
|
|
743
897
|
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
744
|
-
})(), " tokens",
|
|
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}` : ""] }) }))] }));
|
|
745
905
|
}
|
|
746
906
|
// Clear screen before render
|
|
747
907
|
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.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",
|