@matterailab/orbcode 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +405 -18
- package/dist/api/models.js +19 -3
- package/dist/commands/mcp.js +266 -0
- package/dist/config/settings.js +27 -0
- package/dist/core/agent.js +28 -7
- package/dist/headless.js +18 -0
- package/dist/index.js +9 -0
- package/dist/mcp/auth.js +289 -0
- package/dist/mcp/client.js +132 -0
- package/dist/mcp/config.js +270 -0
- package/dist/mcp/manager.js +277 -0
- package/dist/mcp/types.js +8 -0
- package/dist/memory/loader.js +167 -0
- package/dist/memory/types.js +1 -0
- package/dist/prompts/system.js +26 -7
- package/dist/skills/loader.js +132 -0
- package/dist/skills/types.js +1 -0
- package/dist/tools/executors/skills.js +28 -0
- package/dist/tools/index.js +22 -3
- package/dist/tools/schemas/index.js +6 -3
- package/dist/tools/schemas/use_skill.js +1 -1
- package/dist/ui/App.js +219 -53
- package/dist/ui/components/McpApprovalPrompt.js +61 -0
- package/dist/ui/components/McpAuthScreen.js +55 -0
- package/dist/ui/components/McpPicker.js +262 -0
- package/dist/ui/components/ModelPicker.js +2 -2
- package/dist/ui/components/StatusBar.js +28 -9
- package/dist/utils/clipboard.js +45 -0
- package/package.json +2 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { COLORS } from "../../branding.js";
|
|
5
|
+
const VISIBLE_ROWS = 8;
|
|
6
|
+
/**
|
|
7
|
+
* Shown at startup when the project's `.mcp.json` defines servers that haven't
|
|
8
|
+
* been approved yet. Project-scope servers ship in the repo and can spawn
|
|
9
|
+
* processes / open network connections, so they require explicit per-project
|
|
10
|
+
* approval — mirroring Claude Code's `.mcp.json` approval dialog.
|
|
11
|
+
*
|
|
12
|
+
* The user toggles servers with Space and confirms with Enter; Escape rejects
|
|
13
|
+
* all. Approved servers are persisted to `.orbcode/settings.json` so they
|
|
14
|
+
* auto-connect on future sessions in this project.
|
|
15
|
+
*/
|
|
16
|
+
export function McpApprovalPrompt({ serverNames, onApprove }) {
|
|
17
|
+
const [selected, setSelected] = useState(0);
|
|
18
|
+
const [checked, setChecked] = useState(new Set(serverNames));
|
|
19
|
+
useInput((input, key) => {
|
|
20
|
+
if (key.upArrow) {
|
|
21
|
+
setSelected((s) => (s - 1 + serverNames.length) % serverNames.length);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (key.downArrow || key.tab) {
|
|
25
|
+
setSelected((s) => (s + 1) % serverNames.length);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (key.escape) {
|
|
29
|
+
onApprove([]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (key.return) {
|
|
33
|
+
onApprove([...checked]);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (input === " ") {
|
|
37
|
+
const name = serverNames[selected];
|
|
38
|
+
if (!name)
|
|
39
|
+
return;
|
|
40
|
+
setChecked((prev) => {
|
|
41
|
+
const next = new Set(prev);
|
|
42
|
+
if (next.has(name))
|
|
43
|
+
next.delete(name);
|
|
44
|
+
else
|
|
45
|
+
next.add(name);
|
|
46
|
+
return next;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (serverNames.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
const count = serverNames.length;
|
|
53
|
+
const windowStart = Math.max(0, Math.min(selected - VISIBLE_ROWS + 1, count - VISIBLE_ROWS));
|
|
54
|
+
const visible = serverNames.slice(windowStart, windowStart + VISIBLE_ROWS);
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.warning, children: [count, " MCP server", count === 1 ? "" : "s", " found in .mcp.json"] }), _jsx(Text, { dimColor: true, children: "Select any you wish to enable for this project." }), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more"] }), visible.map((name, i) => {
|
|
56
|
+
const index = windowStart + i;
|
|
57
|
+
const isSelected = index === selected;
|
|
58
|
+
const isChecked = checked.has(name);
|
|
59
|
+
return (_jsxs(Text, { color: isSelected ? COLORS.accent : undefined, children: [isSelected ? "❯ " : " ", isChecked ? "☑" : "☐", " ", name] }, name));
|
|
60
|
+
}), windowStart + VISIBLE_ROWS < count && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", count - windowStart - VISIBLE_ROWS, " more"] })), _jsx(Text, { dimColor: true, children: "space toggle \u00B7 enter confirm \u00B7 esc reject all" })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { COLORS } from "../../branding.js";
|
|
5
|
+
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
6
|
+
/**
|
|
7
|
+
* Shown when the user triggers OAuth authentication for an MCP server. Mirrors
|
|
8
|
+
* Claude Code's auth screen:
|
|
9
|
+
*
|
|
10
|
+
* - "Authenticating with <server>…"
|
|
11
|
+
* - "A browser window will open for authentication"
|
|
12
|
+
* - The authorization URL (with `c` to copy it)
|
|
13
|
+
* - A paste fallback for when the browser redirect fails
|
|
14
|
+
* - "Return here after authenticating. Press Esc to go back."
|
|
15
|
+
*
|
|
16
|
+
* The auth code can arrive two ways: the loopback callback server (automatic,
|
|
17
|
+
* handled by the manager) or a manual paste here. Both resolve the same
|
|
18
|
+
* promise, so whichever fires first wins.
|
|
19
|
+
*/
|
|
20
|
+
export function McpAuthScreen({ serverName, authUrl, onPasteCode, onCancel }) {
|
|
21
|
+
const [pasted, setPasted] = useState("");
|
|
22
|
+
const [copied, setCopied] = useState(false);
|
|
23
|
+
const [mode, setMode] = useState("waiting");
|
|
24
|
+
useInput((input, key) => {
|
|
25
|
+
if (key.escape) {
|
|
26
|
+
onCancel();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (mode === "paste") {
|
|
30
|
+
if (key.return) {
|
|
31
|
+
if (pasted.trim())
|
|
32
|
+
onPasteCode(pasted.trim());
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (key.backspace || key.delete) {
|
|
36
|
+
setPasted((p) => p.slice(0, -1));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (input && !key.ctrl && !key.meta) {
|
|
40
|
+
setPasted((p) => p + input);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// waiting mode
|
|
45
|
+
if (input.toLowerCase() === "c" && authUrl) {
|
|
46
|
+
const ok = copyToClipboard(authUrl);
|
|
47
|
+
setCopied(ok);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.return) {
|
|
51
|
+
setMode("paste");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.warning, children: ["Authenticating with ", serverName, "\u2026"] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: COLORS.thinking, children: "\u273D " }), _jsx(Text, { children: "A browser window will open for authentication" })] }), _jsx(Text, { children: " " }), authUrl ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "If your browser doesn't open automatically, copy this URL manually (c to copy)" }), _jsx(Box, { paddingLeft: 1, marginTop: 0, children: _jsx(Text, { wrap: "wrap", color: COLORS.accent, children: authUrl }) }), copied && _jsx(Text, { color: COLORS.success, children: "\u2713 Copied to clipboard" })] })) : (_jsx(Text, { dimColor: true, children: "Waiting for the authorization URL\u2026" })), _jsx(Text, { children: " " }), mode === "paste" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "If the redirect page shows a connection error, paste the URL from your browser's address bar:" }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "URL> " }), pasted, _jsx(Text, { inverse: true, children: " " })] }), _jsx(Text, { dimColor: true, children: "enter to submit \u00B7 esc to go back" })] })) : (_jsx(Text, { dimColor: true, children: "Press enter to paste a redirect URL manually \u00B7 esc to go back" })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Return here after authenticating in your browser. Press Esc to go back." })] }));
|
|
55
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { COLORS } from "../../branding.js";
|
|
5
|
+
import { McpAuthScreen } from "./McpAuthScreen.js";
|
|
6
|
+
const VISIBLE_ROWS = 8;
|
|
7
|
+
function statusColor(state) {
|
|
8
|
+
switch (state.status) {
|
|
9
|
+
case "connected":
|
|
10
|
+
return COLORS.success;
|
|
11
|
+
case "connecting":
|
|
12
|
+
return COLORS.warning;
|
|
13
|
+
case "failed":
|
|
14
|
+
return COLORS.error;
|
|
15
|
+
case "needs-auth":
|
|
16
|
+
return COLORS.warning;
|
|
17
|
+
case "disabled":
|
|
18
|
+
return COLORS.dim;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function statusIcon(state) {
|
|
22
|
+
switch (state.status) {
|
|
23
|
+
case "connected":
|
|
24
|
+
return "✓";
|
|
25
|
+
case "connecting":
|
|
26
|
+
return "⋯";
|
|
27
|
+
case "failed":
|
|
28
|
+
return "✗";
|
|
29
|
+
case "needs-auth":
|
|
30
|
+
return "△";
|
|
31
|
+
case "disabled":
|
|
32
|
+
return "○";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Build the action list for a server based on its current state. */
|
|
36
|
+
function buildActions(state) {
|
|
37
|
+
const actions = [];
|
|
38
|
+
if (state.status === "needs-auth")
|
|
39
|
+
actions.push("Authenticate");
|
|
40
|
+
if (state.disabled || state.status === "disabled")
|
|
41
|
+
actions.push("Enable");
|
|
42
|
+
else
|
|
43
|
+
actions.push("Disable");
|
|
44
|
+
actions.push("Reconnect");
|
|
45
|
+
return actions;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Interactive MCP server manager, opened by the `/mcp` command.
|
|
49
|
+
*
|
|
50
|
+
* Two-level navigation (arrow keys + enter only, no shorthand keys):
|
|
51
|
+
* - Server list: ↑/↓ to select a server, enter to open its action menu.
|
|
52
|
+
* - Action menu: ↑/↓ to select an action, enter to execute, esc to go back.
|
|
53
|
+
*
|
|
54
|
+
* Actions adapt to the server's state: Authenticate (needs-auth), Enable
|
|
55
|
+
* (disabled), Disable (connected/failed), Reconnect (always).
|
|
56
|
+
*/
|
|
57
|
+
export function McpPicker({ manager, onChanged, onCancel }) {
|
|
58
|
+
const [snapshot, setSnapshot] = useState(() => manager.snapshot());
|
|
59
|
+
const [selected, setSelected] = useState(0);
|
|
60
|
+
const [busy, setBusy] = useState(false);
|
|
61
|
+
const [busyMessage, setBusyMessage] = useState("");
|
|
62
|
+
const [authingServer, setAuthingServer] = useState(null);
|
|
63
|
+
const [authUrl, setAuthUrl] = useState("");
|
|
64
|
+
const [actionMode, setActionMode] = useState(false);
|
|
65
|
+
const [actionSelected, setActionSelected] = useState(0);
|
|
66
|
+
const codeResolveRef = useRef(null);
|
|
67
|
+
const codeRejectRef = useRef(null);
|
|
68
|
+
const servers = snapshot.servers;
|
|
69
|
+
const count = servers.length;
|
|
70
|
+
const current = servers[selected];
|
|
71
|
+
const actions = current ? buildActions(current) : [];
|
|
72
|
+
useInput((_input, key) => {
|
|
73
|
+
if (authingServer || busy)
|
|
74
|
+
return;
|
|
75
|
+
if (actionMode) {
|
|
76
|
+
if (key.escape) {
|
|
77
|
+
setActionMode(false);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.upArrow) {
|
|
81
|
+
setActionSelected((s) => (s - 1 + actions.length) % actions.length);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key.downArrow || key.tab) {
|
|
85
|
+
setActionSelected((s) => (s + 1) % actions.length);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (key.return) {
|
|
89
|
+
void executeAction(actions[actionSelected]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Server list navigation.
|
|
95
|
+
if (key.escape) {
|
|
96
|
+
onCancel();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (key.upArrow) {
|
|
100
|
+
setSelected((s) => (s - 1 + count) % count);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (key.downArrow || key.tab) {
|
|
104
|
+
setSelected((s) => (s + 1) % count);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.return) {
|
|
108
|
+
setActionMode(true);
|
|
109
|
+
setActionSelected(0);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
async function refresh() {
|
|
114
|
+
setSnapshot(manager.snapshot());
|
|
115
|
+
onChanged();
|
|
116
|
+
}
|
|
117
|
+
async function executeAction(action) {
|
|
118
|
+
if (!action || !current)
|
|
119
|
+
return;
|
|
120
|
+
setActionMode(false);
|
|
121
|
+
if (action === "Authenticate")
|
|
122
|
+
await reauthSelected();
|
|
123
|
+
else if (action === "Enable")
|
|
124
|
+
await enableSelected();
|
|
125
|
+
else if (action === "Disable")
|
|
126
|
+
await disableSelected();
|
|
127
|
+
else if (action === "Reconnect")
|
|
128
|
+
await reconnectSelected();
|
|
129
|
+
}
|
|
130
|
+
async function enableSelected() {
|
|
131
|
+
if (!current)
|
|
132
|
+
return;
|
|
133
|
+
setBusy(true);
|
|
134
|
+
setBusyMessage("Enabling…");
|
|
135
|
+
try {
|
|
136
|
+
await manager.enableServer(current.name);
|
|
137
|
+
await refresh();
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
setBusy(false);
|
|
141
|
+
setBusyMessage("");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function disableSelected() {
|
|
145
|
+
if (!current)
|
|
146
|
+
return;
|
|
147
|
+
setBusy(true);
|
|
148
|
+
setBusyMessage("Disabling…");
|
|
149
|
+
try {
|
|
150
|
+
await manager.disableServer(current.name);
|
|
151
|
+
await refresh();
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
setBusy(false);
|
|
155
|
+
setBusyMessage("");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function reconnectSelected() {
|
|
159
|
+
if (!current)
|
|
160
|
+
return;
|
|
161
|
+
setBusy(true);
|
|
162
|
+
setBusyMessage("Reconnecting…");
|
|
163
|
+
try {
|
|
164
|
+
await manager.disconnectOne(current.name);
|
|
165
|
+
await manager.connectOne(current.name);
|
|
166
|
+
await refresh();
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
setBusy(false);
|
|
170
|
+
setBusyMessage("");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function reauthSelected() {
|
|
174
|
+
if (!current)
|
|
175
|
+
return;
|
|
176
|
+
const intercept = {
|
|
177
|
+
onAuthUrl: (url) => setAuthUrl(url),
|
|
178
|
+
getCode: () => new Promise((resolve, reject) => {
|
|
179
|
+
codeResolveRef.current = resolve;
|
|
180
|
+
codeRejectRef.current = reject;
|
|
181
|
+
}),
|
|
182
|
+
};
|
|
183
|
+
setAuthingServer(current.name);
|
|
184
|
+
setAuthUrl("");
|
|
185
|
+
setBusy(true);
|
|
186
|
+
try {
|
|
187
|
+
await manager.reauthServer(current.name, intercept);
|
|
188
|
+
await refresh();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// auth failed or cancelled; server stays needs-auth
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
setAuthingServer(null);
|
|
195
|
+
setAuthUrl("");
|
|
196
|
+
codeResolveRef.current = null;
|
|
197
|
+
codeRejectRef.current = null;
|
|
198
|
+
setBusy(false);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function handlePasteCode(input) {
|
|
202
|
+
try {
|
|
203
|
+
const url = new URL(input);
|
|
204
|
+
const code = url.searchParams.get("code");
|
|
205
|
+
if (code) {
|
|
206
|
+
codeResolveRef.current?.(code);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// not a URL — treat as bare code
|
|
212
|
+
}
|
|
213
|
+
codeResolveRef.current?.(input);
|
|
214
|
+
}
|
|
215
|
+
function handleAuthCancel() {
|
|
216
|
+
codeRejectRef.current?.(new Error("Authentication cancelled."));
|
|
217
|
+
}
|
|
218
|
+
if (count === 0) {
|
|
219
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsx(Text, { bold: true, color: COLORS.primary, children: "MCP servers" }), _jsx(Text, { dimColor: true, children: "No MCP servers configured." }), _jsx(Text, { dimColor: true, children: "Add servers with: orbcode mcp add <name> <command> [args...]" }), _jsx(Text, { dimColor: true, children: "esc to close" })] }));
|
|
220
|
+
}
|
|
221
|
+
if (authingServer) {
|
|
222
|
+
return (_jsx(McpAuthScreen, { serverName: authingServer, authUrl: authUrl, onPasteCode: handlePasteCode, onCancel: handleAuthCancel }));
|
|
223
|
+
}
|
|
224
|
+
const windowStart = Math.max(0, Math.min(selected - VISIBLE_ROWS + 1, count - VISIBLE_ROWS));
|
|
225
|
+
const visible = servers.slice(windowStart, windowStart + VISIBLE_ROWS);
|
|
226
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.primary, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.primary, children: ["Manage MCP servers (", count, ")"] }), windowStart > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more"] }), visible.map((state, i) => {
|
|
227
|
+
const index = windowStart + i;
|
|
228
|
+
const isSelected = index === selected && !actionMode;
|
|
229
|
+
return (_jsxs(Text, { color: isSelected ? COLORS.accent : undefined, children: [isSelected ? "❯ " : " ", state.name, " ", _jsxs(Text, { color: statusColor(state), children: [statusIcon(state), " ", state.status] }), state.toolCount > 0 && _jsxs(Text, { dimColor: true, children: [" \u00B7 ", state.toolCount, " tools"] })] }, state.name));
|
|
230
|
+
}), windowStart + VISIBLE_ROWS < count && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", count - windowStart - VISIBLE_ROWS, " more"] })), current && (_jsx(DetailPanel, { manager: manager, state: current, actions: actions, actionMode: actionMode, actionSelected: actionSelected })), _jsx(Text, { dimColor: true, children: busy
|
|
231
|
+
? busyMessage
|
|
232
|
+
: actionMode
|
|
233
|
+
? "↑/↓ select action · enter execute · esc back"
|
|
234
|
+
: "↑/↓ select server · enter open actions · esc close" })] }));
|
|
235
|
+
}
|
|
236
|
+
function DetailPanel({ manager, state, actions, actionMode, actionSelected, }) {
|
|
237
|
+
const cfg = manager.getConfig(state.name);
|
|
238
|
+
const configPath = manager.getConfigPath(state.name);
|
|
239
|
+
const isOAuth = manager.isOAuthServer(state.name);
|
|
240
|
+
const authenticated = manager.isAuthenticated(state.name);
|
|
241
|
+
const authText = !isOAuth
|
|
242
|
+
? "— (static / stdio)"
|
|
243
|
+
: authenticated
|
|
244
|
+
? "✓ authenticated"
|
|
245
|
+
: "✗ not authenticated";
|
|
246
|
+
let urlText;
|
|
247
|
+
if (cfg && (cfg.type === "http" || cfg.type === "sse")) {
|
|
248
|
+
urlText = cfg.url;
|
|
249
|
+
}
|
|
250
|
+
else if (cfg) {
|
|
251
|
+
const cmd = cfg.command;
|
|
252
|
+
const args = cfg.args ?? [];
|
|
253
|
+
urlText = `stdio: ${cmd}${args.length ? " " + args.join(" ") : ""}`;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
urlText = "";
|
|
257
|
+
}
|
|
258
|
+
return (_jsxs(Box, { paddingLeft: 1, marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, children: [state.name, " MCP Server"] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Status: " }), _jsxs(Text, { color: statusColor(state), children: [statusIcon(state), " ", state.status] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Auth: " }), _jsx(Text, { color: isOAuth && !authenticated ? COLORS.error : isOAuth ? COLORS.success : COLORS.dim, children: authText })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "URL: " }), urlText] }), configPath && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Config location: " }), _jsx(Text, { dimColor: true, children: configPath })] })), state.detail && (_jsx(Text, { color: state.status === "failed" ? COLORS.error : state.status === "needs-auth" ? COLORS.warning : COLORS.dim, children: state.detail })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: actions.map((action, i) => {
|
|
259
|
+
const isSelected = actionMode && i === actionSelected;
|
|
260
|
+
return (_jsxs(Text, { color: isSelected ? COLORS.accent : undefined, children: [isSelected ? "❯ " : " ", i + 1, ". ", action] }, action));
|
|
261
|
+
}) })] }));
|
|
262
|
+
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import { COLORS } from "../../branding.js";
|
|
5
|
-
import {
|
|
5
|
+
import { BUILTIN_AXON_MODELS } from "../../api/models.js";
|
|
6
6
|
const VISIBLE_ROWS = 6;
|
|
7
7
|
function formatPrice(model) {
|
|
8
8
|
if (model.free)
|
|
@@ -11,7 +11,7 @@ function formatPrice(model) {
|
|
|
11
11
|
return `${perMillion(model.inputPrice)} in / ${perMillion(model.outputPrice)} out per 1M tokens`;
|
|
12
12
|
}
|
|
13
13
|
export function ModelPicker({ currentId, onSelect, onCancel }) {
|
|
14
|
-
const models = Object.values(
|
|
14
|
+
const models = Object.values(BUILTIN_AXON_MODELS);
|
|
15
15
|
const [selected, setSelected] = useState(() => {
|
|
16
16
|
const index = models.findIndex((m) => m.id === currentId);
|
|
17
17
|
return index === -1 ? 0 : index;
|
|
@@ -9,11 +9,11 @@ const MODE_LABELS = {
|
|
|
9
9
|
};
|
|
10
10
|
function formatRelativeTime(isoStr) {
|
|
11
11
|
if (!isoStr)
|
|
12
|
-
return "
|
|
12
|
+
return "on session start";
|
|
13
13
|
const now = Date.now();
|
|
14
14
|
const target = new Date(isoStr).getTime();
|
|
15
15
|
if (Number.isNaN(target))
|
|
16
|
-
return "
|
|
16
|
+
return "on session start";
|
|
17
17
|
const diff = target - now;
|
|
18
18
|
if (diff <= 0)
|
|
19
19
|
return "now";
|
|
@@ -33,18 +33,37 @@ function truncate(text, max) {
|
|
|
33
33
|
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Picks the usage window that currently constrains the agent and formats it as
|
|
37
|
+
* "<window> limit XX% · resets <relative>".
|
|
38
|
+
*
|
|
39
|
+
* The smallest window (5hr) is the one actively in use, so we show it by
|
|
40
|
+
* default. We only escalate to a larger window once it has hit its cap (100%):
|
|
41
|
+
* a capped larger window is the real blocker, since a smaller window resetting
|
|
42
|
+
* won't unblock you until the larger one does. When several are capped, the
|
|
43
|
+
* largest wins (it takes longest to recover).
|
|
37
44
|
* Returns null when tiered usage is unavailable so the line can be hidden.
|
|
38
45
|
*/
|
|
39
|
-
function
|
|
40
|
-
if (!tu
|
|
46
|
+
function usageSummary(tu) {
|
|
47
|
+
if (!tu)
|
|
41
48
|
return null;
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
// Largest-to-smallest, so `find` returns the largest capped window and the
|
|
50
|
+
// last entry is the smallest available window.
|
|
51
|
+
const windows = [
|
|
52
|
+
{ label: "monthly limit", usage: tu.monthly },
|
|
53
|
+
{ label: "weekly limit", usage: tu.weekly },
|
|
54
|
+
{ label: "5hr limit", usage: tu.fiveHour },
|
|
55
|
+
];
|
|
56
|
+
const available = windows.filter((w) => Boolean(w.usage));
|
|
57
|
+
if (available.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
const binding = available.find((w) => w.usage.percentage >= 100) ??
|
|
60
|
+
available[available.length - 1];
|
|
61
|
+
const { percentage, resetsAt } = binding.usage;
|
|
62
|
+
return `${binding.label} ${Math.round(percentage)}% · resets ${formatRelativeTime(resetsAt)}`;
|
|
44
63
|
}
|
|
45
64
|
export function StatusBar({ modelId, contextTokens, totalCost: _totalCost, state, approvalMode, busy, title, plan: _plan, usagePercentage: _usagePercentage, tieredUsage, }) {
|
|
46
65
|
const model = getModel(modelId);
|
|
47
66
|
const contextPct = Math.min(100, Math.round((contextTokens / model.contextWindow) * 100));
|
|
48
|
-
const
|
|
49
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: approvalMode === "ask" ? undefined : COLORS.warning, children: MODE_LABELS[approvalMode] }), " (shift+tab to cycle)", busy && " · esc to interrupt", state ? _jsxs(Text, { color: COLORS.thinking, children: [" \u00B7 ", state] }) : null] }), _jsxs(Text, { dimColor: true, children: [title ? `${truncate(title, 32)} · ` : "", model.name, " \u00B7 ctx ", contextTokens.toLocaleString(), " (", contextPct, "%)"] })] }),
|
|
67
|
+
const usageLine = usageSummary(tieredUsage);
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: approvalMode === "ask" ? undefined : COLORS.warning, children: MODE_LABELS[approvalMode] }), " (shift+tab to cycle)", busy && " · esc to interrupt", state ? _jsxs(Text, { color: COLORS.thinking, children: [" \u00B7 ", state] }) : null] }), _jsxs(Text, { dimColor: true, children: [title ? `${truncate(title, 32)} · ` : "", model.name, " \u00B7 ctx ", contextTokens.toLocaleString(), " (", contextPct, "%)"] })] }), usageLine && (_jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: usageLine }) }))] }));
|
|
50
69
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Copy text to the system clipboard. Uses the platform's native clipboard
|
|
5
|
+
* utility (pbcopy on macOS, xclip/xsel on Linux, clip on Windows). Returns
|
|
6
|
+
* true on success, false if no clipboard utility is available.
|
|
7
|
+
*/
|
|
8
|
+
export function copyToClipboard(text) {
|
|
9
|
+
const cmd = clipboardCommand();
|
|
10
|
+
if (!cmd)
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
execSync(cmd, { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Detect the platform's clipboard command, or null if none is available. */
|
|
21
|
+
function clipboardCommand() {
|
|
22
|
+
const p = platform();
|
|
23
|
+
if (p === "darwin")
|
|
24
|
+
return "pbcopy";
|
|
25
|
+
if (p === "win32")
|
|
26
|
+
return "clip";
|
|
27
|
+
// Linux: try xclip, then xsel. We can't check availability without spawning,
|
|
28
|
+
// so prefer xclip (more common on modern distros) and fall back to xsel.
|
|
29
|
+
if (p === "linux") {
|
|
30
|
+
try {
|
|
31
|
+
execSync("which xclip", { stdio: "ignore" });
|
|
32
|
+
return "xclip -selection clipboard";
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
try {
|
|
36
|
+
execSync("which xsel", { stdio: "ignore" });
|
|
37
|
+
return "xsel --clipboard --input";
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matterailab/orbcode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@ai-sdk/anthropic": "^3.0.85",
|
|
40
40
|
"@ai-sdk/openai-compatible": "^2.0.51",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
42
|
"ai": "^6.0.207",
|
|
42
43
|
"chalk": "^5.3.0",
|
|
43
44
|
"ink": "^5.2.1",
|