@matterailab/orbcode 0.2.2 → 0.2.4
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 +351 -8
- package/dist/commands/mcp.js +266 -0
- package/dist/config/settings.js +27 -0
- package/dist/core/agent.js +34 -7
- package/dist/headless.js +20 -1
- package/dist/index.js +45 -2
- 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 +214 -58
- 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/StatusBar.js +28 -9
- package/dist/utils/clipboard.js +45 -0
- package/package.json +2 -1
|
@@ -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
|
-
*
|
|
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.4",
|
|
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",
|