@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
package/dist/tools/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fileEdit, fileWrite, multiFileEdit, readFile } from "./executors/files.
|
|
|
3
3
|
import { listFiles } from "./executors/listFiles.js";
|
|
4
4
|
import { searchFiles } from "./executors/searchFiles.js";
|
|
5
5
|
import { executeCommand } from "./executors/executeCommand.js";
|
|
6
|
+
import { useSkill } from "./executors/skills.js";
|
|
6
7
|
import { webFetch, webSearch } from "./executors/web.js";
|
|
7
8
|
export { nativeTools };
|
|
8
9
|
/** Tools that modify the user's system and need approval before running. */
|
|
@@ -44,11 +45,18 @@ export function describeToolCall(toolName, args) {
|
|
|
44
45
|
return String(args.url ?? "");
|
|
45
46
|
case "update_todo_list":
|
|
46
47
|
return "updating tasks";
|
|
48
|
+
case "use_skill":
|
|
49
|
+
return String(args.skill_name ?? "");
|
|
47
50
|
case "ask_followup_question":
|
|
48
51
|
return String(args.question ?? "");
|
|
49
52
|
case "attempt_completion":
|
|
50
53
|
return "task complete";
|
|
51
54
|
default:
|
|
55
|
+
// MCP tools (mcp__<server>__<tool>) — show the server + tool name.
|
|
56
|
+
if (/^mcp__/.test(toolName)) {
|
|
57
|
+
const match = /^mcp__([^_]+)__(.+)$/.exec(toolName);
|
|
58
|
+
return match ? `${match[2]} (${match[1]})` : toolName;
|
|
59
|
+
}
|
|
52
60
|
return toolName;
|
|
53
61
|
}
|
|
54
62
|
}
|
|
@@ -62,13 +70,20 @@ const executors = {
|
|
|
62
70
|
execute_command: executeCommand,
|
|
63
71
|
web_search: webSearch,
|
|
64
72
|
web_fetch: webFetch,
|
|
73
|
+
use_skill: useSkill,
|
|
65
74
|
update_todo_list: async (args, context) => {
|
|
66
75
|
context.setTodos(String(args.todos ?? ""));
|
|
67
76
|
return { text: "Todo list updated." };
|
|
68
77
|
},
|
|
69
78
|
// ask_followup_question and attempt_completion are handled by the agent loop.
|
|
79
|
+
// MCP tools (mcp__<server>__<tool>) are routed via the McpManager in executeTool.
|
|
70
80
|
};
|
|
71
|
-
export async function executeTool(toolName, args, context) {
|
|
81
|
+
export async function executeTool(toolName, args, context, mcp) {
|
|
82
|
+
// MCP tools are namespaced as mcp__<server>__<tool> and routed to the manager.
|
|
83
|
+
if (mcp && mcp.hasTool(toolName)) {
|
|
84
|
+
const result = await mcp.callTool(toolName, args);
|
|
85
|
+
return { text: result.text, isError: result.isError };
|
|
86
|
+
}
|
|
72
87
|
const executor = executors[toolName];
|
|
73
88
|
if (!executor) {
|
|
74
89
|
return { text: `Tool "${toolName}" is not available in OrbCode CLI.`, isError: true };
|
|
@@ -80,6 +95,10 @@ export async function executeTool(toolName, args, context) {
|
|
|
80
95
|
return { text: `Tool ${toolName} failed: ${error.message}`, isError: true };
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
/** Native tools plus any MCP tools from connected servers. */
|
|
99
|
+
export function getActiveTools(mcp) {
|
|
100
|
+
const tools = [...nativeTools];
|
|
101
|
+
if (mcp)
|
|
102
|
+
tools.push(...mcp.getTools());
|
|
103
|
+
return tools;
|
|
85
104
|
}
|
|
@@ -8,11 +8,13 @@ import listFiles from "./list_files.js";
|
|
|
8
8
|
import { read_file_single } from "./read_file.js";
|
|
9
9
|
import searchFiles from "./search_files.js";
|
|
10
10
|
import updateTodoList from "./update_todo_list.js";
|
|
11
|
+
import useSkill from "./use_skill.js";
|
|
11
12
|
import webFetch from "./web_fetch.js";
|
|
12
13
|
import webSearch from "./web_search.js";
|
|
13
|
-
// Native tool schemas ported
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// Native tool schemas ported from the Orbital extension. IDE-only tools
|
|
15
|
+
// (codebase_search, lsp, check_past_chat_memories, browser_action, …) are not
|
|
16
|
+
// active in the CLI. use_skill is now active: skills are loaded from
|
|
17
|
+
// ~/.orbcode/skills/ and .orbcode/skills/ (see src/skills/loader.ts).
|
|
16
18
|
export const nativeTools = [
|
|
17
19
|
fileEdit,
|
|
18
20
|
multiFileEdit,
|
|
@@ -24,6 +26,7 @@ export const nativeTools = [
|
|
|
24
26
|
read_file_single,
|
|
25
27
|
searchFiles,
|
|
26
28
|
updateTodoList,
|
|
29
|
+
useSkill,
|
|
27
30
|
webFetch,
|
|
28
31
|
webSearch,
|
|
29
32
|
];
|
|
@@ -2,7 +2,7 @@ export default {
|
|
|
2
2
|
type: "function",
|
|
3
3
|
function: {
|
|
4
4
|
name: "use_skill",
|
|
5
|
-
description: "Use a specific skill to guide the task execution. This tool
|
|
5
|
+
description: "Use a specific skill to guide the task execution. This tool applies predefined skills stored in ~/.orbcode/skills/ (user) and .orbcode/skills/ (project). Each skill is a directory containing a SKILL.md file with specialized instructions for performing specific tasks or following particular patterns. The available skills are listed in the 'Available Skills' section of your system prompt — invoke this tool with a skill_name from that list when the task matches a skill's when-to-use condition.",
|
|
6
6
|
strict: true,
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
package/dist/ui/App.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { Box, Static, Text, useApp, useInput } from "ink";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import { COLORS, VERSION } from "../branding.js";
|
|
6
|
-
import {
|
|
6
|
+
import { BUILTIN_AXON_MODELS, getModel, isValidAxonModel, } from "../api/models.js";
|
|
7
7
|
import { LoginView } from "./LoginView.js";
|
|
8
|
-
import { APP_URL,
|
|
9
|
-
import { getAuthToken, getPendingProjectHooks, loadSettings, saveSettings, trustProjectHooks, } from "../config/settings.js";
|
|
8
|
+
import { APP_URL, fetchProfile, fetchTaskTitle, } from "../auth/auth.js";
|
|
9
|
+
import { getAuthToken, getPendingProjectHooks, loadSettings, saveMcpApproval, saveSettings, trustProjectHooks, } from "../config/settings.js";
|
|
10
10
|
import { Agent } from "../core/agent.js";
|
|
11
|
+
import { McpManager } from "../mcp/manager.js";
|
|
11
12
|
import { Spinner } from "./components/Spinner.js";
|
|
12
13
|
import { InputBox } from "./components/InputBox.js";
|
|
13
14
|
import { ApprovalPrompt } from "./components/ApprovalPrompt.js";
|
|
14
15
|
import { FollowupPrompt } from "./components/FollowupPrompt.js";
|
|
15
16
|
import { HookTrustPrompt } from "./components/HookTrustPrompt.js";
|
|
17
|
+
import { McpApprovalPrompt } from "./components/McpApprovalPrompt.js";
|
|
18
|
+
import { McpPicker } from "./components/McpPicker.js";
|
|
16
19
|
import { StatusBar } from "./components/StatusBar.js";
|
|
17
20
|
import { ModelPicker } from "./components/ModelPicker.js";
|
|
18
21
|
import { SessionPicker } from "./components/SessionPicker.js";
|
|
@@ -21,16 +24,38 @@ import { RowView, formatToolName } from "./components/rows.js";
|
|
|
21
24
|
const SLASH_COMMANDS = [
|
|
22
25
|
{ name: "/help", description: "show available commands" },
|
|
23
26
|
{ name: "/model", description: "select the Axon model to use" },
|
|
24
|
-
{
|
|
27
|
+
{
|
|
28
|
+
name: "/clear",
|
|
29
|
+
description: "clear the screen — the conversation continues",
|
|
30
|
+
},
|
|
25
31
|
{ name: "/new", description: "start a new conversation with a clean slate" },
|
|
26
32
|
{ name: "/resume", description: "resume a previous session" },
|
|
27
|
-
{
|
|
33
|
+
{
|
|
34
|
+
name: "/compact",
|
|
35
|
+
description: "summarize the conversation to free up context",
|
|
36
|
+
},
|
|
28
37
|
{ name: "/tasks", description: "show the current task list" },
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{ name: "/
|
|
38
|
+
{
|
|
39
|
+
name: "/status",
|
|
40
|
+
description: "show session status (model, context, cost, account)",
|
|
41
|
+
},
|
|
42
|
+
{ name: "/usage", description: "fetch plan usage" },
|
|
43
|
+
{
|
|
44
|
+
name: "/init",
|
|
45
|
+
description: "analyze this codebase and create an AGENTS.md",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "/mcp",
|
|
49
|
+
description: "manage MCP servers — enable, disable, reconnect, view status",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "/commit",
|
|
53
|
+
description: "check pending changes and create detailed commits",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "/code-review",
|
|
57
|
+
description: "expert review of pending changes: performance, security, bugs, tests",
|
|
58
|
+
},
|
|
34
59
|
{ name: "/analytics", description: "open your MatterAI analytics dashboard" },
|
|
35
60
|
{ name: "/login", description: "sign in to MatterAI" },
|
|
36
61
|
{ name: "/logout", description: "sign out and remove the saved token" },
|
|
@@ -64,17 +89,11 @@ function formatRelativeTime(isoStr) {
|
|
|
64
89
|
return `in ${min}m`;
|
|
65
90
|
return "soon";
|
|
66
91
|
}
|
|
67
|
-
/** Human lines for the /status and /
|
|
92
|
+
/** Human lines for the /status and /usage usage block (extension profile data). */
|
|
68
93
|
function usageLines(profile) {
|
|
69
94
|
const lines = [];
|
|
70
95
|
if (profile.plan)
|
|
71
|
-
lines.push(`Plan ${profile.plan}`);
|
|
72
|
-
if (typeof profile.usagePercentage === "number") {
|
|
73
|
-
lines.push(`Usage ${profile.usagePercentage}% used · ${Math.max(0, 100 - profile.usagePercentage)}% remaining`);
|
|
74
|
-
}
|
|
75
|
-
if (typeof profile.remainingReviews === "number") {
|
|
76
|
-
lines.push(`Reviews ${profile.remainingReviews} remaining`);
|
|
77
|
-
}
|
|
96
|
+
lines.push(`Plan ${profile.plan?.toUpperCase()}`);
|
|
78
97
|
if (profile.tieredUsage) {
|
|
79
98
|
const ws = [
|
|
80
99
|
["5hr", "5-Hour", profile.tieredUsage.fiveHour],
|
|
@@ -82,10 +101,9 @@ function usageLines(profile) {
|
|
|
82
101
|
["mo", "Monthly", profile.tieredUsage.monthly],
|
|
83
102
|
];
|
|
84
103
|
for (const [short, label, w] of ws) {
|
|
85
|
-
const remaining = Math.max(0, w.remaining || 0);
|
|
86
104
|
const pct = Math.max(0, Math.min(100, w.percentage || 0));
|
|
87
105
|
const reset = formatRelativeTime(w.resetsAt);
|
|
88
|
-
lines.push(`${label.padEnd(12)}
|
|
106
|
+
lines.push(`${label.padEnd(12)}${pct}% used · Resets ${reset}`);
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
109
|
else if (profile.creditsResetDate) {
|
|
@@ -149,7 +167,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
149
167
|
const [view, setView] = useState(initialView ?? (getAuthToken(settings) ? "chat" : "login"));
|
|
150
168
|
const [updateInfo, setUpdateInfo] = useState(null);
|
|
151
169
|
const [rows, setRows] = useState(() => [
|
|
152
|
-
{
|
|
170
|
+
{
|
|
171
|
+
kind: "header",
|
|
172
|
+
id: "header",
|
|
173
|
+
cwd: process.cwd(),
|
|
174
|
+
modelName: getModel(loadSettings().model).name,
|
|
175
|
+
},
|
|
153
176
|
]);
|
|
154
177
|
const [busy, setBusy] = useState(false);
|
|
155
178
|
const [busyLabel, setBusyLabel] = useState("Thinking");
|
|
@@ -167,6 +190,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
167
190
|
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
168
191
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
169
192
|
const [resumableSessions, setResumableSessions] = useState(null);
|
|
193
|
+
// MCP manager (created once, shared across agents in this process). Null until
|
|
194
|
+
// the first agent is created so we don't spawn servers before login.
|
|
195
|
+
const mcpManagerRef = useRef(null);
|
|
196
|
+
const [mcpPickerOpen, setMcpPickerOpen] = useState(false);
|
|
197
|
+
// Project-scope MCP servers awaiting the user's approval at startup.
|
|
198
|
+
const [pendingMcpApproval, setPendingMcpApproval] = useState(null);
|
|
170
199
|
const [staticKey, setStaticKey] = useState(0);
|
|
171
200
|
const [tasks, setTasks] = useState("");
|
|
172
201
|
const [contextTokens, setContextTokens] = useState(0);
|
|
@@ -184,7 +213,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
184
213
|
if (!token)
|
|
185
214
|
return;
|
|
186
215
|
fetchProfile(token)
|
|
187
|
-
.then((profile) => setUsage({
|
|
216
|
+
.then((profile) => setUsage({
|
|
217
|
+
plan: profile.plan,
|
|
218
|
+
usagePercentage: profile.usagePercentage,
|
|
219
|
+
tieredUsage: profile.tieredUsage,
|
|
220
|
+
}))
|
|
188
221
|
.catch(() => { });
|
|
189
222
|
}, []);
|
|
190
223
|
const agentRef = useRef(null);
|
|
@@ -243,7 +276,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
243
276
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
244
277
|
}
|
|
245
278
|
setRows([
|
|
246
|
-
{
|
|
279
|
+
{
|
|
280
|
+
kind: "header",
|
|
281
|
+
id: rowId(),
|
|
282
|
+
cwd: process.cwd(),
|
|
283
|
+
modelName: getModel(loadSettings().model).name,
|
|
284
|
+
},
|
|
247
285
|
]);
|
|
248
286
|
setStaticKey((k) => k + 1);
|
|
249
287
|
}, []);
|
|
@@ -275,7 +313,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
275
313
|
setStreamingText("");
|
|
276
314
|
break;
|
|
277
315
|
case "tool-start":
|
|
278
|
-
setRunningTool({
|
|
316
|
+
setRunningTool({
|
|
317
|
+
id: event.id,
|
|
318
|
+
name: event.name,
|
|
319
|
+
summary: event.summary,
|
|
320
|
+
});
|
|
279
321
|
setBusyLabel(`Running ${event.name}`);
|
|
280
322
|
break;
|
|
281
323
|
case "tool-end":
|
|
@@ -305,7 +347,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
305
347
|
pushRow({ kind: "completion", text: event.result });
|
|
306
348
|
break;
|
|
307
349
|
case "system":
|
|
308
|
-
pushRow({
|
|
350
|
+
pushRow({
|
|
351
|
+
kind: event.isError ? "error" : "info",
|
|
352
|
+
text: event.message,
|
|
353
|
+
});
|
|
309
354
|
break;
|
|
310
355
|
case "error":
|
|
311
356
|
pushRow({ kind: "error", text: event.message });
|
|
@@ -346,6 +391,13 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
346
391
|
}, [pushRow, maybeFetchTitle, refreshUsage, drainQueue]);
|
|
347
392
|
const createAgent = useCallback((resume) => {
|
|
348
393
|
const current = loadSettings();
|
|
394
|
+
// Create the MCP manager once per process and reuse it across agents
|
|
395
|
+
// (so /new and /resume keep the same server connections). It reads
|
|
396
|
+
// the enabled/disabled lists from settings and connects to approved
|
|
397
|
+
// servers lazily on start().
|
|
398
|
+
if (!mcpManagerRef.current) {
|
|
399
|
+
mcpManagerRef.current = new McpManager(process.cwd(), current.disabledMcpServers ?? [], current.enabledMcpServers ?? []);
|
|
400
|
+
}
|
|
349
401
|
return new Agent({
|
|
350
402
|
cwd: process.cwd(),
|
|
351
403
|
token: getAuthToken(current),
|
|
@@ -355,6 +407,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
355
407
|
autoApproveEdits: current.autoApproveEdits,
|
|
356
408
|
autoApproveSafeCommands: current.autoApproveSafeCommands,
|
|
357
409
|
hooks: current.hooks,
|
|
410
|
+
mcp: mcpManagerRef.current,
|
|
358
411
|
resume,
|
|
359
412
|
callbacks: {
|
|
360
413
|
onEvent: handleEvent,
|
|
@@ -393,23 +446,33 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
393
446
|
if (match)
|
|
394
447
|
pushRow({ kind: "user", text: match[1] });
|
|
395
448
|
}
|
|
396
|
-
else if (message.role === "assistant" &&
|
|
449
|
+
else if (message.role === "assistant" &&
|
|
450
|
+
typeof message.content === "string" &&
|
|
451
|
+
message.content.trim()) {
|
|
397
452
|
pushRow({ kind: "assistant", text: message.content });
|
|
398
453
|
}
|
|
399
454
|
}
|
|
400
|
-
pushRow({
|
|
455
|
+
pushRow({
|
|
456
|
+
kind: "info",
|
|
457
|
+
text: `Resumed session: ${session.title || session.id}`,
|
|
458
|
+
});
|
|
401
459
|
}, [createAgent, pushRow, resetTranscript]);
|
|
402
460
|
const switchModel = useCallback((modelId) => {
|
|
403
461
|
const updated = { ...loadSettings(), model: modelId };
|
|
404
462
|
setSettings(updated);
|
|
405
463
|
saveSettings(updated);
|
|
406
464
|
agentRef.current?.setModel(modelId);
|
|
407
|
-
pushRow({
|
|
465
|
+
pushRow({
|
|
466
|
+
kind: "info",
|
|
467
|
+
text: `Model switched to ${getModel(modelId).name}`,
|
|
468
|
+
});
|
|
408
469
|
}, [pushRow]);
|
|
409
470
|
// Fire SessionEnd hooks (best-effort, capped at 3s) before quitting.
|
|
410
471
|
const endAndExit = useCallback((reason) => {
|
|
411
472
|
const agent = agentRef.current;
|
|
412
473
|
if (!agent) {
|
|
474
|
+
// No agent yet, but the MCP manager may have started; tear it down.
|
|
475
|
+
void mcpManagerRef.current?.stop().catch(() => { });
|
|
413
476
|
exit();
|
|
414
477
|
return;
|
|
415
478
|
}
|
|
@@ -444,15 +507,26 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
444
507
|
});
|
|
445
508
|
break;
|
|
446
509
|
case "/model": {
|
|
447
|
-
|
|
448
|
-
|
|
510
|
+
// The interactive picker is restricted to Axon's own models for now.
|
|
511
|
+
// Third-party providers (Anthropic, OpenAI-compatible) are still
|
|
512
|
+
// usable headlessly via `orbcode -p "..." --model <id>` — see
|
|
513
|
+
// the README "Other providers" section.
|
|
514
|
+
const pickerIds = Object.keys(BUILTIN_AXON_MODELS);
|
|
515
|
+
if (arg && pickerIds.includes(arg)) {
|
|
449
516
|
switchModel(arg);
|
|
450
517
|
}
|
|
518
|
+
else if (arg && isValidAxonModel(arg)) {
|
|
519
|
+
// Recognized id, but it's a third-party model: not selectable in the TUI.
|
|
520
|
+
pushRow({
|
|
521
|
+
kind: "info",
|
|
522
|
+
text: `Model "${arg}" is only available in non-interactive mode. Run: orbcode -p "..." --model ${arg}`,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
451
525
|
else if (arg) {
|
|
452
526
|
// Allow short suffixes like "pro" or "mini" to resolve to the
|
|
453
527
|
// latest matching registered id, so /model pro keeps working
|
|
454
528
|
// as new model generations are added.
|
|
455
|
-
const matches =
|
|
529
|
+
const matches = pickerIds
|
|
456
530
|
.filter((id) => id.endsWith(`-${arg}`))
|
|
457
531
|
.sort()
|
|
458
532
|
.reverse();
|
|
@@ -460,7 +534,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
460
534
|
switchModel(matches[0]);
|
|
461
535
|
}
|
|
462
536
|
else {
|
|
463
|
-
pushRow({
|
|
537
|
+
pushRow({
|
|
538
|
+
kind: "error",
|
|
539
|
+
text: `Unknown model "${arg}". Available: ${pickerIds.join(", ")}`,
|
|
540
|
+
});
|
|
464
541
|
}
|
|
465
542
|
}
|
|
466
543
|
else {
|
|
@@ -493,7 +570,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
493
570
|
case "/resume": {
|
|
494
571
|
const sessions = listSessions(process.cwd()).filter((s) => s.id !== agentRef.current?.taskId);
|
|
495
572
|
if (sessions.length === 0) {
|
|
496
|
-
pushRow({
|
|
573
|
+
pushRow({
|
|
574
|
+
kind: "info",
|
|
575
|
+
text: "No previous sessions found for this directory.",
|
|
576
|
+
});
|
|
497
577
|
break;
|
|
498
578
|
}
|
|
499
579
|
setResumableSessions(sessions);
|
|
@@ -553,6 +633,18 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
553
633
|
setBusyLabel("Thinking");
|
|
554
634
|
void getAgent().runTurn(INIT_PROMPT);
|
|
555
635
|
break;
|
|
636
|
+
case "/mcp": {
|
|
637
|
+
const manager = mcpManagerRef.current;
|
|
638
|
+
if (!manager) {
|
|
639
|
+
pushRow({
|
|
640
|
+
kind: "info",
|
|
641
|
+
text: "MCP not initialized yet — send a message first.",
|
|
642
|
+
});
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
setMcpPickerOpen(true);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
556
648
|
case "/commit":
|
|
557
649
|
if (!getAuthToken(settings)) {
|
|
558
650
|
setView("login");
|
|
@@ -576,15 +668,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
576
668
|
case "/version":
|
|
577
669
|
pushRow({ kind: "info", text: `OrbCode CLI v${VERSION}` });
|
|
578
670
|
break;
|
|
579
|
-
case "/
|
|
580
|
-
pushRow({ kind: "info", text:
|
|
671
|
+
case "/usage": {
|
|
672
|
+
pushRow({ kind: "info", text: "Fetching plan usage…" });
|
|
581
673
|
const token = getAuthToken(settings);
|
|
582
674
|
if (token) {
|
|
583
|
-
fetchBalance(token, settings.organizationId).then((balance) => {
|
|
584
|
-
if (balance !== undefined) {
|
|
585
|
-
pushRow({ kind: "info", text: `Account balance: $${balance.toFixed(2)}` });
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
675
|
fetchProfile(token)
|
|
589
676
|
.then((profile) => {
|
|
590
677
|
const lines = usageLines(profile);
|
|
@@ -614,9 +701,24 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
614
701
|
endAndExit("prompt_input_exit");
|
|
615
702
|
break;
|
|
616
703
|
default:
|
|
617
|
-
pushRow({
|
|
704
|
+
pushRow({
|
|
705
|
+
kind: "error",
|
|
706
|
+
text: `Unknown command: ${name}. Try /help.`,
|
|
707
|
+
});
|
|
618
708
|
}
|
|
619
|
-
}, [
|
|
709
|
+
}, [
|
|
710
|
+
settings,
|
|
711
|
+
tasks,
|
|
712
|
+
contextTokens,
|
|
713
|
+
totalCost,
|
|
714
|
+
sessionTitle,
|
|
715
|
+
endAndExit,
|
|
716
|
+
pushRow,
|
|
717
|
+
getAgent,
|
|
718
|
+
switchModel,
|
|
719
|
+
resetTranscript,
|
|
720
|
+
clearQueue,
|
|
721
|
+
]);
|
|
620
722
|
const handleSubmit = useCallback((value) => {
|
|
621
723
|
if (value.startsWith("/")) {
|
|
622
724
|
handleCommand(value);
|
|
@@ -647,7 +749,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
647
749
|
const updated = loadSettings();
|
|
648
750
|
setSettings(updated);
|
|
649
751
|
agentRef.current?.setHooks(updated.hooks);
|
|
650
|
-
pushRow({
|
|
752
|
+
pushRow({
|
|
753
|
+
kind: "info",
|
|
754
|
+
text: "Project hooks trusted — enabled for this workspace.",
|
|
755
|
+
});
|
|
651
756
|
}
|
|
652
757
|
else {
|
|
653
758
|
pushRow({
|
|
@@ -660,6 +765,28 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
660
765
|
if (deferred)
|
|
661
766
|
handleSubmit(deferred);
|
|
662
767
|
}, [handleSubmit, pushRow]);
|
|
768
|
+
// Resolve the MCP server approval prompt: connect to the approved project
|
|
769
|
+
// servers, persist the decision, and proceed with any deferred startup prompt.
|
|
770
|
+
const resolveMcpApproval = useCallback(async (approved) => {
|
|
771
|
+
setPendingMcpApproval(null);
|
|
772
|
+
const manager = mcpManagerRef.current;
|
|
773
|
+
if (manager) {
|
|
774
|
+
// Enable each approved server (connects immediately). Disapproved
|
|
775
|
+
// ones stay disabled and won't prompt again (persisted below).
|
|
776
|
+
await Promise.all(approved.map((name) => manager.enableServer(name).catch(() => { })));
|
|
777
|
+
saveMcpApproval(process.cwd(), manager.getEnabled(), manager.getDisabled());
|
|
778
|
+
const snap = manager.snapshot();
|
|
779
|
+
const connected = snap.servers.filter((s) => s.status === "connected").length;
|
|
780
|
+
pushRow({
|
|
781
|
+
kind: "info",
|
|
782
|
+
text: `MCP: ${connected}/${snap.servers.length} server${snap.servers.length === 1 ? "" : "s"} connected${approved.length ? ` (approved: ${approved.join(", ")})` : ""}.`,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const deferred = deferredPromptRef.current;
|
|
786
|
+
deferredPromptRef.current = null;
|
|
787
|
+
if (deferred)
|
|
788
|
+
handleSubmit(deferred);
|
|
789
|
+
}, [handleSubmit, pushRow]);
|
|
663
790
|
// Apply --resume and an initial prompt (`orbcode "do something"`) on startup.
|
|
664
791
|
const bootedRef = useRef(false);
|
|
665
792
|
useEffect(() => {
|
|
@@ -678,6 +805,19 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
678
805
|
else if (initialPrompt) {
|
|
679
806
|
handleSubmit(initialPrompt);
|
|
680
807
|
}
|
|
808
|
+
// Start the MCP manager and surface any project-scope servers that need
|
|
809
|
+
// approval. The approval prompt is non-blocking: the agent can start
|
|
810
|
+
// working while the user decides, and approved servers connect live.
|
|
811
|
+
const current = loadSettings();
|
|
812
|
+
if (getAuthToken(current)) {
|
|
813
|
+
const manager = new McpManager(process.cwd(), current.disabledMcpServers ?? [], current.enabledMcpServers ?? []);
|
|
814
|
+
mcpManagerRef.current = manager;
|
|
815
|
+
void manager.start().then(() => {
|
|
816
|
+
const pendingMcp = manager.getPendingApproval();
|
|
817
|
+
if (pendingMcp.length > 0)
|
|
818
|
+
setPendingMcpApproval(pendingMcp);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
681
821
|
refreshUsage();
|
|
682
822
|
}, [initialSession, initialPrompt, handleResume, handleSubmit, refreshUsage]);
|
|
683
823
|
// Resolve the npm version check after first paint so the TUI shows up
|
|
@@ -710,7 +850,11 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
710
850
|
const next = prev === "ask" ? "edits" : prev === "edits" ? "auto" : "ask";
|
|
711
851
|
const autoApproveEdits = next !== "ask";
|
|
712
852
|
const autoApproveSafeCommands = next === "auto";
|
|
713
|
-
const updated = {
|
|
853
|
+
const updated = {
|
|
854
|
+
...loadSettings(),
|
|
855
|
+
autoApproveEdits,
|
|
856
|
+
autoApproveSafeCommands,
|
|
857
|
+
};
|
|
714
858
|
setSettings(updated);
|
|
715
859
|
saveSettings(updated);
|
|
716
860
|
agentRef.current?.setApprovalMode(autoApproveEdits, autoApproveSafeCommands);
|
|
@@ -722,7 +866,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
722
866
|
expandReasoningRef.current = expanded;
|
|
723
867
|
// Re-render the whole transcript (including past thinking) with the
|
|
724
868
|
// new expansion state: clear the screen and remount <Static>.
|
|
725
|
-
setRows((prev) => prev.map((row) =>
|
|
869
|
+
setRows((prev) => prev.map((row) => row.kind === "reasoning" ? { ...row, expanded } : row));
|
|
726
870
|
if (process.stdout.isTTY) {
|
|
727
871
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
728
872
|
}
|
|
@@ -735,31 +879,53 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
735
879
|
saveSettings(updated);
|
|
736
880
|
agentRef.current = null;
|
|
737
881
|
setView("chat");
|
|
738
|
-
setUsage({
|
|
882
|
+
setUsage({
|
|
883
|
+
plan: profile.plan,
|
|
884
|
+
usagePercentage: profile.usagePercentage,
|
|
885
|
+
tieredUsage: profile.tieredUsage,
|
|
886
|
+
});
|
|
739
887
|
const who = profile.user?.name || profile.user?.email;
|
|
740
|
-
pushRow({
|
|
888
|
+
pushRow({
|
|
889
|
+
kind: "info",
|
|
890
|
+
text: `Signed in${who ? ` as ${who}` : ""}. Ready when you are.`,
|
|
891
|
+
});
|
|
741
892
|
}, [pushRow]);
|
|
742
|
-
const taskLines = useMemo(() => tasks
|
|
893
|
+
const taskLines = useMemo(() => tasks
|
|
894
|
+
.split("\n")
|
|
895
|
+
.map((l) => l.trim())
|
|
896
|
+
.filter(Boolean), [tasks]);
|
|
743
897
|
const inputActive = view === "chat" &&
|
|
744
898
|
!pendingApproval &&
|
|
745
899
|
!pendingFollowup &&
|
|
746
900
|
!pendingHookTrust &&
|
|
901
|
+
!pendingMcpApproval &&
|
|
747
902
|
!modelPickerOpen &&
|
|
903
|
+
!mcpPickerOpen &&
|
|
748
904
|
!resumableSessions;
|
|
749
905
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: rows, children: (row) => _jsx(RowView, { row: row }, row.id) }, staticKey), view === "login" ? (_jsx(LoginSection, { onLogin: handleLogin })) : (_jsxs(Box, { flexDirection: "column", children: [updateInfo?.updateAvailable && updateInfo.latest && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 2, alignSelf: "flex-start", children: [_jsxs(Text, { color: COLORS.warning, bold: true, children: ["\u2191 Update available: v", updateInfo.current, " \u2192 v", updateInfo.latest] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: COLORS.accent, children: "orbcode update" }), " to install the latest version, then relaunch."] })] })), streamingReasoning && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: COLORS.thinking, italic: true, children: "\u2726 Thinking\u2026" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: tail(streamingReasoning, expandReasoningRef.current ? 30 : 3) }) })] })), streamingText && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, children: "\u25CF " }), streamingText] }) })), runningTool && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: COLORS.warning, children: [formatToolName(runningTool.name), " ", _jsx(Text, { dimColor: true, children: runningTool.summary })] }) })), taskLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "Tasks" }), taskLines.slice(0, 10).map((line, i) => (_jsx(Text, { dimColor: /^[-*]\s*\[x\]/i.test(line), children: line
|
|
750
906
|
.replace(/^[-*]\s*\[x\]/i, " ■")
|
|
751
907
|
.replace(/^[-*]\s*\[-\]/, " ◧")
|
|
752
|
-
.replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] })] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
|
|
908
|
+
.replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] }))] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
|
|
753
909
|
setModelPickerOpen(false);
|
|
754
910
|
switchModel(modelId);
|
|
755
|
-
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })),
|
|
911
|
+
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })), pendingMcpApproval && (_jsx(Box, { marginTop: 1, children: _jsx(McpApprovalPrompt, { serverNames: pendingMcpApproval, onApprove: resolveMcpApproval }) })), mcpPickerOpen && mcpManagerRef.current && (_jsx(Box, { marginTop: 1, children: _jsx(McpPicker, { manager: mcpManagerRef.current, onChanged: () => {
|
|
912
|
+
const m = mcpManagerRef.current;
|
|
913
|
+
if (m)
|
|
914
|
+
saveMcpApproval(process.cwd(), m.getEnabled(), m.getDisabled());
|
|
915
|
+
}, onCancel: () => setMcpPickerOpen(false) }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
|
|
756
916
|
pendingApproval.resolve(decision);
|
|
757
917
|
setPendingApproval(null);
|
|
758
918
|
} }) })), pendingFollowup && (_jsx(Box, { marginTop: 1, children: _jsx(FollowupPrompt, { question: pendingFollowup.question, suggestions: pendingFollowup.suggestions, onAnswer: (answer) => {
|
|
759
919
|
pushRow({ kind: "user", text: answer });
|
|
760
920
|
pendingFollowup.resolve(answer);
|
|
761
921
|
setPendingFollowup(null);
|
|
762
|
-
} }) })), busy &&
|
|
922
|
+
} }) })), busy &&
|
|
923
|
+
!pendingApproval &&
|
|
924
|
+
!pendingFollowup &&
|
|
925
|
+
!pendingHookTrust &&
|
|
926
|
+
!pendingMcpApproval &&
|
|
927
|
+
!mcpPickerOpen &&
|
|
928
|
+
!streamingText && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [queuedMessages.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginBottom: 1, children: [_jsxs(Text, { dimColor: true, bold: true, children: ["Queue (", queuedMessages.length, ")"] }), queuedMessages.slice(0, 5).map((msg, i) => (_jsxs(Text, { dimColor: true, children: [i + 1, ". ", truncateForQueue(msg).replace(/\n/g, "↵")] }, i))), queuedMessages.length > 5 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", queuedMessages.length - 5, " more"] }))] })), _jsx(InputBox, { active: inputActive, slashCommands: SLASH_COMMANDS, onSubmit: handleSubmit }), _jsx(StatusBar, { modelId: settings.model, contextTokens: contextTokens, totalCost: totalCost, state: busy ? busyLabel : "", approvalMode: approvalMode, busy: busy, title: sessionTitle, plan: usage?.plan, usagePercentage: usage?.usagePercentage, tieredUsage: usage?.tieredUsage })] })] }))] }));
|
|
763
929
|
}
|
|
764
930
|
function tail(text, lines) {
|
|
765
931
|
const all = text.split("\n").filter((l) => l.trim());
|
|
@@ -771,6 +937,6 @@ function truncateForQueue(text) {
|
|
|
771
937
|
return text;
|
|
772
938
|
return text.slice(0, QUEUE_PREVIEW_LIMIT - 1) + "…";
|
|
773
939
|
}
|
|
774
|
-
function LoginSection({ onLogin }) {
|
|
940
|
+
function LoginSection({ onLogin, }) {
|
|
775
941
|
return _jsx(LoginView, { onLogin: onLogin });
|
|
776
942
|
}
|