@megacode/cli 0.2.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/adapters/accounts.js +34 -0
- package/dist/adapters/auth/anthropic.js +36 -0
- package/dist/adapters/auth/chatgpt.js +100 -0
- package/dist/adapters/auth/oauth.js +68 -0
- package/dist/adapters/auth/openrouter.js +27 -0
- package/dist/adapters/auth/store.js +25 -0
- package/dist/adapters/git/worktree.js +113 -0
- package/dist/adapters/mcp/config.js +59 -0
- package/dist/adapters/mcp/manager.js +165 -0
- package/dist/adapters/mcp/results.js +21 -0
- package/dist/adapters/mcp/transport.js +19 -0
- package/dist/adapters/project.js +15 -0
- package/dist/adapters/providers/anthropic.js +103 -0
- package/dist/adapters/providers/catalog.js +80 -0
- package/dist/adapters/providers/chatgpt.js +117 -0
- package/dist/adapters/providers/credentials.js +55 -0
- package/dist/adapters/providers/gemini.js +123 -0
- package/dist/adapters/providers/openai.js +128 -0
- package/dist/adapters/providers/registry.js +68 -0
- package/dist/adapters/providers/shared.js +12 -0
- package/dist/adapters/providers/usage.js +95 -0
- package/dist/adapters/settings.js +15 -0
- package/dist/adapters/skills.js +163 -0
- package/dist/adapters/storage.js +30 -0
- package/dist/adapters/tools/files.js +113 -0
- package/dist/adapters/tools/image.js +63 -0
- package/dist/adapters/tools/index.js +26 -0
- package/dist/adapters/tools/output.js +56 -0
- package/dist/adapters/tools/paths.js +5 -0
- package/dist/adapters/tools/questions.js +52 -0
- package/dist/adapters/tools/shell.js +73 -0
- package/dist/adapters/tools/validation.js +34 -0
- package/dist/args.js +30 -0
- package/dist/cli.js +111 -0
- package/dist/composition.js +18 -0
- package/dist/core/agent.js +128 -0
- package/dist/core/conversation.js +37 -0
- package/dist/core/prompts.js +12 -0
- package/dist/core/provider.js +8 -0
- package/dist/core/settings.js +15 -0
- package/dist/core/suggestion.js +29 -0
- package/dist/core/tools.js +10 -0
- package/dist/lib/async.js +6 -0
- package/dist/ui/App.js +218 -0
- package/dist/ui/ApprovalDialog.js +39 -0
- package/dist/ui/ConfigMenu.js +106 -0
- package/dist/ui/Dialog.js +10 -0
- package/dist/ui/DiffLines.js +12 -0
- package/dist/ui/EffortPicker.js +8 -0
- package/dist/ui/Help.js +21 -0
- package/dist/ui/KeyList.js +10 -0
- package/dist/ui/LoginDialog.js +154 -0
- package/dist/ui/McpMenu.js +138 -0
- package/dist/ui/ModelPicker.js +117 -0
- package/dist/ui/PromptInput.js +161 -0
- package/dist/ui/Questionnaire.js +35 -0
- package/dist/ui/Select.js +28 -0
- package/dist/ui/Spinner.js +18 -0
- package/dist/ui/StatusLine.js +19 -0
- package/dist/ui/TextField.js +22 -0
- package/dist/ui/Transcript.js +40 -0
- package/dist/ui/WorktreeMenu.js +83 -0
- package/dist/ui/autocomplete.js +6 -0
- package/dist/ui/commands.js +117 -0
- package/dist/ui/diff.js +130 -0
- package/dist/ui/format.js +138 -0
- package/dist/ui/history.js +9 -0
- package/dist/ui/hooks/useAgentSession.js +34 -0
- package/dist/ui/hooks/usePromptSuggestion.js +23 -0
- package/dist/ui/hooks/useStreamedText.js +38 -0
- package/dist/ui/hooks/useWorktree.js +30 -0
- package/dist/ui/loginFlow.js +37 -0
- package/dist/ui/mcpWizard.js +18 -0
- package/dist/ui/plain.js +60 -0
- package/dist/ui/session.js +168 -0
- package/package.json +57 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput, usePaste } from "ink";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { promptCompletion } from "./autocomplete.js";
|
|
5
|
+
import { KeyList } from "./KeyList.js";
|
|
6
|
+
/**
|
|
7
|
+
* Multi-line prompt editor with readline-style shortcuts, history and a slash-command menu.
|
|
8
|
+
* Newline: "\" + enter, or option/alt + enter.
|
|
9
|
+
*/
|
|
10
|
+
export function PromptInput({ value, onChange, onSubmit, onHelp, isActive, history, autocomplete, suggestion, commands, placeholder }) {
|
|
11
|
+
const [cursor, setCursor] = useState(value.length);
|
|
12
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
13
|
+
const historyPos = useRef(-1); // -1 = editing a fresh draft
|
|
14
|
+
const draft = useRef("");
|
|
15
|
+
// Keep the cursor valid when the value is changed from outside (clear, history, etc.).
|
|
16
|
+
useEffect(() => setCursor((c) => Math.min(c, value.length)), [value]);
|
|
17
|
+
const completion = promptCompletion(value, cursor, suggestion, autocomplete && isActive);
|
|
18
|
+
const menu = /^\/\S*$/.test(value) ? commands.filter((c) => c.name.startsWith(value)) : [];
|
|
19
|
+
useEffect(() => setMenuIndex(0), [value]);
|
|
20
|
+
const set = (v, c = v.length) => {
|
|
21
|
+
onChange(v);
|
|
22
|
+
setCursor(c);
|
|
23
|
+
};
|
|
24
|
+
const insert = (text) => set(value.slice(0, cursor) + text + value.slice(cursor), cursor + text.length);
|
|
25
|
+
// Line/column helpers for multi-line navigation.
|
|
26
|
+
const lineStart = value.lastIndexOf("\n", cursor - 1) + 1;
|
|
27
|
+
const lineEndIdx = value.indexOf("\n", cursor);
|
|
28
|
+
const lineEnd = lineEndIdx === -1 ? value.length : lineEndIdx;
|
|
29
|
+
const onFirstLine = lineStart === 0;
|
|
30
|
+
const onLastLine = lineEndIdx === -1;
|
|
31
|
+
const browseHistory = (dir) => {
|
|
32
|
+
if (!history.length)
|
|
33
|
+
return;
|
|
34
|
+
if (historyPos.current === -1) {
|
|
35
|
+
if (dir === 1)
|
|
36
|
+
return;
|
|
37
|
+
draft.current = value;
|
|
38
|
+
historyPos.current = history.length;
|
|
39
|
+
}
|
|
40
|
+
const next = historyPos.current + dir;
|
|
41
|
+
if (next < 0)
|
|
42
|
+
return;
|
|
43
|
+
if (next >= history.length) {
|
|
44
|
+
historyPos.current = -1;
|
|
45
|
+
return set(draft.current);
|
|
46
|
+
}
|
|
47
|
+
historyPos.current = next;
|
|
48
|
+
set(history[next]);
|
|
49
|
+
};
|
|
50
|
+
const moveVertical = (dir) => {
|
|
51
|
+
const col = cursor - lineStart;
|
|
52
|
+
if (dir === -1) {
|
|
53
|
+
const prevStart = value.lastIndexOf("\n", lineStart - 2) + 1;
|
|
54
|
+
setCursor(Math.min(prevStart + col, lineStart - 1));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const nextEndIdx = value.indexOf("\n", lineEnd + 1);
|
|
58
|
+
const nextEnd = nextEndIdx === -1 ? value.length : nextEndIdx;
|
|
59
|
+
setCursor(Math.min(lineEnd + 1 + col, nextEnd));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const wordStart = () => {
|
|
63
|
+
let i = cursor;
|
|
64
|
+
while (i > 0 && /\s/.test(value[i - 1]))
|
|
65
|
+
i--;
|
|
66
|
+
while (i > 0 && !/\s/.test(value[i - 1]))
|
|
67
|
+
i--;
|
|
68
|
+
return i;
|
|
69
|
+
};
|
|
70
|
+
const wordEnd = () => {
|
|
71
|
+
let i = cursor;
|
|
72
|
+
while (i < value.length && /\s/.test(value[i]))
|
|
73
|
+
i++;
|
|
74
|
+
while (i < value.length && !/\s/.test(value[i]))
|
|
75
|
+
i++;
|
|
76
|
+
return i;
|
|
77
|
+
};
|
|
78
|
+
const submit = (text) => {
|
|
79
|
+
historyPos.current = -1;
|
|
80
|
+
onSubmit(text);
|
|
81
|
+
};
|
|
82
|
+
usePaste((text) => insert(text.replace(/\r\n?/g, "\n")), { isActive });
|
|
83
|
+
useInput((input, key) => {
|
|
84
|
+
// Handled by the app: interrupt, exit, permission mode.
|
|
85
|
+
if ((key.ctrl && (input === "c" || input === "d")) || key.escape || (key.shift && key.tab))
|
|
86
|
+
return;
|
|
87
|
+
if (key.return) {
|
|
88
|
+
if (key.meta)
|
|
89
|
+
return insert("\n");
|
|
90
|
+
if (value[cursor - 1] === "\\")
|
|
91
|
+
return set(value.slice(0, cursor - 1) + "\n" + value.slice(cursor), cursor);
|
|
92
|
+
if (menu.length && value !== menu[menuIndex].name)
|
|
93
|
+
return submit(menu[menuIndex].name);
|
|
94
|
+
return submit(value);
|
|
95
|
+
}
|
|
96
|
+
if (key.tab) {
|
|
97
|
+
if (menu.length)
|
|
98
|
+
set(menu[menuIndex].name + " ");
|
|
99
|
+
else if (completion)
|
|
100
|
+
set(value + completion);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (key.upArrow) {
|
|
104
|
+
if (menu.length)
|
|
105
|
+
return setMenuIndex((i) => (i - 1 + menu.length) % menu.length);
|
|
106
|
+
return onFirstLine ? browseHistory(-1) : moveVertical(-1);
|
|
107
|
+
}
|
|
108
|
+
if (key.downArrow) {
|
|
109
|
+
if (menu.length)
|
|
110
|
+
return setMenuIndex((i) => (i + 1) % menu.length);
|
|
111
|
+
return onLastLine ? browseHistory(1) : moveVertical(1);
|
|
112
|
+
}
|
|
113
|
+
// Some terminals send Option+arrows as readline's Escape+b / Escape+f.
|
|
114
|
+
if (key.meta && (key.leftArrow || input === "b"))
|
|
115
|
+
return setCursor(wordStart());
|
|
116
|
+
if (key.meta && (key.rightArrow || input === "f"))
|
|
117
|
+
return setCursor(wordEnd());
|
|
118
|
+
if (key.leftArrow || (key.ctrl && input === "b"))
|
|
119
|
+
return setCursor(Math.max(0, cursor - 1));
|
|
120
|
+
if (key.rightArrow || (key.ctrl && input === "f"))
|
|
121
|
+
return setCursor(Math.min(value.length, cursor + 1));
|
|
122
|
+
if (key.home || (key.ctrl && input === "a"))
|
|
123
|
+
return setCursor(lineStart);
|
|
124
|
+
if (key.end || (key.ctrl && input === "e"))
|
|
125
|
+
return setCursor(lineEnd);
|
|
126
|
+
if (key.ctrl && input === "u")
|
|
127
|
+
return set(value.slice(0, lineStart) + value.slice(cursor), lineStart);
|
|
128
|
+
if (key.ctrl && input === "k")
|
|
129
|
+
return set(value.slice(0, cursor) + value.slice(lineEnd), cursor);
|
|
130
|
+
if ((key.ctrl && input === "w") || (key.meta && key.backspace)) {
|
|
131
|
+
const start = wordStart();
|
|
132
|
+
return set(value.slice(0, start) + value.slice(cursor), start);
|
|
133
|
+
}
|
|
134
|
+
if (key.backspace) {
|
|
135
|
+
if (cursor > 0)
|
|
136
|
+
set(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (key.delete)
|
|
140
|
+
return set(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
|
|
141
|
+
if (key.ctrl || key.meta || !input)
|
|
142
|
+
return;
|
|
143
|
+
if (input === "?" && value === "")
|
|
144
|
+
return onHelp();
|
|
145
|
+
// Typed-ahead text can arrive in one chunk together with the enter key.
|
|
146
|
+
if (input.length > 1 && input.endsWith("\r") && !input.slice(0, -1).includes("\r"))
|
|
147
|
+
return submit(value.slice(0, cursor) + input.slice(0, -1) + value.slice(cursor));
|
|
148
|
+
insert(input.replace(/\r\n?/g, "\n"));
|
|
149
|
+
}, { isActive });
|
|
150
|
+
// Render with a block cursor.
|
|
151
|
+
const lines = (value + completion).split("\n");
|
|
152
|
+
let offset = 0;
|
|
153
|
+
const rendered = lines.map((line, i) => {
|
|
154
|
+
const start = offset;
|
|
155
|
+
offset += line.length + 1;
|
|
156
|
+
const col = cursor - start;
|
|
157
|
+
const showCursor = isActive && col >= 0 && col <= line.length;
|
|
158
|
+
return (_jsxs(Box, { children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: i === 0 ? "> " : " " }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsx(Text, { wrap: "wrap", children: showCursor ? (_jsxs(_Fragment, { children: [line.slice(0, col), _jsx(Text, { inverse: true, dimColor: Boolean(completion), children: line[col] ?? " " }), _jsx(Text, { dimColor: Boolean(completion), children: line.slice(col + 1) })] })) : _jsx(Text, { dimColor: start > value.length, children: line || " " }) }) })] }, i));
|
|
159
|
+
});
|
|
160
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: value === "" && !completion ? (_jsxs(Box, { children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { children: "> " }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsxs(Text, { wrap: "wrap", children: [isActive ? _jsx(Text, { inverse: true, children: placeholder[0] }) : null, _jsx(Text, { dimColor: true, children: isActive ? placeholder.slice(1) : placeholder })] }) })] })) : (rendered) }), completion && _jsx(Text, { dimColor: true, children: " tab to accept suggestion" }), menu.length > 0 && (_jsx(Box, { paddingX: 2, children: _jsx(KeyList, { rows: menu.map((c) => [c.name, c.description]), selected: menuIndex }) }))] }));
|
|
161
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Dialog } from "./Dialog.js";
|
|
5
|
+
import { Select } from "./Select.js";
|
|
6
|
+
import { TextField } from "./TextField.js";
|
|
7
|
+
/** Collect all answers before submitting anything to the model. */
|
|
8
|
+
export function Questionnaire({ questions, onSubmit, onCancel }) {
|
|
9
|
+
const [answers, setAnswers] = useState([]);
|
|
10
|
+
const [value, setValue] = useState("");
|
|
11
|
+
const [custom, setCustom] = useState(false);
|
|
12
|
+
const question = questions[answers.length];
|
|
13
|
+
function answer(text) {
|
|
14
|
+
if (!text.trim())
|
|
15
|
+
return;
|
|
16
|
+
setAnswers([...answers, text.trim()]);
|
|
17
|
+
setValue("");
|
|
18
|
+
setCustom(false);
|
|
19
|
+
}
|
|
20
|
+
const footer = "enter continue · esc cancel and interrupt";
|
|
21
|
+
if (!question)
|
|
22
|
+
return (_jsxs(Dialog, { title: "Review answers", footer: footer, children: [questions.map((q, i) => (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [q.question, " \u2192"] }), " ", _jsx(Text, { color: "cyan", children: answers[i] })] }, i))), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: [
|
|
23
|
+
{ label: "Submit answers", value: "submit" },
|
|
24
|
+
{ label: "Start over", value: "restart" },
|
|
25
|
+
{ label: "Cancel", value: "cancel" },
|
|
26
|
+
], onSelect: (choice) => {
|
|
27
|
+
if (choice === "submit")
|
|
28
|
+
onSubmit(questions.map((q, i) => ({ question: q.question, answer: answers[i] })));
|
|
29
|
+
else if (choice === "restart")
|
|
30
|
+
setAnswers([]);
|
|
31
|
+
else
|
|
32
|
+
onCancel();
|
|
33
|
+
}, onCancel: onCancel }, "review") })] }));
|
|
34
|
+
return (_jsxs(Dialog, { title: `Question ${answers.length + 1} of ${questions.length}`, footer: footer, children: [_jsx(Text, { children: question.question }), _jsx(Box, { marginTop: 1, children: question.options && !custom ? (_jsx(Select, { options: [...question.options.map((label) => ({ label, value: label })), { label: "Other — type an answer", value: null }], onSelect: (choice) => (choice === null ? setCustom(true) : answer(choice)), onCancel: onCancel }, answers.length)) : (_jsx(TextField, { value: value, onChange: setValue, onSubmit: answer, onCancel: onCancel, placeholder: "Type your answer\u2026" })) })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* Arrow-key list: ↑/↓ (or ctrl+p/n) to move, enter to pick, 1-9 to pick directly, esc to cancel.
|
|
6
|
+
* `accent` colors the cursor and selected label; `numberColor` the "1." column.
|
|
7
|
+
*/
|
|
8
|
+
export function Select({ options, onSelect, onCancel, initialIndex = 0, accent = "cyan", numberColor, }) {
|
|
9
|
+
const [index, setIndex] = useState(initialIndex);
|
|
10
|
+
useInput((input, key) => {
|
|
11
|
+
if (key.upArrow || (key.ctrl && input === "p"))
|
|
12
|
+
setIndex((i) => (i - 1 + options.length) % options.length);
|
|
13
|
+
else if (key.downArrow || (key.ctrl && input === "n"))
|
|
14
|
+
setIndex((i) => (i + 1) % options.length);
|
|
15
|
+
else if (key.return)
|
|
16
|
+
onSelect(options[index].value);
|
|
17
|
+
else if (key.escape)
|
|
18
|
+
onCancel?.();
|
|
19
|
+
else if (/^[1-9]$/.test(input) && Number(input) <= options.length)
|
|
20
|
+
onSelect(options[Number(input) - 1].value);
|
|
21
|
+
});
|
|
22
|
+
// "❯ 1. " stays in its own column so long labels wrap under the label, not under the number.
|
|
23
|
+
const marker = `❯ ${options.length}. `.length;
|
|
24
|
+
return (_jsx(Box, { flexDirection: "column", children: options.map((o, i) => {
|
|
25
|
+
const selected = i === index;
|
|
26
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: marker, flexShrink: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: accent, children: selected ? "❯ " : " " }), _jsxs(Text, { color: numberColor ?? (selected ? accent : undefined), children: [i + 1, "."] })] }) }), _jsx(Box, { flexShrink: 1, minWidth: 0, children: _jsxs(Text, { color: selected ? accent : undefined, children: [o.label, o.hint ? _jsxs(Text, { dimColor: true, children: [" ", o.hint] }) : null] }) })] }, i));
|
|
27
|
+
}) }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useAnimation } from "ink";
|
|
3
|
+
const FRAMES = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"];
|
|
4
|
+
/** "✻ Thinking… (3s · esc to interrupt)" while a turn runs. */
|
|
5
|
+
export function Spinner({ verb }) {
|
|
6
|
+
const { frame, time } = useAnimation({ interval: 120 });
|
|
7
|
+
return (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "cyan", children: [FRAMES[frame % FRAMES.length], " ", verb, "\u2026", " "] }), _jsxs(Text, { dimColor: true, children: ["(", Math.floor(time / 1000), "s \u00B7 esc to interrupt)"] })] }));
|
|
8
|
+
}
|
|
9
|
+
/** Blinking dot in front of a running tool call. */
|
|
10
|
+
export function Blink() {
|
|
11
|
+
const { frame } = useAnimation({ interval: 500 });
|
|
12
|
+
return _jsx(Text, { color: "cyan", children: frame % 2 ? " " : "⏺" });
|
|
13
|
+
}
|
|
14
|
+
/** Braille spinner with a message, for dialogs waiting on the network or a browser. */
|
|
15
|
+
export function Waiting({ text }) {
|
|
16
|
+
const { frame } = useAnimation({ interval: 100 });
|
|
17
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"[frame % 10] }), " ", text] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { tildify } from "./format.js";
|
|
4
|
+
const MODE_LABELS = {
|
|
5
|
+
"accept-edits": { text: "⏵⏵ accept edits", color: "magenta" },
|
|
6
|
+
ask: { text: "ask mode", color: "cyan" },
|
|
7
|
+
};
|
|
8
|
+
/** Row under the prompt: permission mode and cwd on the left; model, worktree and tokens on the right. */
|
|
9
|
+
export function StatusLine({ mode, model, loggedIn, exitArmed, usage, worktree, }) {
|
|
10
|
+
// Give exit confirmation the whole row instead of competing with model/worktree metadata.
|
|
11
|
+
if (exitArmed) {
|
|
12
|
+
return (_jsx(Box, { paddingX: 2, children: _jsx(Text, { color: "yellow", children: "Press Ctrl-C again to exit" }) }));
|
|
13
|
+
}
|
|
14
|
+
// Bypass is the default, so only the other modes get a label.
|
|
15
|
+
const modeLabel = MODE_LABELS[mode];
|
|
16
|
+
const tokens = usage.input + usage.output;
|
|
17
|
+
// When space runs out the path gives way first, then the details on the right; the mode never does.
|
|
18
|
+
return (_jsxs(Box, { paddingX: 2, gap: 2, children: [_jsxs(Box, { flexGrow: 1, flexShrink: 100, minWidth: modeLabel ? modeLabel.text.length + 1 : 0, children: [modeLabel && (_jsx(Box, { flexShrink: 0, marginRight: 1, children: _jsx(Text, { color: modeLabel.color, children: modeLabel.text }) })), _jsx(Box, { flexShrink: 1, minWidth: 0, children: _jsx(Text, { dimColor: true, wrap: "truncate-start", children: tildify(process.cwd()) }) })] }), _jsx(Box, { flexShrink: 1, minWidth: 0, children: _jsxs(Text, { dimColor: loggedIn, color: loggedIn ? undefined : "yellow", wrap: "truncate-end", children: [model, loggedIn ? "" : " · not logged in (/login)", worktree ? ` · ⎇ ${worktree}` : "", tokens ? ` · ${tokens < 1000 ? tokens : `${(tokens / 1000).toFixed(1)}k`} tokens` : ""] }) })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text, useInput, usePaste } from "ink";
|
|
3
|
+
/** Single-line input for dialogs. `mask` hides the value (API keys), showing only the last 4 chars. */
|
|
4
|
+
export function TextField({ value, onChange, onSubmit, onCancel, mask, placeholder = "", }) {
|
|
5
|
+
usePaste((text) => onChange(value + text.replace(/[\r\n]/g, "").trim()));
|
|
6
|
+
useInput((input, key) => {
|
|
7
|
+
if (key.return)
|
|
8
|
+
return onSubmit(value.trim());
|
|
9
|
+
if (key.escape)
|
|
10
|
+
return onCancel?.();
|
|
11
|
+
if (key.backspace || key.delete)
|
|
12
|
+
return onChange(value.slice(0, -1));
|
|
13
|
+
if (key.ctrl && input === "u")
|
|
14
|
+
return onChange("");
|
|
15
|
+
if (key.ctrl || key.meta || key.upArrow || key.downArrow || key.tab)
|
|
16
|
+
return;
|
|
17
|
+
if (input)
|
|
18
|
+
onChange(value + input.replace(/[\r\n]/g, ""));
|
|
19
|
+
});
|
|
20
|
+
const shown = mask && value.length > 4 ? "•".repeat(Math.min(value.length - 4, 40)) + value.slice(-4) : value;
|
|
21
|
+
return (_jsxs(Text, { children: ["> ", shown || _jsx(Text, { dimColor: true, children: placeholder }), _jsx(Text, { inverse: true, children: " " })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { DIFF_COLORS } from "./diff.js";
|
|
4
|
+
import { DiffLines } from "./DiffLines.js";
|
|
5
|
+
import { callParts, previewOutput, previewPrompt, renderMarkdown, tildify } from "./format.js";
|
|
6
|
+
/** Claude Code's colors for a finished call and for secondary text. */
|
|
7
|
+
const DONE = "#4eba65";
|
|
8
|
+
const MUTED = DIFF_COLORS.muted;
|
|
9
|
+
import { Blink } from "./Spinner.js";
|
|
10
|
+
export function ItemView({ item, model }) {
|
|
11
|
+
switch (item.kind) {
|
|
12
|
+
case "banner":
|
|
13
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", alignSelf: "flex-start", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u273B" }), " Welcome to ", _jsx(Text, { bold: true, children: "megacode" })] }), _jsx(Text, { dimColor: true, children: "/help for commands \u00B7 ? for shortcuts" }), _jsxs(Text, { dimColor: true, children: ["model: ", model, "\n", "cwd: ", tildify(process.cwd())] })] }));
|
|
14
|
+
case "user":
|
|
15
|
+
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: "> " }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0, children: _jsx(Text, { wrap: "wrap", children: previewPrompt(item.text) }) })] }));
|
|
16
|
+
case "assistant":
|
|
17
|
+
return _jsx(AssistantText, { text: item.text, first: item.first });
|
|
18
|
+
case "tool":
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TranscriptRow, { prefix: _jsx(Text, { color: item.isError ? "red" : DONE, children: "\u23FA " }), width: 2, children: _jsx(CallHeader, { call: item.call }) }), _jsx(TranscriptRow, { prefix: _jsx(Text, { color: MUTED, children: " ⎿ " }), width: 5, children: item.diff ? (_jsx(Text, { children: item.output })) : (_jsx(Text, { dimColor: !item.isError, color: item.isError ? "red" : undefined, children: previewOutput(item.output) || "(no output)" })) }), item.diff && (_jsx(Box, { marginLeft: 5, marginRight: 7, flexDirection: "column", children: _jsx(DiffLines, { model: item.diff }) }))] }));
|
|
20
|
+
case "notice":
|
|
21
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(TranscriptRow, { prefix: _jsx(Text, { dimColor: true, children: " ⎿ " }), width: 5, children: _jsx(Text, { color: item.level === "error" ? "red" : item.level === "warn" ? "yellow" : undefined, dimColor: item.level === "info" && !item.bright, children: item.text }) }) }));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Assistant Markdown; `first` marks the start of a reply (also used for the live, still-streaming text). */
|
|
25
|
+
export function AssistantText({ text, first }) {
|
|
26
|
+
return (_jsx(Box, { marginTop: first ? 1 : 0, children: _jsx(TranscriptRow, { prefix: _jsx(Text, { children: first ? "⏺ " : " " }), width: 2, children: _jsx(Text, { children: renderMarkdown(text) }) }) }));
|
|
27
|
+
}
|
|
28
|
+
/** "Update(src/a.ts)" with only the tool name in bold. */
|
|
29
|
+
function CallHeader({ call }) {
|
|
30
|
+
const { name, arg } = callParts(call);
|
|
31
|
+
return (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: name }), "(", arg, ")"] }));
|
|
32
|
+
}
|
|
33
|
+
/** The tool call in progress. While it waits for approval, just its header with a gray dot. */
|
|
34
|
+
export function RunningTool({ call, waiting = false }) {
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [waiting ? _jsx(Text, { color: MUTED, children: "\u23FA" }) : _jsx(Blink, {}), " ", _jsx(CallHeader, { call: call })] }), !waiting && _jsx(Text, { color: MUTED, children: " ⎿ Running…" })] }));
|
|
36
|
+
}
|
|
37
|
+
/** Keep prefixes out of the text's wrapping width, including on continuation lines. */
|
|
38
|
+
function TranscriptRow({ prefix, width, children }) {
|
|
39
|
+
return (_jsxs(Box, { width: "100%", children: [_jsx(Box, { width: width, flexShrink: 0, children: prefix }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0, children: children })] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { baseRef, describeChanges, listWorktrees, WORKTREE_NAME, worktreeChanges } from "../adapters/git/worktree.js";
|
|
5
|
+
import { Dialog } from "./Dialog.js";
|
|
6
|
+
import { tildify } from "./format.js";
|
|
7
|
+
import { Select } from "./Select.js";
|
|
8
|
+
import { Waiting } from "./Spinner.js";
|
|
9
|
+
import { TextField } from "./TextField.js";
|
|
10
|
+
/** Runs `load` once on mount; undefined until it resolves. */
|
|
11
|
+
function useLoaded(load) {
|
|
12
|
+
const [value, setValue] = useState();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let live = true;
|
|
15
|
+
load().then((v) => live && setValue(v));
|
|
16
|
+
return () => void (live = false);
|
|
17
|
+
}, []);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
async function loadMenu(current) {
|
|
21
|
+
const [worktrees, base, currentChanges] = await Promise.all([
|
|
22
|
+
listWorktrees().then((list) => Promise.all(list.map(async (wt) => ({ wt, changes: describeChanges(await worktreeChanges(wt)) })))),
|
|
23
|
+
baseRef(),
|
|
24
|
+
current ? worktreeChanges(current).then(describeChanges) : "",
|
|
25
|
+
]);
|
|
26
|
+
return { worktrees, base, currentChanges };
|
|
27
|
+
}
|
|
28
|
+
/** Create a worktree, switch to an existing one, or go back to the main checkout. */
|
|
29
|
+
export function WorktreeMenu({ current, onCreate, onOpen, onLeave, onCancel, }) {
|
|
30
|
+
const [naming, setNaming] = useState(false);
|
|
31
|
+
const [name, setName] = useState("");
|
|
32
|
+
const [error, setError] = useState("");
|
|
33
|
+
const data = useLoaded(() => loadMenu(current));
|
|
34
|
+
if (!data)
|
|
35
|
+
return _jsx(Frame, { current: current, children: _jsx(Waiting, { text: "Reading worktrees\u2026" }) });
|
|
36
|
+
const { worktrees, base, currentChanges } = data;
|
|
37
|
+
const options = [
|
|
38
|
+
{ label: "New worktree", value: { type: "new" }, hint: `(branches from ${base})` },
|
|
39
|
+
...worktrees.map(({ wt, changes }) => ({
|
|
40
|
+
label: wt.path === current?.path ? `${wt.name} (current)` : `Switch to ${wt.name}`,
|
|
41
|
+
value: { type: "open", wt },
|
|
42
|
+
hint: `${wt.branch} · ${changes}`,
|
|
43
|
+
})),
|
|
44
|
+
];
|
|
45
|
+
if (current)
|
|
46
|
+
options.push({ label: "Return to main checkout, keep worktree", value: { type: "leave", remove: false } }, {
|
|
47
|
+
label: "Return to main checkout, remove worktree",
|
|
48
|
+
value: { type: "leave", remove: true },
|
|
49
|
+
hint: currentChanges === "no changes" ? "" : `(discards ${currentChanges})`,
|
|
50
|
+
});
|
|
51
|
+
function select(a) {
|
|
52
|
+
if (a.type === "new")
|
|
53
|
+
return setNaming(true);
|
|
54
|
+
if (a.type === "leave")
|
|
55
|
+
return onLeave(a.remove);
|
|
56
|
+
if (a.wt.path === current?.path)
|
|
57
|
+
return onCancel();
|
|
58
|
+
onOpen(a.wt);
|
|
59
|
+
}
|
|
60
|
+
function submitName(v) {
|
|
61
|
+
if (v && !WORKTREE_NAME.test(v))
|
|
62
|
+
return setError("Use letters, digits, - and _ (max 64), starting with a letter or digit.");
|
|
63
|
+
onCreate(v || undefined);
|
|
64
|
+
}
|
|
65
|
+
return (_jsx(Frame, { current: current, help: naming ? "enter create · esc back" : "↑↓ navigate · enter select · esc cancel", children: naming ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["Name for the new worktree (branch worktree-", name || "<name>", "):"] }), _jsx(TextField, { value: name, onChange: (v) => {
|
|
66
|
+
setName(v);
|
|
67
|
+
setError("");
|
|
68
|
+
}, onSubmit: submitName, onCancel: () => setNaming(false), placeholder: "leave empty for a random name" }), error && _jsx(Text, { color: "red", children: error })] })) : (_jsx(Select, { options: options, onSelect: select, onCancel: onCancel })) }));
|
|
69
|
+
}
|
|
70
|
+
function Frame({ current, help, children }) {
|
|
71
|
+
return (_jsx(Dialog, { title: "Worktrees", subtitle: current ? `in ${current.name} (${current.branch})` : "in the main checkout", footer: help, children: children }));
|
|
72
|
+
}
|
|
73
|
+
/** Shown when exiting while in a worktree: keep it for later, or remove it and its branch. */
|
|
74
|
+
export function ExitWorktreeDialog({ worktree, onSelect, onCancel, }) {
|
|
75
|
+
// undefined while git runs; null if git couldn't tell.
|
|
76
|
+
const changes = useLoaded(() => worktreeChanges(worktree));
|
|
77
|
+
const summary = changes === undefined ? "" : describeChanges(changes);
|
|
78
|
+
const clean = changes?.files === 0 && changes.commits === 0;
|
|
79
|
+
return (_jsxs(Dialog, { title: `Exiting worktree ${worktree.name}`, tone: "warn", footer: "esc to stay", children: [_jsxs(Text, { dimColor: true, children: [tildify(worktree.path), " \u00B7 branch ", worktree.branch, summary && ` · ${summary}`] }), _jsx(Box, { marginTop: 1, children: changes === undefined ? (_jsx(Waiting, { text: "Checking for changes\u2026" })) : (_jsx(Select, { initialIndex: clean ? 1 : 0, options: [
|
|
80
|
+
{ label: "Keep worktree", value: false, hint: `(return with megacode -w ${worktree.name})` },
|
|
81
|
+
{ label: "Remove worktree and branch", value: true, hint: clean ? "" : `(discards ${summary})` },
|
|
82
|
+
], onSelect: onSelect, onCancel: onCancel })) })] }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Suggestions are display-only until explicitly accepted. */
|
|
2
|
+
export function promptCompletion(value, cursor, suggestion, enabled) {
|
|
3
|
+
if (!enabled || cursor !== value.length || value.trimStart().startsWith("/"))
|
|
4
|
+
return "";
|
|
5
|
+
return suggestion.startsWith(value) ? suggestion.slice(value.length) : "";
|
|
6
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { savedProviders } from "../adapters/auth/store.js";
|
|
2
|
+
import { runSkillsCommand } from "../adapters/skills.js";
|
|
3
|
+
import { mainRoot } from "../adapters/git/worktree.js";
|
|
4
|
+
import { PROVIDERS, providerInfo } from "../adapters/providers/catalog.js";
|
|
5
|
+
import { providerOf } from "../adapters/providers/credentials.js";
|
|
6
|
+
import { providerUsage } from "../adapters/providers/usage.js";
|
|
7
|
+
import { EFFORTS } from "../core/provider.js";
|
|
8
|
+
export const COMMANDS = [
|
|
9
|
+
{
|
|
10
|
+
name: "/skills",
|
|
11
|
+
description: "List skills or install <local-directory|owner/repo|GitHub-URL> [--global]",
|
|
12
|
+
run(ctx, arg) {
|
|
13
|
+
if (ctx.running)
|
|
14
|
+
return ctx.notice("Can't manage skills while a turn is running (esc to interrupt).", "warn");
|
|
15
|
+
// Preserve spaces in a source path; the only supported option is a trailing --global.
|
|
16
|
+
const match = /^install\s+(.+?)(\s+--global)?$/.exec(arg);
|
|
17
|
+
const source = match?.[1].replace(/^([\"'])(.*)\1$/, "$2");
|
|
18
|
+
const args = match ? ["install", source, ...(match[2] ? ["--global"] : [])] : arg ? [arg] : [];
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
if (match)
|
|
21
|
+
ctx.notice("Installing skills…");
|
|
22
|
+
runSkillsCommand(args, { cwd }).then(ctx.print, (error) => ctx.notice(error instanceof Error ? error.message : String(error), "error"));
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "/model",
|
|
27
|
+
description: "Switch model (or /model provider:model)",
|
|
28
|
+
run: (ctx, arg) => (arg ? ctx.selectModel(arg) : ctx.open({ type: "model" })),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "/effort",
|
|
32
|
+
description: "Change model effort (default, low, medium, high)",
|
|
33
|
+
run(ctx, arg) {
|
|
34
|
+
if (!arg)
|
|
35
|
+
return ctx.open({ type: "effort" });
|
|
36
|
+
if (!EFFORTS.includes(arg))
|
|
37
|
+
return ctx.notice(`Unknown effort "${arg}". Available: ${EFFORTS.join(", ")}`, "warn");
|
|
38
|
+
ctx.selectEffort(arg);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "/login",
|
|
43
|
+
description: "Connect a provider (API key or local server)",
|
|
44
|
+
run(ctx, arg) {
|
|
45
|
+
if (arg && !PROVIDERS.includes(arg))
|
|
46
|
+
return ctx.notice(`Unknown provider "${arg}". Available: ${PROVIDERS.join(", ")}`, "warn");
|
|
47
|
+
ctx.open({ type: "login", provider: arg || undefined });
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "/logout",
|
|
52
|
+
description: "Remove saved credentials",
|
|
53
|
+
run(ctx, arg) {
|
|
54
|
+
if (arg)
|
|
55
|
+
return ctx.logout(arg);
|
|
56
|
+
if (!savedProviders().length)
|
|
57
|
+
return ctx.notice("No saved credentials. (Keys from environment variables aren't stored by megacode.)");
|
|
58
|
+
ctx.open({ type: "logout" });
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "/config",
|
|
63
|
+
description: "View and change settings",
|
|
64
|
+
aliases: ["/settings"],
|
|
65
|
+
run: (ctx) => ctx.open({ type: "config" }),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "/mcp",
|
|
69
|
+
description: "Manage MCP servers (add, remove, reconnect, see tools)",
|
|
70
|
+
run: (ctx) => ctx.open({ type: "mcp" }),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "/worktree",
|
|
74
|
+
description: "Create or switch git worktrees (or /worktree name)",
|
|
75
|
+
run(ctx, arg) {
|
|
76
|
+
if (arg)
|
|
77
|
+
return ctx.enterWorktree(arg);
|
|
78
|
+
mainRoot().then(() => ctx.open({ type: "worktree" }), (e) => ctx.notice(e.message, "warn"));
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "/clear",
|
|
83
|
+
description: "Clear conversation history and screen",
|
|
84
|
+
run(ctx) {
|
|
85
|
+
if (ctx.running)
|
|
86
|
+
return ctx.notice("Can't clear while a turn is running (esc to interrupt).", "warn");
|
|
87
|
+
ctx.clear();
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "/usage",
|
|
92
|
+
description: "Fetch current provider's account usage and limits",
|
|
93
|
+
run(ctx) {
|
|
94
|
+
const provider = providerOf(ctx.model);
|
|
95
|
+
const { label } = providerInfo(provider);
|
|
96
|
+
ctx.notice(`Fetching ${label} usage…`);
|
|
97
|
+
providerUsage(provider).then(ctx.print, (error) => ctx.notice(`${label}: ${error instanceof Error ? error.message : "Unable to fetch usage."}`, "error"));
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{ name: "/help", description: "Show commands and keyboard shortcuts", run: (ctx) => ctx.showHelp() },
|
|
101
|
+
{ name: "/exit", description: "Exit megacode", aliases: ["/quit"], run: (ctx) => ctx.quit() },
|
|
102
|
+
];
|
|
103
|
+
const parse = (input) => {
|
|
104
|
+
const [name = "", ...args] = input.trim().split(/\s+/);
|
|
105
|
+
return { name, arg: args.join(" ") };
|
|
106
|
+
};
|
|
107
|
+
const byName = (name) => COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
|
|
108
|
+
/** Whether the text is a known slash command (with or without arguments). */
|
|
109
|
+
export const isCommand = (text) => !!byName(parse(text).name);
|
|
110
|
+
/** Runs a slash command line such as "/model openai:gpt-5". */
|
|
111
|
+
export function runCommand(input, ctx) {
|
|
112
|
+
const { name, arg } = parse(input);
|
|
113
|
+
const command = byName(name);
|
|
114
|
+
if (!command)
|
|
115
|
+
return ctx.notice(`Unknown command ${name}. Type / to see commands.`, "warn");
|
|
116
|
+
command.run(ctx, arg);
|
|
117
|
+
}
|