byterover-cli 1.1.0 → 1.2.1
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 +24 -5
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +61 -0
- package/dist/core/domain/cipher/agent-events/types.d.ts +44 -1
- package/dist/core/domain/entities/agent.js +72 -18
- package/dist/core/domain/entities/connector-type.d.ts +2 -1
- package/dist/core/domain/entities/connector-type.js +2 -1
- package/dist/core/interfaces/cipher/i-chat-session.d.ts +3 -1
- package/dist/core/interfaces/connectors/connector-types.d.ts +13 -0
- package/dist/core/interfaces/i-mcp-config-writer.d.ts +40 -0
- package/dist/core/interfaces/i-mcp-config-writer.js +1 -0
- package/dist/core/interfaces/i-rule-template-service.d.ts +4 -2
- package/dist/core/interfaces/transport/i-transport-client.d.ts +7 -0
- package/dist/infra/cipher/agent/cipher-agent.d.ts +8 -0
- package/dist/infra/cipher/agent/cipher-agent.js +16 -0
- package/dist/infra/cipher/llm/context/context-manager.d.ts +8 -0
- package/dist/infra/cipher/llm/context/context-manager.js +16 -0
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +4 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +38 -10
- package/dist/infra/cipher/session/chat-session.d.ts +6 -1
- package/dist/infra/cipher/session/chat-session.js +12 -4
- package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +1 -8
- package/dist/infra/cipher/tools/implementations/curate-tool.js +360 -22
- package/dist/infra/cipher/tools/implementations/task-tool.js +3 -3
- package/dist/infra/connectors/connector-manager.js +2 -0
- package/dist/infra/connectors/mcp/index.d.ts +4 -0
- package/dist/infra/connectors/mcp/index.js +4 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.d.ts +26 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.js +71 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.d.ts +229 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.js +173 -0
- package/dist/infra/connectors/mcp/mcp-connector.d.ts +80 -0
- package/dist/infra/connectors/mcp/mcp-connector.js +324 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.d.ts +45 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.js +134 -0
- package/dist/infra/connectors/rules/rules-connector.d.ts +1 -8
- package/dist/infra/connectors/rules/rules-connector.js +20 -85
- package/dist/infra/connectors/shared/rule-file-manager.d.ts +72 -0
- package/dist/infra/connectors/shared/rule-file-manager.js +119 -0
- package/dist/infra/connectors/shared/template-service.d.ts +10 -1
- package/dist/infra/connectors/shared/template-service.js +53 -16
- package/dist/infra/mcp/index.d.ts +2 -0
- package/dist/infra/mcp/index.js +2 -0
- package/dist/infra/mcp/mcp-server.d.ts +58 -0
- package/dist/infra/mcp/mcp-server.js +178 -0
- package/dist/infra/mcp/tools/brv-curate-tool.d.ts +23 -0
- package/dist/infra/mcp/tools/brv-curate-tool.js +68 -0
- package/dist/infra/mcp/tools/brv-query-tool.d.ts +17 -0
- package/dist/infra/mcp/tools/brv-query-tool.js +68 -0
- package/dist/infra/mcp/tools/index.d.ts +3 -0
- package/dist/infra/mcp/tools/index.js +3 -0
- package/dist/infra/mcp/tools/task-result-waiter.d.ts +30 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +56 -0
- package/dist/infra/process/agent-worker.js +37 -0
- package/dist/infra/repl/commands/curate-command.js +2 -2
- package/dist/infra/transport/socket-io-transport-client.d.ts +16 -0
- package/dist/infra/transport/socket-io-transport-client.js +46 -2
- package/dist/infra/transport/socket-io-transport-server.js +4 -0
- package/dist/infra/usecase/connectors-use-case.d.ts +4 -0
- package/dist/infra/usecase/connectors-use-case.js +29 -10
- package/dist/infra/usecase/init-use-case.js +2 -3
- package/dist/infra/usecase/status-use-case.d.ts +10 -0
- package/dist/infra/usecase/status-use-case.js +53 -0
- package/dist/resources/prompts/curate.yml +107 -4
- package/dist/templates/mcp-base.md +1 -0
- package/dist/templates/sections/command-reference.md +5 -96
- package/dist/templates/sections/mcp-workflow.md +13 -0
- package/dist/templates/sections/workflow.md +21 -16
- package/dist/tui/app.js +4 -1
- package/dist/tui/components/command-details.js +1 -1
- package/dist/tui/components/execution/execution-changes.d.ts +2 -0
- package/dist/tui/components/execution/execution-changes.js +5 -1
- package/dist/tui/components/execution/execution-content.d.ts +2 -0
- package/dist/tui/components/execution/execution-content.js +8 -18
- package/dist/tui/components/execution/execution-input.d.ts +2 -0
- package/dist/tui/components/execution/execution-input.js +6 -4
- package/dist/tui/components/execution/execution-progress.d.ts +2 -0
- package/dist/tui/components/execution/execution-progress.js +6 -2
- package/dist/tui/components/execution/expanded-log-view.d.ts +20 -0
- package/dist/tui/components/execution/expanded-log-view.js +75 -0
- package/dist/tui/components/execution/expanded-message-view.d.ts +24 -0
- package/dist/tui/components/execution/expanded-message-view.js +68 -0
- package/dist/tui/components/execution/index.d.ts +2 -0
- package/dist/tui/components/execution/index.js +2 -0
- package/dist/tui/components/execution/log-item.d.ts +4 -0
- package/dist/tui/components/execution/log-item.js +2 -2
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/index.d.ts +2 -1
- package/dist/tui/components/index.js +2 -1
- package/dist/tui/components/init.js +2 -9
- package/dist/tui/components/logo.js +4 -3
- package/dist/tui/components/markdown.d.ts +13 -0
- package/dist/tui/components/markdown.js +88 -0
- package/dist/tui/components/message-item.js +1 -1
- package/dist/tui/components/onboarding/onboarding-flow.js +1 -1
- package/dist/tui/components/suggestions.js +3 -3
- package/dist/tui/contexts/mode-context.js +6 -2
- package/dist/tui/hooks/index.d.ts +1 -0
- package/dist/tui/hooks/index.js +1 -0
- package/dist/tui/hooks/use-is-latest-version.d.ts +6 -0
- package/dist/tui/hooks/use-is-latest-version.js +22 -0
- package/dist/tui/views/command-view.d.ts +1 -1
- package/dist/tui/views/command-view.js +83 -98
- package/dist/tui/views/logs-view.d.ts +8 -0
- package/dist/tui/views/logs-view.js +55 -27
- package/oclif.manifest.json +26 -1
- package/package.json +9 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createTransportClientFactory } from '../transport/transport-client-factory.js';
|
|
4
|
+
import { registerBrvCurateTool, registerBrvQueryTool } from './tools/index.js';
|
|
5
|
+
/** Reconnection configuration */
|
|
6
|
+
const RECONNECT_DELAY_MS = 1000;
|
|
7
|
+
const RECONNECT_MAX_DELAY_MS = 30_000;
|
|
8
|
+
const RECONNECT_BACKOFF_MULTIPLIER = 1.5;
|
|
9
|
+
/**
|
|
10
|
+
* ByteRover MCP Server.
|
|
11
|
+
*
|
|
12
|
+
* Exposes brv-query and brv-curate as MCP tools for coding agents.
|
|
13
|
+
* Connects to a running brv instance via Socket.IO transport.
|
|
14
|
+
*
|
|
15
|
+
* Architecture:
|
|
16
|
+
* - Coding agent spawns `brv mcp` process
|
|
17
|
+
* - MCP server connects to running brv instance via Socket.IO
|
|
18
|
+
* - MCP tools create tasks via transport
|
|
19
|
+
* - Tasks are executed by the existing agent process
|
|
20
|
+
*/
|
|
21
|
+
export class ByteRoverMcpServer {
|
|
22
|
+
client;
|
|
23
|
+
config;
|
|
24
|
+
currentReconnectDelay = RECONNECT_DELAY_MS;
|
|
25
|
+
isReconnecting = false;
|
|
26
|
+
reconnectTimer;
|
|
27
|
+
server;
|
|
28
|
+
transport;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.server = new McpServer({
|
|
32
|
+
name: 'byterover',
|
|
33
|
+
version: config.version,
|
|
34
|
+
});
|
|
35
|
+
// Register tools with lazy client getter
|
|
36
|
+
// Client will be set when start() is called
|
|
37
|
+
registerBrvQueryTool(this.server, () => this.client, () => this.config.workingDirectory);
|
|
38
|
+
registerBrvCurateTool(this.server, () => this.client, () => this.config.workingDirectory);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Starts the MCP server.
|
|
42
|
+
*
|
|
43
|
+
* 1. Connects to running brv instance via Socket.IO
|
|
44
|
+
* 2. Starts MCP server with stdio transport
|
|
45
|
+
*
|
|
46
|
+
* @throws NoInstanceRunningError - No brv instance is running
|
|
47
|
+
* @throws ConnectionFailedError - Failed to connect to brv instance
|
|
48
|
+
*/
|
|
49
|
+
async start() {
|
|
50
|
+
this.log('Starting MCP server...');
|
|
51
|
+
this.log(`Working directory: ${this.config.workingDirectory}`);
|
|
52
|
+
// Connect to running brv instance
|
|
53
|
+
const factory = createTransportClientFactory();
|
|
54
|
+
this.log('Connecting to brv instance...');
|
|
55
|
+
let connectionResult;
|
|
56
|
+
try {
|
|
57
|
+
connectionResult = await factory.connect(this.config.workingDirectory);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
this.log(`Connection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const { client, projectRoot } = connectionResult;
|
|
64
|
+
this.client = client;
|
|
65
|
+
this.log(`Connected to brv instance at ${projectRoot}`);
|
|
66
|
+
this.log(`Client ID: ${client.getClientId()}`);
|
|
67
|
+
this.log(`Initial connection state: ${client.getState()}`);
|
|
68
|
+
// Monitor connection state changes and handle reconnection
|
|
69
|
+
this.setupStateChangeHandler(client);
|
|
70
|
+
// Start MCP server with stdio transport
|
|
71
|
+
this.transport = new StdioServerTransport();
|
|
72
|
+
await this.server.connect(this.transport);
|
|
73
|
+
this.log('MCP server started and ready for tool calls');
|
|
74
|
+
// Log client state periodically to debug connection issues
|
|
75
|
+
setInterval(() => {
|
|
76
|
+
if (this.client) {
|
|
77
|
+
this.log(`[heartbeat] Client state: ${this.client.getState()}, ID: ${this.client.getClientId()}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.log('[heartbeat] Client is undefined!');
|
|
81
|
+
}
|
|
82
|
+
}, 10_000);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Stops the MCP server.
|
|
86
|
+
*
|
|
87
|
+
* Disconnects from the brv instance.
|
|
88
|
+
*/
|
|
89
|
+
async stop() {
|
|
90
|
+
// Clear any pending reconnection timer
|
|
91
|
+
if (this.reconnectTimer) {
|
|
92
|
+
clearTimeout(this.reconnectTimer);
|
|
93
|
+
this.reconnectTimer = undefined;
|
|
94
|
+
}
|
|
95
|
+
this.isReconnecting = false;
|
|
96
|
+
if (this.client) {
|
|
97
|
+
await this.client.disconnect();
|
|
98
|
+
this.client = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Attempts to reconnect to the brv instance.
|
|
103
|
+
* Uses exponential backoff for retry delays.
|
|
104
|
+
*/
|
|
105
|
+
async attemptReconnect() {
|
|
106
|
+
if (this.isReconnecting) {
|
|
107
|
+
this.log('Reconnection already in progress, skipping...');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.isReconnecting = true;
|
|
111
|
+
this.log(`Attempting to reconnect in ${this.currentReconnectDelay}ms...`);
|
|
112
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const factory = createTransportClientFactory();
|
|
115
|
+
const result = await factory.connect(this.config.workingDirectory);
|
|
116
|
+
// Disconnect old client if it exists
|
|
117
|
+
if (this.client) {
|
|
118
|
+
try {
|
|
119
|
+
await this.client.disconnect();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Ignore disconnect errors on old client
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
this.client = result.client;
|
|
126
|
+
this.log(`Reconnected successfully! Client ID: ${result.client.getClientId()}`);
|
|
127
|
+
// Reset backoff delay on successful connection
|
|
128
|
+
this.currentReconnectDelay = RECONNECT_DELAY_MS;
|
|
129
|
+
this.isReconnecting = false;
|
|
130
|
+
// Set up state change handler for the new client
|
|
131
|
+
this.setupStateChangeHandler(result.client);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
this.log(`Reconnection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
// Increase delay with exponential backoff (capped at max)
|
|
136
|
+
this.currentReconnectDelay = Math.min(this.currentReconnectDelay * RECONNECT_BACKOFF_MULTIPLIER, RECONNECT_MAX_DELAY_MS);
|
|
137
|
+
this.isReconnecting = false;
|
|
138
|
+
// Schedule next reconnection attempt
|
|
139
|
+
this.attemptReconnect();
|
|
140
|
+
}
|
|
141
|
+
}, this.currentReconnectDelay);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Log to stderr (stdout is reserved for MCP protocol).
|
|
145
|
+
*/
|
|
146
|
+
log(msg) {
|
|
147
|
+
process.stderr.write(`[brv-mcp] ${msg}\n`);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sets up the state change handler for a client.
|
|
151
|
+
* Handles disconnection by triggering auto-reconnect.
|
|
152
|
+
*/
|
|
153
|
+
setupStateChangeHandler(client) {
|
|
154
|
+
client.onStateChange((state) => {
|
|
155
|
+
const timestamp = new Date().toISOString();
|
|
156
|
+
this.log(`[${timestamp}] Connection state changed: ${state}`);
|
|
157
|
+
switch (state) {
|
|
158
|
+
case 'connected': {
|
|
159
|
+
this.log(`[${timestamp}] Connected to brv instance.`);
|
|
160
|
+
// Reset backoff delay on successful connection
|
|
161
|
+
this.currentReconnectDelay = RECONNECT_DELAY_MS;
|
|
162
|
+
this.isReconnecting = false;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'disconnected': {
|
|
166
|
+
this.log(`[${timestamp}] Socket disconnected from brv instance. Initiating reconnection...`);
|
|
167
|
+
// Trigger auto-reconnect
|
|
168
|
+
this.attemptReconnect();
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'reconnecting': {
|
|
172
|
+
this.log(`[${timestamp}] Socket.IO attempting to reconnect...`);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ITransportClient } from '../../../core/interfaces/transport/index.js';
|
|
4
|
+
export declare const BrvCurateInputSchema: z.ZodObject<{
|
|
5
|
+
context: z.ZodString;
|
|
6
|
+
files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
context: string;
|
|
9
|
+
files?: string[] | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
context: string;
|
|
12
|
+
files?: string[] | undefined;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Registers the brv-curate tool with the MCP server.
|
|
16
|
+
*
|
|
17
|
+
* This tool allows coding agents to store context to the ByteRover context tree.
|
|
18
|
+
* Use it to save patterns, architectural decisions, error solutions, or insights.
|
|
19
|
+
*
|
|
20
|
+
* Uses fire-and-forget pattern: returns immediately after queueing the task.
|
|
21
|
+
* The curation is processed asynchronously by the ByteRover agent.
|
|
22
|
+
*/
|
|
23
|
+
export declare function registerBrvCurateTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const BrvCurateInputSchema = z.object({
|
|
4
|
+
context: z.string().describe('Knowledge to store: patterns, decisions, errors, or insights about the codebase'),
|
|
5
|
+
files: z
|
|
6
|
+
.array(z.string())
|
|
7
|
+
.max(5)
|
|
8
|
+
.optional()
|
|
9
|
+
.describe('Optional file paths with critical context to include (max 5 files)'),
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Registers the brv-curate tool with the MCP server.
|
|
13
|
+
*
|
|
14
|
+
* This tool allows coding agents to store context to the ByteRover context tree.
|
|
15
|
+
* Use it to save patterns, architectural decisions, error solutions, or insights.
|
|
16
|
+
*
|
|
17
|
+
* Uses fire-and-forget pattern: returns immediately after queueing the task.
|
|
18
|
+
* The curation is processed asynchronously by the ByteRover agent.
|
|
19
|
+
*/
|
|
20
|
+
export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
|
|
21
|
+
server.registerTool('brv-curate', {
|
|
22
|
+
description: 'Store context to the ByteRover context tree. Save patterns, decisions, or insights.',
|
|
23
|
+
inputSchema: BrvCurateInputSchema,
|
|
24
|
+
title: 'ByteRover Curate',
|
|
25
|
+
}, async ({ context, files }) => {
|
|
26
|
+
const client = getClient();
|
|
27
|
+
if (!client) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ text: 'Error: Not connected to ByteRover instance. Run "brv" first.', type: 'text' }],
|
|
30
|
+
isError: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Check connection state before making request
|
|
34
|
+
const state = client.getState();
|
|
35
|
+
if (state !== 'connected') {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ text: `Error: Socket not connected. Current state: ${state}. Ensure "brv" is running.`, type: 'text' }],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const taskId = randomUUID();
|
|
43
|
+
// Create task via transport (same pattern as brv curate command)
|
|
44
|
+
await client.request('task:create', {
|
|
45
|
+
clientCwd: getWorkingDirectory(),
|
|
46
|
+
content: context,
|
|
47
|
+
taskId,
|
|
48
|
+
type: 'curate',
|
|
49
|
+
...(files?.length ? { files } : {}),
|
|
50
|
+
});
|
|
51
|
+
// Fire-and-forget: return immediately after task is queued
|
|
52
|
+
// Curation is processed asynchronously by the ByteRover agent
|
|
53
|
+
return {
|
|
54
|
+
content: [{
|
|
55
|
+
text: `✓ Context queued for curation (taskId: ${taskId}). The curation will be processed asynchronously.`,
|
|
56
|
+
type: 'text',
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ text: `Error: ${message}`, type: 'text' }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ITransportClient } from '../../../core/interfaces/transport/index.js';
|
|
4
|
+
export declare const BrvQueryInputSchema: z.ZodObject<{
|
|
5
|
+
query: z.ZodString;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
query: string;
|
|
8
|
+
}, {
|
|
9
|
+
query: string;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Registers the brv-query tool with the MCP server.
|
|
13
|
+
*
|
|
14
|
+
* This tool allows coding agents to query the ByteRover context tree
|
|
15
|
+
* for patterns, decisions, implementation details, or any stored knowledge.
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerBrvQueryTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { waitForTaskResult } from './task-result-waiter.js';
|
|
4
|
+
export const BrvQueryInputSchema = z.object({
|
|
5
|
+
query: z.string().describe('Natural language question about the codebase or project'),
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* Registers the brv-query tool with the MCP server.
|
|
9
|
+
*
|
|
10
|
+
* This tool allows coding agents to query the ByteRover context tree
|
|
11
|
+
* for patterns, decisions, implementation details, or any stored knowledge.
|
|
12
|
+
*/
|
|
13
|
+
export function registerBrvQueryTool(server, getClient, getWorkingDirectory) {
|
|
14
|
+
server.registerTool('brv-query', {
|
|
15
|
+
description: 'Query the ByteRover context tree for patterns, decisions, or implementation details.',
|
|
16
|
+
inputSchema: BrvQueryInputSchema,
|
|
17
|
+
title: 'ByteRover Query',
|
|
18
|
+
}, async ({ query }) => {
|
|
19
|
+
const timestamp = new Date().toISOString();
|
|
20
|
+
process.stderr.write(`[brv-mcp] [${timestamp}] brv-query tool called with query: ${query.slice(0, 50)}...\n`);
|
|
21
|
+
const client = getClient();
|
|
22
|
+
process.stderr.write(`[brv-mcp] [${timestamp}] Client exists: ${client}\n`);
|
|
23
|
+
if (!client) {
|
|
24
|
+
process.stderr.write(`[brv-mcp] [${timestamp}] ERROR: Client is undefined\n`);
|
|
25
|
+
return {
|
|
26
|
+
content: [{ text: 'Error: Not connected to ByteRover instance. Run "brv" first.', type: 'text' }],
|
|
27
|
+
isError: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Check connection state before making request
|
|
31
|
+
const state = client.getState();
|
|
32
|
+
process.stderr.write(`[brv-mcp] [${timestamp}] Client state: ${state}, Client ID: ${client.getClientId()}\n`);
|
|
33
|
+
if (state !== 'connected') {
|
|
34
|
+
process.stderr.write(`[brv-mcp] [${timestamp}] ERROR: Socket not connected\n`);
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
text: `Error: Socket not connected. Current state: ${state}. Ensure "brv" is running.`,
|
|
39
|
+
type: 'text',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const taskId = randomUUID();
|
|
47
|
+
// Create task via transport (same pattern as brv query command)
|
|
48
|
+
await client.request('task:create', {
|
|
49
|
+
clientCwd: getWorkingDirectory(),
|
|
50
|
+
content: query,
|
|
51
|
+
taskId,
|
|
52
|
+
type: 'query',
|
|
53
|
+
});
|
|
54
|
+
// Wait for task completion and return result
|
|
55
|
+
const result = await waitForTaskResult(client, taskId);
|
|
56
|
+
return {
|
|
57
|
+
content: [{ text: result, type: 'text' }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ text: `Error: ${message}`, type: 'text' }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ITransportClient } from '../../../core/interfaces/transport/index.js';
|
|
2
|
+
export interface TaskCompletedPayload {
|
|
3
|
+
result: string;
|
|
4
|
+
taskId: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TaskErrorPayload {
|
|
7
|
+
error: {
|
|
8
|
+
code?: string;
|
|
9
|
+
details?: Record<string, unknown>;
|
|
10
|
+
message: string;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
taskId: string;
|
|
14
|
+
}
|
|
15
|
+
export interface LlmResponsePayload {
|
|
16
|
+
content: string;
|
|
17
|
+
taskId: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Waits for a task to complete and returns the result.
|
|
21
|
+
*
|
|
22
|
+
* Unlike the fire-and-forget pattern used by `brv curate`, MCP tools need
|
|
23
|
+
* to wait for the task to finish and return the result to the coding agent.
|
|
24
|
+
*
|
|
25
|
+
* This function listens for:
|
|
26
|
+
* - llmservice:response: Captures the LLM's text response
|
|
27
|
+
* - task:completed: Task finished successfully
|
|
28
|
+
* - task:error: Task failed with an error
|
|
29
|
+
*/
|
|
30
|
+
export declare function waitForTaskResult(client: ITransportClient, taskId: string, timeoutMs?: number): Promise<string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waits for a task to complete and returns the result.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the fire-and-forget pattern used by `brv curate`, MCP tools need
|
|
5
|
+
* to wait for the task to finish and return the result to the coding agent.
|
|
6
|
+
*
|
|
7
|
+
* This function listens for:
|
|
8
|
+
* - llmservice:response: Captures the LLM's text response
|
|
9
|
+
* - task:completed: Task finished successfully
|
|
10
|
+
* - task:error: Task failed with an error
|
|
11
|
+
*/
|
|
12
|
+
export async function waitForTaskResult(client, taskId, timeoutMs = 120_000) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let result = '';
|
|
15
|
+
let completed = false;
|
|
16
|
+
const unsubscribers = [];
|
|
17
|
+
const cleanup = () => {
|
|
18
|
+
clearTimeout(timeout);
|
|
19
|
+
for (const unsub of unsubscribers) {
|
|
20
|
+
unsub();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const timeout = setTimeout(() => {
|
|
24
|
+
if (!completed) {
|
|
25
|
+
completed = true;
|
|
26
|
+
cleanup();
|
|
27
|
+
reject(new Error(`Task timeout after ${timeoutMs}ms`));
|
|
28
|
+
}
|
|
29
|
+
}, timeoutMs);
|
|
30
|
+
// Set up all event listeners
|
|
31
|
+
unsubscribers.push(
|
|
32
|
+
// Listen for LLM response content
|
|
33
|
+
client.on('llmservice:response', (payload) => {
|
|
34
|
+
if (payload.taskId === taskId && payload.content) {
|
|
35
|
+
result = payload.content;
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
// Listen for task completion
|
|
39
|
+
client.on('task:completed', (payload) => {
|
|
40
|
+
if (payload.taskId === taskId && !completed) {
|
|
41
|
+
completed = true;
|
|
42
|
+
cleanup();
|
|
43
|
+
// Use the result from the event if available, otherwise use accumulated result
|
|
44
|
+
resolve(payload.result || result);
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
// Listen for task error
|
|
48
|
+
client.on('task:error', (payload) => {
|
|
49
|
+
if (payload.taskId === taskId && !completed) {
|
|
50
|
+
completed = true;
|
|
51
|
+
cleanup();
|
|
52
|
+
reject(new Error(payload.error.message));
|
|
53
|
+
}
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -808,6 +808,43 @@ async function startAgent() {
|
|
|
808
808
|
await transportClient?.request('agent:restarted', { error: message, success: false });
|
|
809
809
|
}
|
|
810
810
|
});
|
|
811
|
+
// Handle agent:newSession from Transport (triggered by /new command)
|
|
812
|
+
transportClient.on('agent:newSession', async (data) => {
|
|
813
|
+
agentLog(`New session requested: ${data.reason ?? 'no reason'}`);
|
|
814
|
+
try {
|
|
815
|
+
if (!cipherAgent) {
|
|
816
|
+
agentLog('Cannot create new session - agent not initialized');
|
|
817
|
+
await transportClient?.request('agent:newSessionCreated', {
|
|
818
|
+
error: 'Agent not initialized',
|
|
819
|
+
success: false,
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
// Generate new session ID
|
|
824
|
+
const newSessionId = `agent-session-${randomUUID()}`;
|
|
825
|
+
// Create new session
|
|
826
|
+
await cipherAgent.createSession(newSessionId);
|
|
827
|
+
// Switch the agent's default session to the new one
|
|
828
|
+
// This ensures execute()/generate()/stream() use the new session
|
|
829
|
+
cipherAgent.switchDefaultSession(newSessionId);
|
|
830
|
+
// Update the local session ID reference
|
|
831
|
+
chatSessionId = newSessionId;
|
|
832
|
+
agentLog(`New session created: ${newSessionId}`);
|
|
833
|
+
// Notify Transport that new session was created
|
|
834
|
+
await transportClient?.request('agent:newSessionCreated', {
|
|
835
|
+
sessionId: newSessionId,
|
|
836
|
+
success: true,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
841
|
+
agentLog(`New session creation error: ${message}`);
|
|
842
|
+
await transportClient?.request('agent:newSessionCreated', {
|
|
843
|
+
error: message,
|
|
844
|
+
success: false,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
});
|
|
811
848
|
agentLog('Ready to process tasks');
|
|
812
849
|
}
|
|
813
850
|
// ============================================================================
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isDevelopment } from '../../../config/environment.js';
|
|
3
3
|
import { CommandKind } from '../../../tui/types.js';
|
|
4
|
-
import { FileGlobalConfigStore } from
|
|
4
|
+
import { FileGlobalConfigStore } from '../../storage/file-global-config-store.js';
|
|
5
5
|
import { createTokenStore } from '../../storage/token-store.js';
|
|
6
6
|
import { ReplTerminal } from '../../terminal/repl-terminal.js';
|
|
7
7
|
import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
|
|
@@ -73,7 +73,7 @@ export const curateCommand = {
|
|
|
73
73
|
{
|
|
74
74
|
char: '@',
|
|
75
75
|
description: 'Include files (type @ to browse, max 5)',
|
|
76
|
-
name: '
|
|
76
|
+
name: 'file',
|
|
77
77
|
type: 'file',
|
|
78
78
|
},
|
|
79
79
|
...(isDevelopment() ? toCommandFlags(devFlags) : []),
|
|
@@ -35,6 +35,13 @@ export declare class SocketIOTransportClient implements ITransportClient {
|
|
|
35
35
|
disconnect(): Promise<void>;
|
|
36
36
|
getClientId(): string | undefined;
|
|
37
37
|
getState(): ConnectionState;
|
|
38
|
+
/**
|
|
39
|
+
* Checks if the socket is actually connected and responsive.
|
|
40
|
+
* Uses Socket.IO's built-in volatile emit with acknowledgement to verify bidirectional communication.
|
|
41
|
+
* @param timeoutMs - Timeout in milliseconds (default: 2000)
|
|
42
|
+
* @returns true if socket is connected and responsive, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
isConnected(timeoutMs?: number): Promise<boolean>;
|
|
38
45
|
joinRoom(room: string): Promise<void>;
|
|
39
46
|
leaveRoom(room: string): Promise<void>;
|
|
40
47
|
on<T = unknown>(event: string, handler: EventHandler<T>): () => void;
|
|
@@ -65,6 +72,15 @@ export declare class SocketIOTransportClient implements ITransportClient {
|
|
|
65
72
|
* Used when initiating new connection attempt - counter preserved for backoff continuity.
|
|
66
73
|
*/
|
|
67
74
|
private clearForceReconnectTimer;
|
|
75
|
+
/**
|
|
76
|
+
* Remove all registered event listeners from socket.
|
|
77
|
+
* Used before re-registering to prevent listener accumulation across reconnects.
|
|
78
|
+
*
|
|
79
|
+
* Why this is needed: Socket.IO preserves the Socket instance across internal reconnects.
|
|
80
|
+
* If we clear registeredSocketEvents without calling socket.off(), the old listeners
|
|
81
|
+
* remain attached, and registerPendingEventHandlers() adds new ones - causing duplicates.
|
|
82
|
+
*/
|
|
83
|
+
private clearSocketEventListeners;
|
|
68
84
|
/**
|
|
69
85
|
* Handle system wake from sleep/hibernate.
|
|
70
86
|
* Re-triggers reconnection if not connected and force reconnect has given up.
|
|
@@ -145,8 +145,10 @@ export class SocketIOTransportClient {
|
|
|
145
145
|
clientLog(`Socket.IO built-in reconnect succeeded after ${attemptNumber} attempts`);
|
|
146
146
|
this.setState('connected');
|
|
147
147
|
// Re-register event handlers after reconnect
|
|
148
|
-
//
|
|
149
|
-
|
|
148
|
+
// FIX: Remove existing socket listeners before re-registering to prevent
|
|
149
|
+
// listener accumulation. Socket.IO preserves the Socket instance across
|
|
150
|
+
// internal reconnects, so old listeners remain attached if not removed.
|
|
151
|
+
this.clearSocketEventListeners();
|
|
150
152
|
this.registerPendingEventHandlers();
|
|
151
153
|
// Auto-rejoin rooms after reconnect
|
|
152
154
|
// Use process.nextTick to ensure socket.connected is true
|
|
@@ -192,6 +194,31 @@ export class SocketIOTransportClient {
|
|
|
192
194
|
getState() {
|
|
193
195
|
return this.state;
|
|
194
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Checks if the socket is actually connected and responsive.
|
|
199
|
+
* Uses Socket.IO's built-in volatile emit with acknowledgement to verify bidirectional communication.
|
|
200
|
+
* @param timeoutMs - Timeout in milliseconds (default: 2000)
|
|
201
|
+
* @returns true if socket is connected and responsive, false otherwise
|
|
202
|
+
*/
|
|
203
|
+
async isConnected(timeoutMs = 2000) {
|
|
204
|
+
const { socket } = this;
|
|
205
|
+
// Quick check: if socket doesn't exist or isn't marked as connected, return false immediately
|
|
206
|
+
if (!socket?.connected) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
// Verify actual bidirectional communication with a ping
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
const timeout = setTimeout(() => {
|
|
212
|
+
resolve(false);
|
|
213
|
+
}, timeoutMs);
|
|
214
|
+
// Use Socket.IO's built-in ping mechanism via volatile emit
|
|
215
|
+
// Volatile means if the message can't be sent, it won't be buffered
|
|
216
|
+
socket.volatile.emit('ping', { timestamp: Date.now() }, () => {
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
resolve(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
195
222
|
async joinRoom(room) {
|
|
196
223
|
const { socket } = this;
|
|
197
224
|
if (!socket?.connected) {
|
|
@@ -383,6 +410,23 @@ export class SocketIOTransportClient {
|
|
|
383
410
|
this.forceReconnectTimer = undefined;
|
|
384
411
|
}
|
|
385
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Remove all registered event listeners from socket.
|
|
415
|
+
* Used before re-registering to prevent listener accumulation across reconnects.
|
|
416
|
+
*
|
|
417
|
+
* Why this is needed: Socket.IO preserves the Socket instance across internal reconnects.
|
|
418
|
+
* If we clear registeredSocketEvents without calling socket.off(), the old listeners
|
|
419
|
+
* remain attached, and registerPendingEventHandlers() adds new ones - causing duplicates.
|
|
420
|
+
*/
|
|
421
|
+
clearSocketEventListeners() {
|
|
422
|
+
const { socket } = this;
|
|
423
|
+
if (!socket)
|
|
424
|
+
return;
|
|
425
|
+
for (const event of this.registeredSocketEvents) {
|
|
426
|
+
socket.off(event);
|
|
427
|
+
}
|
|
428
|
+
this.registeredSocketEvents.clear();
|
|
429
|
+
}
|
|
386
430
|
/**
|
|
387
431
|
* Handle system wake from sleep/hibernate.
|
|
388
432
|
* Re-triggers reconnection if not connected and force reconnect has given up.
|
|
@@ -145,6 +145,10 @@ export class SocketIOTransportServer {
|
|
|
145
145
|
socket.leave(room);
|
|
146
146
|
callback?.({ success: true });
|
|
147
147
|
});
|
|
148
|
+
// Handle ping requests for health checks
|
|
149
|
+
socket.on('ping', (_data, callback) => {
|
|
150
|
+
callback?.({ pong: true, timestamp: Date.now() });
|
|
151
|
+
});
|
|
148
152
|
});
|
|
149
153
|
this.httpServer.on('error', (err) => {
|
|
150
154
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -20,6 +20,10 @@ export declare class ConnectorsUseCase implements IConnectorsUseCase {
|
|
|
20
20
|
private readonly trackingService;
|
|
21
21
|
constructor(options: ConnectorsUseCaseOptions);
|
|
22
22
|
run(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Display manual setup instructions for MCP configuration.
|
|
25
|
+
*/
|
|
26
|
+
private displayManualInstructions;
|
|
23
27
|
/**
|
|
24
28
|
* Gets a description for a connector type.
|
|
25
29
|
*/
|