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.
Files changed (107) hide show
  1. package/README.md +24 -5
  2. package/dist/commands/mcp.d.ts +13 -0
  3. package/dist/commands/mcp.js +61 -0
  4. package/dist/core/domain/cipher/agent-events/types.d.ts +44 -1
  5. package/dist/core/domain/entities/agent.js +72 -18
  6. package/dist/core/domain/entities/connector-type.d.ts +2 -1
  7. package/dist/core/domain/entities/connector-type.js +2 -1
  8. package/dist/core/interfaces/cipher/i-chat-session.d.ts +3 -1
  9. package/dist/core/interfaces/connectors/connector-types.d.ts +13 -0
  10. package/dist/core/interfaces/i-mcp-config-writer.d.ts +40 -0
  11. package/dist/core/interfaces/i-mcp-config-writer.js +1 -0
  12. package/dist/core/interfaces/i-rule-template-service.d.ts +4 -2
  13. package/dist/core/interfaces/transport/i-transport-client.d.ts +7 -0
  14. package/dist/infra/cipher/agent/cipher-agent.d.ts +8 -0
  15. package/dist/infra/cipher/agent/cipher-agent.js +16 -0
  16. package/dist/infra/cipher/llm/context/context-manager.d.ts +8 -0
  17. package/dist/infra/cipher/llm/context/context-manager.js +16 -0
  18. package/dist/infra/cipher/llm/internal-llm-service.d.ts +4 -0
  19. package/dist/infra/cipher/llm/internal-llm-service.js +38 -10
  20. package/dist/infra/cipher/session/chat-session.d.ts +6 -1
  21. package/dist/infra/cipher/session/chat-session.js +12 -4
  22. package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +1 -8
  23. package/dist/infra/cipher/tools/implementations/curate-tool.js +360 -22
  24. package/dist/infra/cipher/tools/implementations/task-tool.js +3 -3
  25. package/dist/infra/connectors/connector-manager.js +2 -0
  26. package/dist/infra/connectors/mcp/index.d.ts +4 -0
  27. package/dist/infra/connectors/mcp/index.js +4 -0
  28. package/dist/infra/connectors/mcp/json-mcp-config-writer.d.ts +26 -0
  29. package/dist/infra/connectors/mcp/json-mcp-config-writer.js +71 -0
  30. package/dist/infra/connectors/mcp/mcp-connector-config.d.ts +229 -0
  31. package/dist/infra/connectors/mcp/mcp-connector-config.js +173 -0
  32. package/dist/infra/connectors/mcp/mcp-connector.d.ts +80 -0
  33. package/dist/infra/connectors/mcp/mcp-connector.js +324 -0
  34. package/dist/infra/connectors/mcp/toml-mcp-config-writer.d.ts +45 -0
  35. package/dist/infra/connectors/mcp/toml-mcp-config-writer.js +134 -0
  36. package/dist/infra/connectors/rules/rules-connector.d.ts +1 -8
  37. package/dist/infra/connectors/rules/rules-connector.js +20 -85
  38. package/dist/infra/connectors/shared/rule-file-manager.d.ts +72 -0
  39. package/dist/infra/connectors/shared/rule-file-manager.js +119 -0
  40. package/dist/infra/connectors/shared/template-service.d.ts +10 -1
  41. package/dist/infra/connectors/shared/template-service.js +53 -16
  42. package/dist/infra/mcp/index.d.ts +2 -0
  43. package/dist/infra/mcp/index.js +2 -0
  44. package/dist/infra/mcp/mcp-server.d.ts +58 -0
  45. package/dist/infra/mcp/mcp-server.js +178 -0
  46. package/dist/infra/mcp/tools/brv-curate-tool.d.ts +23 -0
  47. package/dist/infra/mcp/tools/brv-curate-tool.js +68 -0
  48. package/dist/infra/mcp/tools/brv-query-tool.d.ts +17 -0
  49. package/dist/infra/mcp/tools/brv-query-tool.js +68 -0
  50. package/dist/infra/mcp/tools/index.d.ts +3 -0
  51. package/dist/infra/mcp/tools/index.js +3 -0
  52. package/dist/infra/mcp/tools/task-result-waiter.d.ts +30 -0
  53. package/dist/infra/mcp/tools/task-result-waiter.js +56 -0
  54. package/dist/infra/process/agent-worker.js +37 -0
  55. package/dist/infra/repl/commands/curate-command.js +2 -2
  56. package/dist/infra/transport/socket-io-transport-client.d.ts +16 -0
  57. package/dist/infra/transport/socket-io-transport-client.js +46 -2
  58. package/dist/infra/transport/socket-io-transport-server.js +4 -0
  59. package/dist/infra/usecase/connectors-use-case.d.ts +4 -0
  60. package/dist/infra/usecase/connectors-use-case.js +29 -10
  61. package/dist/infra/usecase/init-use-case.js +2 -3
  62. package/dist/infra/usecase/status-use-case.d.ts +10 -0
  63. package/dist/infra/usecase/status-use-case.js +53 -0
  64. package/dist/resources/prompts/curate.yml +107 -4
  65. package/dist/templates/mcp-base.md +1 -0
  66. package/dist/templates/sections/command-reference.md +5 -96
  67. package/dist/templates/sections/mcp-workflow.md +13 -0
  68. package/dist/templates/sections/workflow.md +21 -16
  69. package/dist/tui/app.js +4 -1
  70. package/dist/tui/components/command-details.js +1 -1
  71. package/dist/tui/components/execution/execution-changes.d.ts +2 -0
  72. package/dist/tui/components/execution/execution-changes.js +5 -1
  73. package/dist/tui/components/execution/execution-content.d.ts +2 -0
  74. package/dist/tui/components/execution/execution-content.js +8 -18
  75. package/dist/tui/components/execution/execution-input.d.ts +2 -0
  76. package/dist/tui/components/execution/execution-input.js +6 -4
  77. package/dist/tui/components/execution/execution-progress.d.ts +2 -0
  78. package/dist/tui/components/execution/execution-progress.js +6 -2
  79. package/dist/tui/components/execution/expanded-log-view.d.ts +20 -0
  80. package/dist/tui/components/execution/expanded-log-view.js +75 -0
  81. package/dist/tui/components/execution/expanded-message-view.d.ts +24 -0
  82. package/dist/tui/components/execution/expanded-message-view.js +68 -0
  83. package/dist/tui/components/execution/index.d.ts +2 -0
  84. package/dist/tui/components/execution/index.js +2 -0
  85. package/dist/tui/components/execution/log-item.d.ts +4 -0
  86. package/dist/tui/components/execution/log-item.js +2 -2
  87. package/dist/tui/components/footer.js +1 -1
  88. package/dist/tui/components/index.d.ts +2 -1
  89. package/dist/tui/components/index.js +2 -1
  90. package/dist/tui/components/init.js +2 -9
  91. package/dist/tui/components/logo.js +4 -3
  92. package/dist/tui/components/markdown.d.ts +13 -0
  93. package/dist/tui/components/markdown.js +88 -0
  94. package/dist/tui/components/message-item.js +1 -1
  95. package/dist/tui/components/onboarding/onboarding-flow.js +1 -1
  96. package/dist/tui/components/suggestions.js +3 -3
  97. package/dist/tui/contexts/mode-context.js +6 -2
  98. package/dist/tui/hooks/index.d.ts +1 -0
  99. package/dist/tui/hooks/index.js +1 -0
  100. package/dist/tui/hooks/use-is-latest-version.d.ts +6 -0
  101. package/dist/tui/hooks/use-is-latest-version.js +22 -0
  102. package/dist/tui/views/command-view.d.ts +1 -1
  103. package/dist/tui/views/command-view.js +83 -98
  104. package/dist/tui/views/logs-view.d.ts +8 -0
  105. package/dist/tui/views/logs-view.js +55 -27
  106. package/oclif.manifest.json +26 -1
  107. 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,3 @@
1
+ export { registerBrvCurateTool } from './brv-curate-tool.js';
2
+ export { registerBrvQueryTool } from './brv-query-tool.js';
3
+ export { waitForTaskResult } from './task-result-waiter.js';
@@ -0,0 +1,3 @@
1
+ export { registerBrvCurateTool } from './brv-curate-tool.js';
2
+ export { registerBrvQueryTool } from './brv-query-tool.js';
3
+ export { waitForTaskResult } from './task-result-waiter.js';
@@ -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 "../../storage/file-global-config-store.js";
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: 'files',
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
- // Clear tracking first since socket listeners were reset during reconnect
149
- this.registeredSocketEvents.clear();
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
  */