codemaxxing 1.0.0 → 1.0.2

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.
Files changed (47) hide show
  1. package/README.md +18 -12
  2. package/dist/agent.d.ts +4 -0
  3. package/dist/agent.js +91 -16
  4. package/dist/commands/git.d.ts +2 -0
  5. package/dist/commands/git.js +50 -0
  6. package/dist/commands/ollama.d.ts +27 -0
  7. package/dist/commands/ollama.js +171 -0
  8. package/dist/commands/output.d.ts +2 -0
  9. package/dist/commands/output.js +18 -0
  10. package/dist/commands/registry.d.ts +2 -0
  11. package/dist/commands/registry.js +8 -0
  12. package/dist/commands/skills.d.ts +18 -0
  13. package/dist/commands/skills.js +121 -0
  14. package/dist/commands/types.d.ts +5 -0
  15. package/dist/commands/types.js +1 -0
  16. package/dist/commands/ui.d.ts +16 -0
  17. package/dist/commands/ui.js +79 -0
  18. package/dist/config.d.ts +9 -0
  19. package/dist/config.js +13 -3
  20. package/dist/exec.js +4 -1
  21. package/dist/index.js +75 -401
  22. package/dist/tools/files.js +58 -3
  23. package/dist/utils/context.js +6 -0
  24. package/dist/utils/mcp.d.ts +7 -2
  25. package/dist/utils/mcp.js +34 -6
  26. package/package.json +8 -5
  27. package/src/agent.ts +0 -894
  28. package/src/auth-cli.ts +0 -287
  29. package/src/cli.ts +0 -37
  30. package/src/config.ts +0 -352
  31. package/src/exec.ts +0 -183
  32. package/src/index.tsx +0 -2647
  33. package/src/skills/registry.ts +0 -1436
  34. package/src/themes.ts +0 -335
  35. package/src/tools/files.ts +0 -374
  36. package/src/utils/auth.ts +0 -606
  37. package/src/utils/context.ts +0 -174
  38. package/src/utils/git.ts +0 -117
  39. package/src/utils/hardware.ts +0 -131
  40. package/src/utils/lint.ts +0 -116
  41. package/src/utils/mcp.ts +0 -307
  42. package/src/utils/models.ts +0 -218
  43. package/src/utils/ollama.ts +0 -352
  44. package/src/utils/repomap.ts +0 -220
  45. package/src/utils/sessions.ts +0 -254
  46. package/src/utils/skills.ts +0 -241
  47. package/tsconfig.json +0 -16
package/dist/index.js CHANGED
@@ -7,15 +7,19 @@ import TextInput from "ink-text-input";
7
7
  import { CodingAgent } from "./agent.js";
8
8
  import { loadConfig, saveConfig, detectLocalProvider, detectLocalProviderDetailed, parseCLIArgs, applyOverrides, listModels } from "./config.js";
9
9
  import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
10
- import { execSync } from "child_process";
11
- import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
10
+ import { isGitRepo, getBranch, getStatus } from "./utils/git.js";
11
+ import { tryHandleGitCommand } from "./commands/git.js";
12
+ import { tryHandleOllamaCommand } from "./commands/ollama.js";
13
+ import { dispatchRegisteredCommands } from "./commands/registry.js";
12
14
  import { getTheme, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
15
+ import { tryHandleUiCommand } from "./commands/ui.js";
13
16
  import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
14
- import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
17
+ import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
15
18
  import { listServers, addServer, removeServer, getConnectedServers } from "./utils/mcp.js";
19
+ import { tryHandleSkillsCommand } from "./commands/skills.js";
16
20
  import { detectHardware, formatBytes } from "./utils/hardware.js";
17
21
  import { getRecommendationsWithLlmfit, getFitIcon, isLlmfitAvailable } from "./utils/models.js";
18
- import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, deleteModel, getGPUMemoryUsage } from "./utils/ollama.js";
22
+ import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, deleteModel } from "./utils/ollama.js";
19
23
  import { createRequire } from "module";
