macha-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/macha.js +2 -0
- package/dist/App.js +134 -0
- package/dist/ai/client.js +150 -0
- package/dist/ai/tools.js +313 -0
- package/dist/components/FileTree.js +105 -0
- package/dist/components/Header.js +6 -0
- package/dist/components/Input.js +50 -0
- package/dist/components/MessageList.js +43 -0
- package/dist/components/Settings.js +102 -0
- package/dist/components/ToolCall.js +60 -0
- package/dist/config/index.js +98 -0
- package/dist/index.js +114 -0
- package/dist/mcp/client.js +107 -0
- package/package.json +61 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
import path from "path";
|
|
6
|
+
async function buildTree(dir, depth = 0, maxDepth = 3) {
|
|
7
|
+
if (depth >= maxDepth)
|
|
8
|
+
return [];
|
|
9
|
+
const IGNORE = new Set([
|
|
10
|
+
"node_modules",
|
|
11
|
+
".git",
|
|
12
|
+
"dist",
|
|
13
|
+
".next",
|
|
14
|
+
"__pycache__",
|
|
15
|
+
".cache",
|
|
16
|
+
"build",
|
|
17
|
+
".turbo",
|
|
18
|
+
]);
|
|
19
|
+
try {
|
|
20
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
21
|
+
const nodes = [];
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (IGNORE.has(entry.name) || entry.name.startsWith("."))
|
|
24
|
+
continue;
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
const children = depth < maxDepth - 1
|
|
27
|
+
? await buildTree(path.join(dir, entry.name), depth + 1, maxDepth)
|
|
28
|
+
: [];
|
|
29
|
+
nodes.push({ name: entry.name, isDir: true, children });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
nodes.push({ name: entry.name, isDir: false });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
nodes.sort((a, b) => {
|
|
36
|
+
if (a.isDir !== b.isDir)
|
|
37
|
+
return a.isDir ? -1 : 1;
|
|
38
|
+
return a.name.localeCompare(b.name);
|
|
39
|
+
});
|
|
40
|
+
return nodes;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function FileNodeView({ node, indent = 0, }) {
|
|
47
|
+
const ext = node.name.split(".").pop() || "";
|
|
48
|
+
const fileColors = {
|
|
49
|
+
ts: "cyan",
|
|
50
|
+
tsx: "cyan",
|
|
51
|
+
js: "yellow",
|
|
52
|
+
jsx: "yellow",
|
|
53
|
+
py: "blue",
|
|
54
|
+
rs: "red",
|
|
55
|
+
go: "cyan",
|
|
56
|
+
md: "green",
|
|
57
|
+
json: "yellow",
|
|
58
|
+
toml: "yellow",
|
|
59
|
+
yaml: "yellow",
|
|
60
|
+
yml: "yellow",
|
|
61
|
+
css: "magenta",
|
|
62
|
+
html: "red",
|
|
63
|
+
sh: "green",
|
|
64
|
+
};
|
|
65
|
+
const color = node.isDir ? "blue" : fileColors[ext] || "white";
|
|
66
|
+
const icon = node.isDir ? "đ" : getFileIcon(ext);
|
|
67
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: " ".repeat(indent * 2) }), _jsx(Text, { children: icon }), _jsxs(Text, { color: color, children: [" ", node.name] })] }), node.isDir &&
|
|
68
|
+
node.children?.map((child) => (_jsx(FileNodeView, { node: child, indent: indent + 1 }, child.name)))] }));
|
|
69
|
+
}
|
|
70
|
+
function getFileIcon(ext) {
|
|
71
|
+
const icons = {
|
|
72
|
+
ts: "â¸",
|
|
73
|
+
tsx: "â¸",
|
|
74
|
+
js: "â¸",
|
|
75
|
+
jsx: "â¸",
|
|
76
|
+
py: "â¸",
|
|
77
|
+
rs: "â¸",
|
|
78
|
+
go: "â¸",
|
|
79
|
+
md: "âĄ",
|
|
80
|
+
json: "{}",
|
|
81
|
+
toml: "âĄ",
|
|
82
|
+
yaml: "âĄ",
|
|
83
|
+
yml: "âĄ",
|
|
84
|
+
css: "â",
|
|
85
|
+
html: "â",
|
|
86
|
+
sh: "$",
|
|
87
|
+
env: "đ",
|
|
88
|
+
};
|
|
89
|
+
return icons[ext] || "¡";
|
|
90
|
+
}
|
|
91
|
+
export function FileTree({ workingDir }) {
|
|
92
|
+
const [tree, setTree] = useState([]);
|
|
93
|
+
const [error, setError] = useState("");
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
buildTree(workingDir, 0, 3)
|
|
96
|
+
.then(setTree)
|
|
97
|
+
.catch((e) => setError(String(e)));
|
|
98
|
+
const interval = setInterval(() => {
|
|
99
|
+
buildTree(workingDir, 0, 3).then(setTree).catch(() => { });
|
|
100
|
+
}, 5000);
|
|
101
|
+
return () => clearInterval(interval);
|
|
102
|
+
}, [workingDir]);
|
|
103
|
+
const dirName = path.basename(workingDir);
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 1, marginBottom: 0, children: [_jsx(Text, { color: "blue", bold: true, children: "\uD83D\uDCC1" }), _jsx(Text, { color: "blue", bold: true, children: dirName })] }), error ? (_jsx(Text, { color: "red", children: error })) : tree.length === 0 ? (_jsx(Text, { color: "gray", dimColor: true, children: "(empty)" })) : (tree.slice(0, 30).map((node) => (_jsx(FileNodeView, { node: node, indent: 0 }, node.name))))] }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export function Header({ config, workingDir, mcpCount, screen, isStreaming, }) {
|
|
4
|
+
const dir = workingDir.split("/").slice(-2).join("/");
|
|
5
|
+
return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, marginBottom: 0, children: _jsxs(Box, { justifyContent: "space-between", alignItems: "center", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u25C6 MACHA" }), _jsx(Text, { color: "gray", children: "\u2502" }), _jsx(Text, { color: "cyan", children: config.provider }), _jsx(Text, { color: "gray", children: "\u203A" }), _jsx(Text, { color: "white", bold: true, children: config.model }), mcpCount > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "\u2502" }), _jsxs(Text, { color: "yellow", children: ["MCP: ", mcpCount] })] })), isStreaming && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: "\u2502" }), _jsx(Text, { color: "magenta", children: "\u25CF streaming" })] }))] }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", children: ["\uD83D\uDCC1 ", dir] }), _jsx(Text, { color: screen === "chat" ? "green" : "gray", bold: screen === "chat", children: screen === "chat" ? "[chat]" : "chat" }), _jsx(Text, { color: screen === "settings" ? "green" : "gray", bold: screen === "settings", children: screen === "settings" ? "[settings]" : "settings" })] })] }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
export function Input({ onSubmit, onTabPress, onClearChat, isStreaming, disabled, }) {
|
|
6
|
+
const [value, setValue] = useState("");
|
|
7
|
+
const [history, setHistory] = useState([]);
|
|
8
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (key.tab) {
|
|
11
|
+
onTabPress();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (key.ctrl && input === "l") {
|
|
15
|
+
onClearChat();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (key.upArrow && history.length > 0) {
|
|
19
|
+
const newIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
20
|
+
setHistoryIndex(newIndex);
|
|
21
|
+
setValue(history[newIndex] || "");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (key.downArrow) {
|
|
25
|
+
if (historyIndex > 0) {
|
|
26
|
+
const newIndex = historyIndex - 1;
|
|
27
|
+
setHistoryIndex(newIndex);
|
|
28
|
+
setValue(history[newIndex] || "");
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
setHistoryIndex(-1);
|
|
32
|
+
setValue("");
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
function handleSubmit(text) {
|
|
38
|
+
const trimmed = text.trim();
|
|
39
|
+
if (!trimmed || isStreaming)
|
|
40
|
+
return;
|
|
41
|
+
setHistory((prev) => [trimmed, ...prev.slice(0, 49)]);
|
|
42
|
+
setHistoryIndex(-1);
|
|
43
|
+
setValue("");
|
|
44
|
+
onSubmit(trimmed);
|
|
45
|
+
}
|
|
46
|
+
const placeholder = isStreaming
|
|
47
|
+
? "Macha is thinking..."
|
|
48
|
+
: "Message Macha... (Tab: settings, ââ: history, Ctrl+L: clear)";
|
|
49
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: isStreaming ? "yellow" : "green", paddingX: 1, flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { color: isStreaming ? "yellow" : "green", bold: true, children: isStreaming ? "â" : "âś" }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { value: value, onChange: setValue, onSubmit: handleSubmit, placeholder: placeholder, focus: !disabled }) })] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { ToolCallDisplay } from "./ToolCall.js";
|
|
4
|
+
function UserMessage({ message }) {
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u25B6 You" }), _jsx(Text, { color: "gray", dimColor: true, children: message.timestamp.toLocaleTimeString([], {
|
|
6
|
+
hour: "2-digit",
|
|
7
|
+
minute: "2-digit",
|
|
8
|
+
}) })] }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "white", children: message.content }) })] }));
|
|
9
|
+
}
|
|
10
|
+
function AssistantMessage({ message }) {
|
|
11
|
+
const hasTools = message.toolCalls && message.toolCalls.length > 0;
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u25C6 Macha" }), _jsx(Text, { color: "gray", dimColor: true, children: message.timestamp.toLocaleTimeString([], {
|
|
13
|
+
hour: "2-digit",
|
|
14
|
+
minute: "2-digit",
|
|
15
|
+
}) })] }), message.content && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: message.content.split("\n").map((line, i) => {
|
|
16
|
+
if (line.startsWith("```")) {
|
|
17
|
+
return (_jsx(Text, { color: "gray", dimColor: true, children: line }, i));
|
|
18
|
+
}
|
|
19
|
+
if (line.startsWith("# ")) {
|
|
20
|
+
return (_jsx(Text, { color: "cyan", bold: true, children: line.slice(2) }, i));
|
|
21
|
+
}
|
|
22
|
+
if (line.startsWith("## ")) {
|
|
23
|
+
return (_jsx(Text, { color: "green", bold: true, children: line.slice(3) }, i));
|
|
24
|
+
}
|
|
25
|
+
if (line.startsWith("- ") || line.startsWith("* ")) {
|
|
26
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", children: "\u2022" }), _jsx(Text, { color: "white", children: line.slice(2) })] }, i));
|
|
27
|
+
}
|
|
28
|
+
if (line.match(/^\d+\. /)) {
|
|
29
|
+
return (_jsx(Text, { color: "white", children: line }, i));
|
|
30
|
+
}
|
|
31
|
+
if (line.includes("`")) {
|
|
32
|
+
const parts = line.split("`");
|
|
33
|
+
return (_jsx(Box, { flexWrap: "wrap", children: parts.map((part, j) => j % 2 === 0 ? (_jsx(Text, { color: "white", children: part }, j)) : (_jsx(Text, { color: "yellow", backgroundColor: "black", children: part }, j))) }, i));
|
|
34
|
+
}
|
|
35
|
+
return (_jsx(Text, { color: "white", children: line }, i));
|
|
36
|
+
}) })), hasTools && (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0, gap: 0, children: message.toolCalls.map((tc) => (_jsx(ToolCallDisplay, { toolCall: tc, compact: false }, tc.id))) }))] }));
|
|
37
|
+
}
|
|
38
|
+
export function MessageList({ messages, streamingText, isStreaming, }) {
|
|
39
|
+
if (messages.length === 0 && !isStreaming) {
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, gap: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u25C6 MACHA" }), _jsx(Text, { color: "gray", children: "AI coding assistant for your terminal" }), _jsx(Text, { color: "gray", dimColor: true, children: "Start typing to begin \u2022 Tab for settings \u2022 Ctrl+C to quit" })] }));
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, flexGrow: 1, children: [messages.map((msg) => msg.role === "user" ? (_jsx(UserMessage, { message: msg }, msg.id)) : (_jsx(AssistantMessage, { message: msg }, msg.id))), isStreaming && streamingText && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u25C6 Macha" }), _jsx(Text, { color: "yellow", children: "\u25CF" })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: streamingText.split("\n").map((line, i) => (_jsx(Text, { color: "white", children: line }, i))) })] })), isStreaming && !streamingText && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "green", bold: true, children: "\u25C6 Macha" }), _jsx(Text, { color: "yellow", children: "thinking..." })] }))] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import SelectInput from "ink-select-input";
|
|
6
|
+
import { getConfig, setConfig, getAllProviders, BUILTIN_PROVIDERS, } from "../config/index.js";
|
|
7
|
+
export function Settings({ onClose, onConfigChange }) {
|
|
8
|
+
const [section, setSection] = useState("main");
|
|
9
|
+
const [config, setLocalConfig] = useState(getConfig());
|
|
10
|
+
const [inputValue, setInputValue] = useState("");
|
|
11
|
+
const [message, setMessage] = useState("");
|
|
12
|
+
useInput((input, key) => {
|
|
13
|
+
if (key.escape) {
|
|
14
|
+
if (section !== "main") {
|
|
15
|
+
setSection("main");
|
|
16
|
+
setInputValue("");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
onClose();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
function showMessage(msg) {
|
|
24
|
+
setMessage(msg);
|
|
25
|
+
setTimeout(() => setMessage(""), 2000);
|
|
26
|
+
}
|
|
27
|
+
function refreshConfig() {
|
|
28
|
+
setLocalConfig(getConfig());
|
|
29
|
+
onConfigChange();
|
|
30
|
+
}
|
|
31
|
+
const mainMenuItems = [
|
|
32
|
+
{ label: `đ API Key ${config.apiKey ? "â˘â˘â˘â˘" + config.apiKey.slice(-4) : "(not set)"}`, value: "apikey" },
|
|
33
|
+
{ label: `đ Base URL ${config.baseURL}`, value: "baseurl" },
|
|
34
|
+
{ label: `đ¤ Provider ${config.provider}`, value: "provider" },
|
|
35
|
+
{ label: `đ Model ${config.model}`, value: "model" },
|
|
36
|
+
{ label: `đ System Prompt (edit)`, value: "prompt" },
|
|
37
|
+
{ label: `đ MCP Servers ${config.mcpServers.length} configured`, value: "mcp" },
|
|
38
|
+
{ label: `â Close Settings`, value: "close" },
|
|
39
|
+
];
|
|
40
|
+
function handleMainSelect(item) {
|
|
41
|
+
if (item.value === "close") {
|
|
42
|
+
onClose();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setSection(item.value);
|
|
46
|
+
if (item.value === "apikey")
|
|
47
|
+
setInputValue(config.apiKey || "");
|
|
48
|
+
if (item.value === "baseurl")
|
|
49
|
+
setInputValue(config.baseURL || "");
|
|
50
|
+
if (item.value === "model")
|
|
51
|
+
setInputValue(config.model || "");
|
|
52
|
+
if (item.value === "prompt")
|
|
53
|
+
setInputValue(config.systemPrompt || "");
|
|
54
|
+
}
|
|
55
|
+
const providers = getAllProviders();
|
|
56
|
+
const providerItems = providers.map((p) => ({
|
|
57
|
+
label: `${p.name === config.provider ? "âś " : " "}${p.name} â ${p.baseURL || "(custom)"}`,
|
|
58
|
+
value: p.name,
|
|
59
|
+
}));
|
|
60
|
+
function handleProviderSelect(item) {
|
|
61
|
+
const provider = providers.find((p) => p.name === item.value);
|
|
62
|
+
if (provider) {
|
|
63
|
+
setConfig("provider", provider.name);
|
|
64
|
+
if (provider.baseURL)
|
|
65
|
+
setConfig("baseURL", provider.baseURL);
|
|
66
|
+
if (provider.defaultModel)
|
|
67
|
+
setConfig("model", provider.defaultModel);
|
|
68
|
+
refreshConfig();
|
|
69
|
+
showMessage(`Switched to ${provider.name}`);
|
|
70
|
+
setSection("main");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function handleApiKeySubmit(val) {
|
|
74
|
+
setConfig("apiKey", val.trim());
|
|
75
|
+
refreshConfig();
|
|
76
|
+
showMessage("API key saved!");
|
|
77
|
+
setSection("main");
|
|
78
|
+
setInputValue("");
|
|
79
|
+
}
|
|
80
|
+
function handleBaseUrlSubmit(val) {
|
|
81
|
+
setConfig("baseURL", val.trim());
|
|
82
|
+
refreshConfig();
|
|
83
|
+
showMessage("Base URL saved!");
|
|
84
|
+
setSection("main");
|
|
85
|
+
setInputValue("");
|
|
86
|
+
}
|
|
87
|
+
function handleModelSubmit(val) {
|
|
88
|
+
setConfig("model", val.trim());
|
|
89
|
+
refreshConfig();
|
|
90
|
+
showMessage("Model saved!");
|
|
91
|
+
setSection("main");
|
|
92
|
+
setInputValue("");
|
|
93
|
+
}
|
|
94
|
+
function handlePromptSubmit(val) {
|
|
95
|
+
setConfig("systemPrompt", val.trim());
|
|
96
|
+
refreshConfig();
|
|
97
|
+
showMessage("System prompt saved!");
|
|
98
|
+
setSection("main");
|
|
99
|
+
setInputValue("");
|
|
100
|
+
}
|
|
101
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "green", padding: 1, flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, alignItems: "center", children: [_jsx(Text, { color: "green", bold: true, children: "\u2699 SETTINGS" }), section !== "main" && (_jsxs(Text, { color: "gray", children: ["\u203A ", section.toUpperCase()] })), _jsx(Text, { color: "gray", dimColor: true, children: "(Esc to go back)" })] }), message && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] }) })), section === "main" && (_jsx(SelectInput, { items: mainMenuItems, onSelect: handleMainSelect })), section === "apikey" && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "cyan", children: "Enter your API key:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Stored locally in ~/.config/macha/config.json" }), _jsx(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, children: _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleApiKeySubmit, placeholder: "sk-...", mask: "*" }) }), _jsx(Text, { color: "gray", dimColor: true, children: "Press Enter to save" })] })), section === "baseurl" && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "cyan", children: "Enter Base URL:" }), _jsx(Text, { color: "gray", dimColor: true, children: "Any OpenAI-compatible endpoint (OpenAI, Groq, Ollama, etc.)" }), _jsx(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, children: _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleBaseUrlSubmit, placeholder: "https://api.openai.com/v1" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsx(Text, { color: "gray", children: "Quick presets:" }), BUILTIN_PROVIDERS.filter((p) => p.baseURL).map((p) => (_jsxs(Text, { color: "gray", dimColor: true, children: [p.name, ": ", p.baseURL] }, p.name)))] })] })), section === "model" && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "cyan", children: "Enter Model Name:" }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Current: ", config.model, " (provider: ", config.provider, ")"] }), _jsx(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, children: _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleModelSubmit, placeholder: config.model }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsx(Text, { color: "gray", children: "Common models:" }), _jsx(Text, { color: "gray", dimColor: true, children: "OpenAI: gpt-4o, gpt-4o-mini, o3-mini" }), _jsx(Text, { color: "gray", dimColor: true, children: "Anthropic: claude-opus-4-5, claude-sonnet-4-5" }), _jsx(Text, { color: "gray", dimColor: true, children: "Groq: llama-3.3-70b-versatile, mixtral-8x7b-32768" }), _jsx(Text, { color: "gray", dimColor: true, children: "Ollama: llama3.2, qwen2.5-coder, deepseek-r1" })] })] })), section === "provider" && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "cyan", children: "Select Provider:" }), _jsx(SelectInput, { items: providerItems, onSelect: handleProviderSelect })] })), section === "prompt" && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "cyan", children: "System Prompt (single line, Enter to save):" }), _jsx(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, children: _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handlePromptSubmit }) })] })), section === "mcp" && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: "cyan", children: "MCP Servers:" }), config.mcpServers.length === 0 ? (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsx(Text, { color: "gray", children: "No MCP servers configured." }), _jsx(Text, { color: "gray", dimColor: true, children: "Add servers by editing ~/.config/macha/config.json" }), _jsx(Text, { color: "gray", dimColor: true, children: "Example entry in mcpServers array:" }), _jsx(Text, { color: "gray", dimColor: true, children: `{ "id": "fs", "name": "Filesystem", "transport": "stdio",` }), _jsx(Text, { color: "gray", dimColor: true, children: ` "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],` }), _jsx(Text, { color: "gray", dimColor: true, children: ` "enabled": true }` })] })) : (config.mcpServers.map((srv) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: srv.enabled ? "green" : "gray", children: srv.enabled ? "â" : "â" }), _jsx(Text, { color: "white", children: srv.name }), _jsxs(Text, { color: "gray", children: ["(", srv.transport, ")"] })] }, srv.id))))] }))] }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
const TOOL_ICONS = {
|
|
4
|
+
read_file: "đ",
|
|
5
|
+
write_file: "âď¸",
|
|
6
|
+
list_files: "đ",
|
|
7
|
+
execute_command: "âĄ",
|
|
8
|
+
search_files: "đ",
|
|
9
|
+
delete_file: "đď¸",
|
|
10
|
+
move_file: "đŚ",
|
|
11
|
+
};
|
|
12
|
+
const TOOL_LABELS = {
|
|
13
|
+
read_file: "Read",
|
|
14
|
+
write_file: "Write",
|
|
15
|
+
list_files: "List",
|
|
16
|
+
execute_command: "Run",
|
|
17
|
+
search_files: "Search",
|
|
18
|
+
delete_file: "Delete",
|
|
19
|
+
move_file: "Move",
|
|
20
|
+
};
|
|
21
|
+
function getArgSummary(name, argsStr) {
|
|
22
|
+
try {
|
|
23
|
+
const args = JSON.parse(argsStr);
|
|
24
|
+
switch (name) {
|
|
25
|
+
case "read_file":
|
|
26
|
+
case "write_file":
|
|
27
|
+
case "delete_file":
|
|
28
|
+
return args.path || "";
|
|
29
|
+
case "list_files":
|
|
30
|
+
return args.path || ".";
|
|
31
|
+
case "execute_command":
|
|
32
|
+
return args.command?.slice(0, 60) || "";
|
|
33
|
+
case "search_files":
|
|
34
|
+
return `"${args.query}" ${args.glob ? `in ${args.glob}` : ""}`;
|
|
35
|
+
case "move_file":
|
|
36
|
+
return `${args.from} â ${args.to}`;
|
|
37
|
+
default:
|
|
38
|
+
return argsStr.slice(0, 50);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return argsStr.slice(0, 50);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function ToolCallDisplay({ toolCall, compact = false }) {
|
|
46
|
+
const icon = TOOL_ICONS[toolCall.name] || "đ§";
|
|
47
|
+
const label = TOOL_LABELS[toolCall.name] || toolCall.name;
|
|
48
|
+
const summary = getArgSummary(toolCall.name, toolCall.args);
|
|
49
|
+
const hasResult = toolCall.result !== undefined;
|
|
50
|
+
const isSuccess = toolCall.result?.success !== false;
|
|
51
|
+
if (compact) {
|
|
52
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: hasResult ? (isSuccess ? "green" : "red") : "yellow", children: hasResult ? (isSuccess ? "â" : "â") : "â" }), _jsx(Text, { color: "cyan", dimColor: true, children: label }), _jsx(Text, { color: "gray", children: summary })] }));
|
|
53
|
+
}
|
|
54
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: hasResult ? (isSuccess ? "green" : "red") : "yellow", paddingX: 1, marginLeft: 2, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { children: icon }), _jsx(Text, { color: "cyan", bold: true, children: label }), _jsx(Text, { color: "white", children: summary }), !hasResult && _jsx(Text, { color: "yellow", children: " (running...)" }), hasResult && (_jsx(Text, { color: isSuccess ? "green" : "red", children: isSuccess ? " â" : " â" }))] }), hasResult && toolCall.result?.output && !compact && (_jsx(Box, { marginTop: 0, flexDirection: "column", children: _jsxs(Text, { color: "gray", children: [toolCall.result.output
|
|
55
|
+
.split("\n")
|
|
56
|
+
.slice(0, 8)
|
|
57
|
+
.join("\n"), toolCall.result.output.split("\n").length > 8
|
|
58
|
+
? `\n... (${toolCall.result.output.split("\n").length - 8} more lines)`
|
|
59
|
+
: ""] }) })), hasResult && toolCall.result?.error && (_jsx(Text, { color: "red", children: toolCall.result.error }))] }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export const BUILTIN_PROVIDERS = [
|
|
4
|
+
{
|
|
5
|
+
name: "OpenAI",
|
|
6
|
+
baseURL: "https://api.openai.com/v1",
|
|
7
|
+
defaultModel: "gpt-4o",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: "Anthropic (OpenAI-compat)",
|
|
11
|
+
baseURL: "https://api.anthropic.com/v1",
|
|
12
|
+
defaultModel: "claude-opus-4-5",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "Groq",
|
|
16
|
+
baseURL: "https://api.groq.com/openai/v1",
|
|
17
|
+
defaultModel: "llama-3.3-70b-versatile",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "Ollama (local)",
|
|
21
|
+
baseURL: "http://localhost:11434/v1",
|
|
22
|
+
defaultModel: "llama3.2",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "OpenRouter",
|
|
26
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
27
|
+
defaultModel: "anthropic/claude-3.5-sonnet",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "Together AI",
|
|
31
|
+
baseURL: "https://api.together.xyz/v1",
|
|
32
|
+
defaultModel: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "Mistral",
|
|
36
|
+
baseURL: "https://api.mistral.ai/v1",
|
|
37
|
+
defaultModel: "mistral-large-latest",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "Custom",
|
|
41
|
+
baseURL: "",
|
|
42
|
+
defaultModel: "",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const DEFAULT_SYSTEM_PROMPT = `You are Macha, an expert AI coding assistant running in the terminal. You help users write, understand, debug, and improve code.
|
|
46
|
+
|
|
47
|
+
You have access to tools that let you read files, write files, execute shell commands, and search through code in the user's project directory. Always use these tools to help accomplish tasks concretely.
|
|
48
|
+
|
|
49
|
+
Guidelines:
|
|
50
|
+
- Be concise and direct. Users are developers who prefer clarity.
|
|
51
|
+
- When creating or editing files, always use the write_file tool.
|
|
52
|
+
- When running commands, use the execute_command tool and show output.
|
|
53
|
+
- Prefer making real changes over describing what to do.
|
|
54
|
+
- If a task requires multiple steps, work through them systematically.
|
|
55
|
+
- Always respect the working directory - never access files outside it.`;
|
|
56
|
+
export const conf = new Conf({
|
|
57
|
+
projectName: "macha",
|
|
58
|
+
defaults: {
|
|
59
|
+
apiKey: "",
|
|
60
|
+
baseURL: "https://api.openai.com/v1",
|
|
61
|
+
model: "gpt-4o",
|
|
62
|
+
provider: "OpenAI",
|
|
63
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
64
|
+
maxTokens: 8192,
|
|
65
|
+
temperature: 0.7,
|
|
66
|
+
mcpServers: [],
|
|
67
|
+
customProviders: [],
|
|
68
|
+
theme: "dark",
|
|
69
|
+
showFileTree: true,
|
|
70
|
+
workingDir: process.cwd(),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
export function getConfig() {
|
|
74
|
+
return {
|
|
75
|
+
apiKey: conf.get("apiKey"),
|
|
76
|
+
baseURL: conf.get("baseURL"),
|
|
77
|
+
model: conf.get("model"),
|
|
78
|
+
provider: conf.get("provider"),
|
|
79
|
+
systemPrompt: conf.get("systemPrompt"),
|
|
80
|
+
maxTokens: conf.get("maxTokens"),
|
|
81
|
+
temperature: conf.get("temperature"),
|
|
82
|
+
mcpServers: conf.get("mcpServers"),
|
|
83
|
+
customProviders: conf.get("customProviders"),
|
|
84
|
+
theme: conf.get("theme"),
|
|
85
|
+
showFileTree: conf.get("showFileTree"),
|
|
86
|
+
workingDir: conf.get("workingDir"),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function setConfig(key, value) {
|
|
90
|
+
conf.set(key, value);
|
|
91
|
+
}
|
|
92
|
+
export function getAllProviders() {
|
|
93
|
+
const custom = conf.get("customProviders") || [];
|
|
94
|
+
return [...BUILTIN_PROVIDERS, ...custom];
|
|
95
|
+
}
|
|
96
|
+
export function configPath() {
|
|
97
|
+
return path.dirname(conf.path);
|
|
98
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { App } from "./App.js";
|
|
8
|
+
import { getConfig, setConfig, getAllProviders, conf } from "./config/index.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("macha")
|
|
12
|
+
.description("â Macha â a beautiful terminal AI coding assistant\n" +
|
|
13
|
+
" Like Claude Code and OpenCode, but yours.")
|
|
14
|
+
.version("0.1.0")
|
|
15
|
+
.argument("[directory]", "Working directory to open (default: current dir)")
|
|
16
|
+
.option("-m, --model <model>", "AI model to use")
|
|
17
|
+
.option("-p, --provider <provider>", "Provider preset to use")
|
|
18
|
+
.option("-k, --api-key <key>", "API key (also: MACHA_API_KEY env var)")
|
|
19
|
+
.option("-u, --base-url <url>", "Custom base URL for API")
|
|
20
|
+
.option("--prompt <text>", "Send an initial prompt immediately")
|
|
21
|
+
.option("--config", "Show config file location and current settings")
|
|
22
|
+
.action((directory, opts) => {
|
|
23
|
+
if (opts.config) {
|
|
24
|
+
const cfg = getConfig();
|
|
25
|
+
console.log("\nâ Macha Configuration\n");
|
|
26
|
+
console.log(`Config file: ${conf.path}`);
|
|
27
|
+
console.log(`Provider: ${cfg.provider}`);
|
|
28
|
+
console.log(`Model: ${cfg.model}`);
|
|
29
|
+
console.log(`Base URL: ${cfg.baseURL}`);
|
|
30
|
+
console.log(`API Key: ${cfg.apiKey ? "â˘â˘â˘â˘" + cfg.apiKey.slice(-4) : "(not set)"}`);
|
|
31
|
+
console.log(`MCP Servers: ${cfg.mcpServers.length} configured\n`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
let workingDir = process.cwd();
|
|
35
|
+
if (directory) {
|
|
36
|
+
const resolved = path.resolve(process.cwd(), directory);
|
|
37
|
+
if (!fs.existsSync(resolved)) {
|
|
38
|
+
fs.mkdirSync(resolved, { recursive: true });
|
|
39
|
+
console.log(`Created directory: ${resolved}`);
|
|
40
|
+
}
|
|
41
|
+
workingDir = resolved;
|
|
42
|
+
}
|
|
43
|
+
if (opts.apiKey || process.env.MACHA_API_KEY) {
|
|
44
|
+
setConfig("apiKey", opts.apiKey || process.env.MACHA_API_KEY || "");
|
|
45
|
+
}
|
|
46
|
+
if (opts.baseUrl) {
|
|
47
|
+
setConfig("baseURL", opts.baseUrl);
|
|
48
|
+
}
|
|
49
|
+
if (opts.model) {
|
|
50
|
+
setConfig("model", opts.model);
|
|
51
|
+
}
|
|
52
|
+
if (opts.provider) {
|
|
53
|
+
const providers = getAllProviders();
|
|
54
|
+
const found = providers.find((p) => p.name.toLowerCase() === opts.provider.toLowerCase());
|
|
55
|
+
if (found) {
|
|
56
|
+
setConfig("provider", found.name);
|
|
57
|
+
if (found.baseURL)
|
|
58
|
+
setConfig("baseURL", found.baseURL);
|
|
59
|
+
if (found.defaultModel)
|
|
60
|
+
setConfig("model", found.defaultModel);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!process.stdin.isTTY) {
|
|
64
|
+
let input = "";
|
|
65
|
+
process.stdin.setEncoding("utf-8");
|
|
66
|
+
process.stdin.on("data", (chunk) => (input += chunk));
|
|
67
|
+
process.stdin.on("end", () => {
|
|
68
|
+
console.log("Non-interactive mode: use a real terminal for the full TUI experience.");
|
|
69
|
+
process.exit(0);
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const { waitUntilExit } = render(_jsx(App, { workingDir: workingDir, initialPrompt: opts.prompt }), {
|
|
74
|
+
exitOnCtrlC: false,
|
|
75
|
+
patchConsole: true,
|
|
76
|
+
});
|
|
77
|
+
waitUntilExit().then(() => {
|
|
78
|
+
console.log("\nâ Macha session ended. Goodbye!\n");
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
program
|
|
83
|
+
.command("config:set <key> <value>")
|
|
84
|
+
.description("Set a config value")
|
|
85
|
+
.action((key, value) => {
|
|
86
|
+
const validKeys = [
|
|
87
|
+
"apiKey",
|
|
88
|
+
"baseURL",
|
|
89
|
+
"model",
|
|
90
|
+
"provider",
|
|
91
|
+
"maxTokens",
|
|
92
|
+
"temperature",
|
|
93
|
+
];
|
|
94
|
+
if (!validKeys.includes(key)) {
|
|
95
|
+
console.error(`Unknown config key: ${key}`);
|
|
96
|
+
console.error(`Valid keys: ${validKeys.join(", ")}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
setConfig(key, value);
|
|
100
|
+
console.log(`â Set ${key} = ${key.includes("Key") ? "â˘â˘â˘â˘" : value}`);
|
|
101
|
+
});
|
|
102
|
+
program
|
|
103
|
+
.command("config:get <key>")
|
|
104
|
+
.description("Get a config value")
|
|
105
|
+
.action((key) => {
|
|
106
|
+
const cfg = getConfig();
|
|
107
|
+
const val = cfg[key];
|
|
108
|
+
if (val === undefined) {
|
|
109
|
+
console.error(`Unknown config key: ${key}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log(String(val));
|
|
113
|
+
});
|
|
114
|
+
program.parse();
|