@matterailab/orbcode 0.2.2 → 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.
@@ -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
+ }
@@ -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
- * Formats the 5hr window as "5hr limit XX% · resets <relative>".
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 fiveHourSummary(tu) {
40
- if (!tu?.fiveHour)
46
+ function usageSummary(tu) {
47
+ if (!tu)
41
48
  return null;
42
- const { percentage, resetsAt } = tu.fiveHour;
43
- return `5hr limit ${Math.round(percentage)}% · resets ${formatRelativeTime(resetsAt)}`;
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 fiveHourLine = fiveHourSummary(tieredUsage);
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, "%)"] })] }), fiveHourLine && (_jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: fiveHourLine }) }))] }));
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.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",