20
24
  const _require = createRequire(import.meta.url);
21
25
  const VERSION = _require("../package.json").version;
@@ -54,12 +58,6 @@ const SLASH_COMMANDS = [
54
58
  { cmd: "/session delete", desc: "delete a session" },
55
59
  { cmd: "/resume", desc: "resume a past session" },
56
60
  { cmd: "/skills", desc: "manage skill packs" },
57
- { cmd: "/skills install", desc: "install a skill" },
58
- { cmd: "/skills remove", desc: "remove a skill" },
59
- { cmd: "/skills list", desc: "show installed skills" },
60
- { cmd: "/skills search", desc: "search registry" },
61
- { cmd: "/skills on", desc: "enable skill for session" },
62
- { cmd: "/skills off", desc: "disable skill for session" },
63
61
  { cmd: "/architect", desc: "toggle architect mode" },
64
62
  { cmd: "/lint", desc: "show auto-lint status" },
65
63
  { cmd: "/lint on", desc: "enable auto-lint" },
@@ -70,6 +68,7 @@ const SLASH_COMMANDS = [
70
68
  { cmd: "/mcp remove", desc: "remove MCP server" },
71
69
  { cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
72
70
  { cmd: "/ollama", desc: "Ollama status & models" },
71
+ { cmd: "/ollama status", desc: "show Ollama status & models" },
73
72
  { cmd: "/ollama list", desc: "list installed models" },
74
73
  { cmd: "/ollama start", desc: "start Ollama server" },
75
74
  { cmd: "/ollama stop", desc: "stop Ollama server" },
@@ -407,8 +406,7 @@ function App() {
407
406
  if (selected) {
408
407
  // Commands that need args (like /commit, /model) — fill input instead of executing
409
408
  if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
410
- selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
411
- selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
409
+ selected.cmd === "/architect") {
412
410
  setInput(selected.cmd + " ");
413
411
  setCmdIndex(0);
414
412
  setInputKey((k) => k + 1);
@@ -502,346 +500,42 @@ function App() {
502
500
  ].join("\n"));
503
501
  return;
504
502
  }
505
- // ── Skills commands (work without agent) ──
506
- if (trimmed === "/skills") {
507
- setSkillsPicker("menu");
508
- setSkillsPickerIndex(0);
509
- return;
510
- }
511
- if (trimmed.startsWith("/skills install ")) {
512
- const name = trimmed.replace("/skills install ", "").trim();
513
- const result = installSkill(name);
514
- addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
515
- return;
516
- }
517
- if (trimmed.startsWith("/skills remove ")) {
518
- const name = trimmed.replace("/skills remove ", "").trim();
519
- const result = removeSkill(name);
520
- addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
521
- return;
522
- }
523
- if (trimmed === "/skills list") {
524
- const installed = listInstalledSkills();
525
- if (installed.length === 0) {
526
- addMsg("info", "No skills installed. Use /skills to browse & install.");
527
- }
528
- else {
529
- const active = getActiveSkills(process.cwd(), sessionDisabledSkills);
530
- const lines = installed.map((s) => {
531
- const isActive = active.includes(s.name);
532
- const disabledBySession = sessionDisabledSkills.has(s.name);
533
- const status = disabledBySession ? " (off)" : isActive ? " (on)" : "";
534
- return ` ${isActive ? "●" : "○"} ${s.name} — ${s.description}${status}`;
535
- });
536
- addMsg("info", `Installed skills:\n${lines.join("\n")}`);
537
- }
538
- return;
539
- }
540
- if (trimmed.startsWith("/skills search ")) {
541
- const query = trimmed.replace("/skills search ", "").trim();
542
- const results = searchRegistry(query);
543
- if (results.length === 0) {
544
- addMsg("info", `No skills found matching "${query}".`);
545
- }
546
- else {
547
- const installed = listInstalledSkills().map((s) => s.name);
548
- const lines = results.map((s) => {
549
- const mark = installed.includes(s.name) ? " ✓" : "";
550
- return ` ${s.name} — ${s.description}${mark}`;
551
- });
552
- addMsg("info", `Registry matches:\n${lines.join("\n")}`);
553
- }
554
- return;
555
- }
556
- if (trimmed.startsWith("/skills create ")) {
557
- const name = trimmed.replace("/skills create ", "").trim();
558
- if (!name) {
559
- addMsg("info", "Usage: /skills create <name>");
560
- return;
561
- }
562
- const result = createSkillScaffold(name);
563
- addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}\n Edit: ${result.path}/prompt.md` : `✗ ${result.message}`);
564
- return;
565
- }
566
- if (trimmed.startsWith("/skills on ")) {
567
- const name = trimmed.replace("/skills on ", "").trim();
568
- const installed = listInstalledSkills().map((s) => s.name);
569
- if (!installed.includes(name)) {
570
- addMsg("error", `Skill "${name}" is not installed.`);
571
- return;
572
- }
573
- setSessionDisabledSkills((prev) => { const next = new Set(prev); next.delete(name); return next; });
574
- if (agent)
575
- agent.enableSkill(name);
576
- addMsg("info", `✅ Enabled skill: ${name}`);
577
- return;
578
- }
579
- if (trimmed.startsWith("/skills off ")) {
580
- const name = trimmed.replace("/skills off ", "").trim();
581
- const installed = listInstalledSkills().map((s) => s.name);
582
- if (!installed.includes(name)) {
583
- addMsg("error", `Skill "${name}" is not installed.`);
584
- return;
585
- }
586
- setSessionDisabledSkills((prev) => { const next = new Set(prev); next.add(name); return next; });
587
- if (agent)
588
- agent.disableSkill(name);
589
- addMsg("info", `✅ Disabled skill: ${name} (session only)`);
590
- return;
591
- }
592
- if (trimmed.startsWith("/theme")) {
593
- const themeName = trimmed.replace("/theme", "").trim();
594
- if (!themeName) {
595
- const themeKeys = listThemes();
596
- const currentIdx = themeKeys.indexOf(theme.name.toLowerCase());
597
- setThemePicker(true);
598
- setThemePickerIndex(currentIdx >= 0 ? currentIdx : 0);
599
- return;
600
- }
601
- if (!THEMES[themeName]) {
602
- addMsg("error", `Theme "${themeName}" not found. Use /theme to see available themes.`);
603
- return;
604
- }
605
- setTheme(getTheme(themeName));
606
- addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
607
- return;
608
- }
609
- // ── Architect commands (work without agent) ──
610
- if (trimmed === "/architect") {
611
- if (!agent) {
612
- addMsg("info", "🏗️ Architect mode: no agent connected. Connect first with /login or /connect.");
613
- return;
614
- }
615
- const current = agent.getArchitectModel();
616
- if (current) {
617
- agent.setArchitectModel(null);
618
- addMsg("info", "🏗️ Architect mode OFF");
619
- }
620
- else {
621
- // Use config default or a sensible default
622
- const defaultModel = loadConfig().defaults.architectModel || agent.getModel();
623
- agent.setArchitectModel(defaultModel);
624
- addMsg("info", `🏗️ Architect mode ON (planner: ${defaultModel})`);
625
- }
626
- return;
627
- }
628
- if (trimmed.startsWith("/architect ")) {
629
- const model = trimmed.replace("/architect ", "").trim();
630
- if (!model) {
631
- addMsg("info", "Usage: /architect <model> or /architect to toggle");
632
- return;
633
- }
634
- if (agent) {
635
- agent.setArchitectModel(model);
636
- addMsg("info", `🏗️ Architect mode ON (planner: ${model})`);
637
- }
638
- else {
639
- addMsg("info", "⚠ No agent connected. Connect first.");
640
- }
641
- return;
642
- }
643
- // ── Lint commands (work without agent) ──
644
- if (trimmed === "/lint") {
645
- const { detectLinter } = await import("./utils/lint.js");
646
- const linter = detectLinter(process.cwd());
647
- const enabled = agent ? agent.isAutoLintEnabled() : true;
648
- if (linter) {
649
- addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n Detected: ${linter.name}\n Command: ${linter.command} <file>`);
650
- }
651
- else {
652
- addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n No linter detected in this project.`);
653
- }
654
- return;
655
- }
656
- if (trimmed === "/lint on") {
657
- if (agent)
658
- agent.setAutoLint(true);
659
- addMsg("info", "🔍 Auto-lint ON");
660
- return;
661
- }
662
- if (trimmed === "/lint off") {
663
- if (agent)
664
- agent.setAutoLint(false);
665
- addMsg("info", "🔍 Auto-lint OFF");
666
- return;
667
- }
668
- // ── Ollama commands (work without agent) ──
669
- if (trimmed === "/ollama" || trimmed === "/ollama status") {
670
- const running = await isOllamaRunning();
671
- const lines = [`Ollama: ${running ? "running" : "stopped"}`];
672
- if (running) {
673
- const models = await listInstalledModelsDetailed();
674
- if (models.length > 0) {
675
- lines.push(`Installed models (${models.length}):`);
676
- for (const m of models) {
677
- const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
678
- lines.push(` ${m.name} (${sizeGB} GB)`);
679
- }
680
- }
681
- else {
682
- lines.push("No models installed.");
683
- }
684
- const gpuMem = getGPUMemoryUsage();
685
- if (gpuMem)
686
- lines.push(`GPU: ${gpuMem}`);
687
- }
688
- else {
689
- lines.push("Start with: /ollama start");
690
- }
691
- addMsg("info", lines.join("\n"));
692
- return;
693
- }
694
- if (trimmed === "/ollama list") {
695
- const running = await isOllamaRunning();
696
- if (!running) {
697
- addMsg("info", "Ollama is not running. Start with /ollama start");
698
- return;
699
- }
700
- const models = await listInstalledModelsDetailed();
701
- if (models.length === 0) {
702
- addMsg("info", "No models installed. Pull one with /ollama pull <model>");
703
- }
704
- else {
705
- const lines = models.map((m) => {
706
- const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
707
- return ` ${m.name} (${sizeGB} GB)`;
708
- });
709
- addMsg("info", `Installed models:\n${lines.join("\n")}`);
710
- }
711
- return;
712
- }
713
- if (trimmed === "/ollama start") {
714
- const running = await isOllamaRunning();
715
- if (running) {
716
- addMsg("info", "Ollama is already running.");
717
- return;
718
- }
719
- if (!isOllamaInstalled()) {
720
- addMsg("error", `Ollama is not installed. Install with: ${getOllamaInstallCommand(process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")}`);
721
- return;
722
- }
723
- startOllama();
724
- addMsg("info", "Starting Ollama server...");
725
- // Wait for it to come up
726
- for (let i = 0; i < 10; i++) {
727
- await new Promise(r => setTimeout(r, 1000));
728
- if (await isOllamaRunning()) {
729
- addMsg("info", "Ollama is running.");
730
- await refreshConnectionBanner();
731
- return;
732
- }
733
- }
734
- addMsg("error", "Ollama did not start in time. Try running 'ollama serve' manually.");
735
- return;
736
- }
737
- if (trimmed === "/ollama stop") {
738
- const running = await isOllamaRunning();
739
- if (!running) {
740
- addMsg("info", "Ollama is not running.");
741
- return;
742
- }
743
- addMsg("info", "Stopping Ollama...");
744
- const result = await stopOllama();
745
- addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
746
- if (result.ok)
747
- await refreshConnectionBanner();
748
- return;
749
- }
750
- if (trimmed === "/ollama pull") {
751
- // No model specified — show picker
752
- setOllamaPullPicker(true);
753
- setOllamaPullPickerIndex(0);
754
- return;
755
- }
756
- if (trimmed.startsWith("/ollama pull ")) {
757
- const modelId = trimmed.replace("/ollama pull ", "").trim();
758
- if (!modelId) {
759
- setOllamaPullPicker(true);
760
- setOllamaPullPickerIndex(0);
761
- return;
762
- }
763
- if (!isOllamaInstalled()) {
764
- addMsg("error", "Ollama is not installed.");
765
- return;
766
- }
767
- // Ensure ollama is running
768
- let running = await isOllamaRunning();
769
- if (!running) {
770
- startOllama();
771
- addMsg("info", "Starting Ollama server...");
772
- for (let i = 0; i < 10; i++) {
773
- await new Promise(r => setTimeout(r, 1000));
774
- if (await isOllamaRunning()) {
775
- running = true;
776
- break;
777
- }
778
- }
779
- if (!running) {
780
- addMsg("error", "Could not start Ollama. Run 'ollama serve' manually.");
781
- return;
782
- }
783
- }
784
- setOllamaPulling({ model: modelId, progress: { status: "starting", percent: 0 } });
785
- try {
786
- await pullModel(modelId, (p) => {
787
- setOllamaPulling({ model: modelId, progress: p });
788
- });
789
- setOllamaPulling(null);
790
- addMsg("info", `\u2705 Downloaded ${modelId}`);
791
- }
792
- catch (err) {
793
- setOllamaPulling(null);
794
- addMsg("error", `Failed to pull ${modelId}: ${err.message}`);
795
- }
796
- return;
797
- }
798
- if (trimmed === "/ollama delete") {
799
- // Ensure Ollama is running so we can list models
800
- let running = await isOllamaRunning();
801
- if (!running) {
802
- addMsg("info", "Starting Ollama to list models...");
803
- startOllama();
804
- for (let i = 0; i < 10; i++) {
805
- await new Promise(r => setTimeout(r, 1000));
806
- if (await isOllamaRunning()) {
807
- running = true;
808
- break;
809
- }
810
- }
811
- if (!running) {
812
- addMsg("error", "Could not start Ollama. Start it manually first.");
813
- return;
814
- }
815
- }
816
- const models = await listInstalledModelsDetailed();
817
- if (models.length === 0) {
818
- addMsg("info", "No models installed.");
819
- return;
820
- }
821
- setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
822
- setOllamaDeletePickerIndex(0);
823
- return;
824
- }
825
- if (trimmed.startsWith("/ollama delete ")) {
826
- const modelId = trimmed.replace("/ollama delete ", "").trim();
827
- if (!modelId) {
828
- const models = await listInstalledModelsDetailed();
829
- if (models.length === 0) {
830
- addMsg("info", "No models installed.");
831
- return;
832
- }
833
- setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
834
- setOllamaDeletePickerIndex(0);
835
- return;
836
- }
837
- // Look up size for confirmation
838
- const models = await listInstalledModelsDetailed();
839
- const found = models.find((m) => m.name === modelId || m.name.startsWith(modelId));
840
- if (!found) {
841
- addMsg("error", `Model "${modelId}" not found. Use /ollama list to see installed models.`);
842
- return;
843
- }
844
- setOllamaDeleteConfirm({ model: found.name, size: found.size });
503
+ if (await dispatchRegisteredCommands([
504
+ () => tryHandleSkillsCommand({
505
+ trimmed,
506
+ cwd: process.cwd(),
507
+ addMsg,
508
+ agent,
509
+ sessionDisabledSkills,
510
+ setSkillsPicker,
511
+ setSkillsPickerIndex,
512
+ setSessionDisabledSkills,
513
+ setInput,
514
+ setInputKey,
515
+ }),
516
+ () => tryHandleOllamaCommand({
517
+ trimmed,
518
+ addMsg,
519
+ refreshConnectionBanner,
520
+ setOllamaPullPicker,
521
+ setOllamaPullPickerIndex,
522
+ setOllamaPulling,
523
+ setOllamaDeletePicker,
524
+ setOllamaDeletePickerIndex,
525
+ setOllamaDeleteConfirm,
526
+ }),
527
+ () => tryHandleGitCommand(trimmed, process.cwd(), addMsg),
528
+ () => tryHandleUiCommand({
529
+ trimmed,
530
+ cwd: process.cwd(),
531
+ addMsg,
532
+ agent,
533
+ theme,
534
+ setTheme,
535
+ setThemePicker,
536
+ setThemePickerIndex,
537
+ }),
538
+ ], { trimmed })) {
845
539
  return;
846
540
  }
847
541
  // ── MCP commands (partially work without agent) ──
@@ -992,16 +686,6 @@ function App() {
992
686
  }
993
687
  return;
994
688
  }
995
- if (trimmed === "/diff") {
996
- const diff = getDiff(process.cwd());
997
- addMsg("info", diff);
998
- return;
999
- }
1000
- if (trimmed === "/undo") {
1001
- const result = undoLastCommit(process.cwd());
1002
- addMsg("info", result.success ? `✅ ${result.message}` : `✗ ${result.message}`);
1003
- return;
1004
- }
1005
689
  if (trimmed === "/git on") {
1006
690
  if (!agent.isGitEnabled()) {
1007
691
  addMsg("info", "✗ Not a git repository");
@@ -1096,32 +780,6 @@ function App() {
1096
780
  setSessionPickerIndex(0);
1097
781
  return;
1098
782
  }
1099
- if (trimmed === "/push") {
1100
- try {
1101
- const output = execSync("git push", { cwd: process.cwd(), encoding: "utf-8", stdio: "pipe" });
1102
- addMsg("info", `✅ Pushed to remote${output.trim() ? "\n" + output.trim() : ""}`);
1103
- }
1104
- catch (e) {
1105
- addMsg("error", `Push failed: ${e.stderr || e.message}`);
1106
- }
1107
- return;
1108
- }
1109
- if (trimmed.startsWith("/commit")) {
1110
- const msg = trimmed.replace("/commit", "").trim();
1111
- if (!msg) {
1112
- addMsg("info", "Usage: /commit your commit message here");
1113
- return;
1114
- }
1115
- try {
1116
- execSync("git add -A", { cwd: process.cwd(), stdio: "pipe" });
1117
- execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: process.cwd(), stdio: "pipe" });
1118
- addMsg("info", `✅ Committed: ${msg}`);
1119
- }
1120
- catch (e) {
1121
- addMsg("error", `Commit failed: ${e.stderr || e.message}`);
1122
- }
1123
- return;
1124
- }
1125
783
  setLoading(true);
1126
784
  setStreaming(false);
1127
785
  setSpinnerMsg(SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)]);
@@ -2090,6 +1748,30 @@ process.stdout.write("\x1b[?2004h");
2090
1748
  // Bracketed paste: \x1b[200~ ... \x1b[201~
2091
1749
  let pasteBuffer = "";
2092
1750
  let inPaste = false;
1751
+ function emitPasteChunk(content) {
1752
+ const normalized = content.replace(/\r\n/g, "\n").trim();
1753
+ if (!normalized)
1754
+ return true;
1755
+ const lineCount = normalized.split("\n").length;
1756
+ if (lineCount > 2) {
1757
+ pasteEvents.emit("paste", { content: normalized, lines: lineCount });
1758
+ return true;
1759
+ }
1760
+ const sanitized = normalized.replace(/\n/g, " ");
1761
+ if (sanitized) {
1762
+ origPush(sanitized, "utf-8");
1763
+ }
1764
+ return true;
1765
+ }
1766
+ function looksLikeRawMultilinePaste(data) {
1767
+ const normalized = data.replace(/\x1b\[20[01]~/g, "");
1768
+ const newlineMatches = normalized.match(/\r?\n/g) ?? [];
1769
+ const newlineCount = newlineMatches.length;
1770
+ const contentLength = normalized.replace(/[\r\n]/g, "").trim().length;
1771
+ // Avoid swallowing normal Enter presses while still catching terminals that
1772
+ // don't support bracketed paste and dump the whole paste as one raw chunk.
1773
+ return newlineCount >= 2 || (newlineCount >= 1 && contentLength >= 40);
1774
+ }
2093
1775
  const origPush = process.stdin.push.bind(process.stdin);
2094
1776
  process.stdin.push = function (chunk, encoding) {
2095
1777
  if (chunk === null)
@@ -2105,26 +1787,18 @@ process.stdin.push = function (chunk, encoding) {
2105
1787
  data = data.replace(/\x1b\[201~/g, "");
2106
1788
  pasteBuffer += data;
2107
1789
  inPaste = false;
2108
- const content = pasteBuffer.trim();
1790
+ const content = pasteBuffer;
2109
1791
  pasteBuffer = "";
2110
- const lineCount = content.split("\n").length;
2111
- if (lineCount > 2) {
2112
- // Multi-line paste → store as chunk, don't send to input
2113
- pasteEvents.emit("paste", { content, lines: lineCount });
2114
- return true;
2115
- }
2116
- // Short paste (1-2 lines) — send as normal input
2117
- const sanitized = content.replace(/\r?\n/g, " ");
2118
- if (sanitized) {
2119
- return origPush(sanitized, "utf-8");
2120
- }
2121
- return true;
1792
+ return emitPasteChunk(content);
2122
1793
  }
2123
1794
  if (inPaste) {
2124
1795
  pasteBuffer += data;
2125
1796
  return true;
2126
1797
  }
2127
1798
  data = data.replace(/\x1b\[20[01]~/g, "");
1799
+ if (looksLikeRawMultilinePaste(data)) {
1800
+ return emitPasteChunk(data);
1801
+ }
2128
1802
  return origPush(typeof chunk === "string" ? data : Buffer.from(data), encoding);
2129
1803
  };
2130
1804
  // Disable bracketed paste on exit
@@ -1,5 +1,5 @@
1
- import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "fs";
2
- import { join, relative } from "path";
1
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from "fs";
2
+ import { join, relative, dirname } from "path";
3
3
  /**
4
4
  * Tool definitions for the OpenAI function calling API
5
5
  */
@@ -25,7 +25,7 @@ export const FILE_TOOLS = [
25
25
  type: "function",
26
26
  function: {
27
27
  name: "write_file",
28
- description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does.",
28
+ description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Use this for new files or full rewrites only.",
29
29
  parameters: {
30
30
  type: "object",
31
31
  properties: {
@@ -42,6 +42,35 @@ export const FILE_TOOLS = [
42
42
  },
43
43
  },
44
44
  },
45
+ {
46
+ type: "function",
47
+ function: {
48
+ name: "edit_file",
49
+ description: "Edit an existing file by replacing exact text. Prefer this over write_file for small or localized changes.",
50
+ parameters: {
51
+ type: "object",
52
+ properties: {
53
+ path: {
54
+ type: "string",
55
+ description: "Path to the file to edit (relative to project root)",
56
+ },
57
+ oldText: {
58
+ type: "string",
59
+ description: "Exact text to find in the file",
60
+ },
61
+ newText: {
62
+ type: "string",
63
+ description: "Replacement text",
64
+ },
65
+ replaceAll: {
66
+ type: "boolean",
67
+ description: "Replace all exact matches instead of only the first one (default: false)",
68
+ },
69
+ },
70
+ required: ["path", "oldText", "newText"],
71
+ },
72
+ },
73
+ },
45
74
  {
46
75
  type: "function",
47
76
  function: {
@@ -120,6 +149,7 @@ export async function executeTool(name, args, cwd) {
120
149
  case "write_file": {
121
150
  const filePath = join(cwd, args.path);
122
151
  try {
152
+ mkdirSync(dirname(filePath), { recursive: true });
123
153
  writeFileSync(filePath, args.content, "utf-8");
124
154
  return `✅ Wrote ${args.content.length} bytes to ${args.path}`;
125
155
  }
@@ -127,6 +157,31 @@ export async function executeTool(name, args, cwd) {
127
157
  return `Error writing file: ${e}`;
128
158
  }
129
159
  }
160
+ case "edit_file": {
161
+ const filePath = join(cwd, args.path);
162
+ if (!existsSync(filePath))
163
+ return `Error: File not found: ${args.path}`;
164
+ try {
165
+ const oldText = String(args.oldText ?? "");
166
+ const newText = String(args.newText ?? "");
167
+ const replaceAll = Boolean(args.replaceAll);
168
+ const content = readFileSync(filePath, "utf-8");
169
+ if (!oldText)
170
+ return "Error: oldText cannot be empty.";
171
+ if (!content.includes(oldText)) {
172
+ return `Error: Could not find exact text in ${args.path}`;
173
+ }
174
+ const matchCount = content.split(oldText).length - 1;
175
+ const nextContent = replaceAll
176
+ ? content.split(oldText).join(newText)
177
+ : content.replace(oldText, newText);
178
+ writeFileSync(filePath, nextContent, "utf-8");
179
+ return `✅ Edited ${args.path} (${replaceAll ? matchCount : 1} replacement${replaceAll ? (matchCount === 1 ? "" : "s") : ""})`;
180
+ }
181
+ catch (e) {
182
+ return `Error editing file: ${e}`;
183
+ }
184
+ }
130
185
  case "list_files": {
131
186
  const dirPath = join(cwd, args.path || ".");
132
187
  if (!existsSync(dirPath))
@@ -116,6 +116,12 @@ You help developers understand, write, debug, and refactor code. You have access
116
116
  - Use the run_command tool for building, testing, and linters
117
117
  - Never delete files without explicit confirmation
118
118
 
119
+ ## Editing Strategy
120
+ - Prefer edit_file for small or localized changes — use it when you only need to change part of a file.
121
+ - edit_file requires the exact text to be found in the file, so read the file first to confirm the exact content.
122
+ - Use write_file only when creating a new file or when the changes affect most of the file.
123
+ - When in doubt about scope, prefer edit_file — it is safer and easier to review.
124
+
119
125
  ## Repository Map
120
126
  The project context below includes a map of the codebase structure. Use this map to understand what files, functions, classes, and types exist where. Use read_file to see full implementations when needed.
121
127
 
@@ -27,11 +27,16 @@ export declare function loadMCPConfig(cwd: string): MCPConfig;
27
27
  export declare function connectToServers(config: MCPConfig, onStatus?: (name: string, status: string) => void): Promise<ConnectedServer[]>;
28
28
  export declare function disconnectAll(): Promise<void>;
29
29
  export declare function getConnectedServers(): ConnectedServer[];
30
+ export declare function buildMCPToolName(serverName: string, toolName: string): string;
30
31
  export declare function getAllMCPTools(servers: ConnectedServer[]): ChatCompletionTool[];
31
32
  /**
32
33
  * Parse an MCP tool call name to extract server name and tool name.
33
- * Format: mcp_<serverName>_<toolName>
34
- * Server names can contain hyphens but not underscores (by convention).
34
+ *
35
+ * New format (collision-safe):
36
+ * mcp_<base64url(server)>__<base64url(tool)>
37
+ *
38
+ * Legacy format still supported for backwards compatibility:
39
+ * mcp_<serverName>_<toolName>
35
40
  */
36
41
  export declare function parseMCPToolName(fullName: string): {
37
42
  serverName: string;