byterover-cli 1.5.0 → 1.6.0
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 +132 -11
- package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
- package/dist/core/domain/errors/headless-prompt-error.js +18 -0
- package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
- package/dist/core/interfaces/i-space-service.d.ts +1 -2
- package/dist/core/interfaces/i-team-service.d.ts +1 -2
- package/dist/core/interfaces/i-user-service.d.ts +1 -2
- package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
- package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
- package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
- package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
- package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
- package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
- package/dist/infra/cipher/agent/service-initializer.js +0 -1
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +0 -1
- package/dist/infra/cipher/http/internal-llm-http-service.js +1 -2
- package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
- package/dist/infra/cogit/http-cogit-push-service.js +0 -1
- package/dist/infra/http/authenticated-http-client.d.ts +1 -3
- package/dist/infra/http/authenticated-http-client.js +1 -5
- package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
- package/dist/infra/memory/http-memory-storage-service.js +2 -2
- package/dist/infra/process/inline-agent-executor.d.ts +32 -0
- package/dist/infra/process/inline-agent-executor.js +259 -0
- package/dist/infra/space/http-space-service.d.ts +1 -1
- package/dist/infra/space/http-space-service.js +2 -2
- package/dist/infra/storage/token-store.d.ts +4 -3
- package/dist/infra/storage/token-store.js +6 -5
- package/dist/infra/team/http-team-service.d.ts +1 -1
- package/dist/infra/team/http-team-service.js +2 -2
- package/dist/infra/terminal/headless-terminal.d.ts +91 -0
- package/dist/infra/terminal/headless-terminal.js +211 -0
- package/dist/infra/usecase/curate-use-case.d.ts +40 -1
- package/dist/infra/usecase/curate-use-case.js +176 -15
- package/dist/infra/usecase/init-use-case.d.ts +27 -5
- package/dist/infra/usecase/init-use-case.js +200 -34
- package/dist/infra/usecase/login-use-case.d.ts +10 -8
- package/dist/infra/usecase/login-use-case.js +35 -2
- package/dist/infra/usecase/pull-use-case.d.ts +19 -5
- package/dist/infra/usecase/pull-use-case.js +71 -13
- package/dist/infra/usecase/push-use-case.d.ts +18 -5
- package/dist/infra/usecase/push-use-case.js +81 -14
- package/dist/infra/usecase/query-use-case.d.ts +21 -0
- package/dist/infra/usecase/query-use-case.js +114 -29
- package/dist/infra/usecase/space-list-use-case.js +1 -1
- package/dist/infra/usecase/space-switch-use-case.js +2 -2
- package/dist/infra/usecase/status-use-case.d.ts +36 -0
- package/dist/infra/usecase/status-use-case.js +185 -48
- package/dist/infra/user/http-user-service.d.ts +1 -1
- package/dist/infra/user/http-user-service.js +2 -2
- package/dist/oclif/commands/curate.d.ts +6 -1
- package/dist/oclif/commands/curate.js +24 -3
- package/dist/oclif/commands/init.d.ts +18 -0
- package/dist/oclif/commands/init.js +129 -0
- package/dist/oclif/commands/login.d.ts +9 -0
- package/dist/oclif/commands/login.js +45 -0
- package/dist/oclif/commands/pull.d.ts +16 -0
- package/dist/oclif/commands/pull.js +78 -0
- package/dist/oclif/commands/push.d.ts +17 -0
- package/dist/oclif/commands/push.js +87 -0
- package/dist/oclif/commands/query.d.ts +6 -1
- package/dist/oclif/commands/query.js +29 -4
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +17 -5
- package/dist/tui/hooks/use-auth-polling.js +1 -1
- package/dist/utils/environment-detector.d.ts +15 -0
- package/dist/utils/environment-detector.js +62 -1
- package/oclif.manifest.json +287 -5
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ import { ConnectionError, ConnectionFailedError, InstanceCrashedError, NoInstanc
|
|
|
3
3
|
import { formatError } from '../../utils/error-handler.js';
|
|
4
4
|
import { getSandboxEnvironmentName, isSandboxEnvironment, isSandboxNetworkError } from '../../utils/sandbox-detector.js';
|
|
5
5
|
import { CipherAgent } from '../cipher/agent/index.js';
|
|
6
|
+
import { InlineAgent } from '../process/inline-agent-executor.js';
|
|
7
|
+
import { HeadlessTerminal } from '../terminal/headless-terminal.js';
|
|
6
8
|
import { createTransportClientFactory } from '../transport/transport-client-factory.js';
|
|
7
9
|
export class QueryUseCase {
|
|
8
10
|
terminal;
|
|
@@ -29,21 +31,33 @@ export class QueryUseCase {
|
|
|
29
31
|
}
|
|
30
32
|
async run(options) {
|
|
31
33
|
await this.trackingService.track('mem:query', { status: 'started' });
|
|
34
|
+
const format = options.format ?? 'text';
|
|
32
35
|
if (!options.query.trim()) {
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
if (format === 'json') {
|
|
37
|
+
this.outputJsonResult({ error: 'Query argument is required', status: 'error' });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.terminal.log('Query argument is required.');
|
|
41
|
+
this.terminal.log('Usage: brv query "your question here"');
|
|
42
|
+
}
|
|
35
43
|
return;
|
|
36
44
|
}
|
|
37
45
|
const verbose = options.verbose || false;
|
|
38
|
-
// Connect to running instance
|
|
46
|
+
// Connect to running instance or create inline agent
|
|
39
47
|
let client;
|
|
40
48
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
if (options.headless) {
|
|
50
|
+
const inlineAgent = await InlineAgent.create();
|
|
51
|
+
client = inlineAgent.transportClient;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const transportClientFactory = this.transportClientFactoryCreator();
|
|
55
|
+
if (verbose) {
|
|
56
|
+
this.terminal.log('Discovering running instance...');
|
|
57
|
+
}
|
|
58
|
+
const { client: connectedClient } = await transportClientFactory.connect();
|
|
59
|
+
client = connectedClient;
|
|
44
60
|
}
|
|
45
|
-
const { client: connectedClient } = await transportClientFactory.connect();
|
|
46
|
-
client = connectedClient;
|
|
47
61
|
if (verbose) {
|
|
48
62
|
this.terminal.log(`Connected to instance (clientId: ${client.getClientId()})`);
|
|
49
63
|
}
|
|
@@ -60,11 +74,16 @@ export class QueryUseCase {
|
|
|
60
74
|
this.terminal.log(`Task created: ${taskId}`);
|
|
61
75
|
}
|
|
62
76
|
// Wait for task completion with streaming
|
|
63
|
-
await this.streamTaskResults(client, taskId, verbose);
|
|
77
|
+
await this.streamTaskResults(client, taskId, verbose, format);
|
|
64
78
|
await this.trackingService.track('mem:query', { status: 'finished' });
|
|
65
79
|
}
|
|
66
80
|
catch (error) {
|
|
67
|
-
|
|
81
|
+
if (format === 'json') {
|
|
82
|
+
this.handleConnectionErrorJson(error);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.handleConnectionError(error);
|
|
86
|
+
}
|
|
68
87
|
await this.trackingService.track('mem:query', { message: formatError(error), status: 'error' });
|
|
69
88
|
}
|
|
70
89
|
finally {
|
|
@@ -233,19 +252,66 @@ export class QueryUseCase {
|
|
|
233
252
|
const message = error instanceof Error ? error.message : String(error);
|
|
234
253
|
this.terminal.log(`Unexpected error: ${message}`);
|
|
235
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Handle connection errors with JSON output.
|
|
257
|
+
*/
|
|
258
|
+
handleConnectionErrorJson(error) {
|
|
259
|
+
let errorMessage = 'An unexpected error occurred';
|
|
260
|
+
if (error instanceof NoInstanceRunningError) {
|
|
261
|
+
errorMessage = 'No ByteRover instance is running. Start one with: brv';
|
|
262
|
+
}
|
|
263
|
+
else if (error instanceof InstanceCrashedError) {
|
|
264
|
+
errorMessage = 'ByteRover instance has crashed. Please restart with: brv';
|
|
265
|
+
}
|
|
266
|
+
else if (error instanceof ConnectionFailedError) {
|
|
267
|
+
errorMessage = `Failed to connect to ByteRover instance: ${error.message}`;
|
|
268
|
+
}
|
|
269
|
+
else if (error instanceof ConnectionError) {
|
|
270
|
+
errorMessage = `Connection error: ${error.message}`;
|
|
271
|
+
}
|
|
272
|
+
else if (error instanceof Error) {
|
|
273
|
+
errorMessage = error.message;
|
|
274
|
+
}
|
|
275
|
+
this.outputJsonResult({ error: errorMessage, status: 'error' });
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Output JSON result for headless mode.
|
|
279
|
+
*/
|
|
280
|
+
outputJsonResult(result) {
|
|
281
|
+
const response = {
|
|
282
|
+
command: 'query',
|
|
283
|
+
data: result,
|
|
284
|
+
success: result.status !== 'error',
|
|
285
|
+
timestamp: new Date().toISOString(),
|
|
286
|
+
};
|
|
287
|
+
if (this.terminal instanceof HeadlessTerminal) {
|
|
288
|
+
this.terminal.writeFinalResponse(response);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
this.terminal.log(JSON.stringify(response));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
236
294
|
/**
|
|
237
295
|
* Stream task results from the connected instance.
|
|
238
296
|
*/
|
|
239
|
-
async streamTaskResults(client, taskId, verbose) {
|
|
297
|
+
async streamTaskResults(client, taskId, verbose, format = 'text') {
|
|
240
298
|
return new Promise((resolve, reject) => {
|
|
241
299
|
let completed = false;
|
|
242
300
|
let resultPrinted = false; // Track if we've already printed the result
|
|
301
|
+
let finalResult;
|
|
302
|
+
const toolCalls = [];
|
|
243
303
|
// Timeout after 5 minutes
|
|
244
304
|
const timeout = setTimeout(() => {
|
|
245
305
|
if (!completed) {
|
|
246
306
|
completed = true;
|
|
247
307
|
cleanup();
|
|
248
|
-
|
|
308
|
+
if (format === 'json') {
|
|
309
|
+
this.outputJsonResult({ error: 'Task timed out after 5 minutes', status: 'error' });
|
|
310
|
+
resolve();
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
reject(new Error('Task timed out after 5 minutes'));
|
|
314
|
+
}
|
|
249
315
|
}
|
|
250
316
|
}, 5 * 60 * 1000);
|
|
251
317
|
// Setup all event handlers
|
|
@@ -262,22 +328,15 @@ export class QueryUseCase {
|
|
|
262
328
|
this.terminal.log('Task started processing...');
|
|
263
329
|
}
|
|
264
330
|
}),
|
|
265
|
-
// llmservice:chunk - streaming content (for future release)
|
|
266
|
-
// client.on<LlmChunkPayload>('llmservice:chunk', (payload) => {
|
|
267
|
-
// if (payload.taskId === taskId) {
|
|
268
|
-
// if (!hasReceivedChunks) {
|
|
269
|
-
// this.terminal.log('\nResult:')
|
|
270
|
-
// }
|
|
271
|
-
// hasReceivedChunks = true
|
|
272
|
-
// process.stdout.write(payload.content)
|
|
273
|
-
// }
|
|
274
|
-
// }),
|
|
275
331
|
// llmservice:response - final response from LLM (only print once)
|
|
276
332
|
client.on('llmservice:response', (payload) => {
|
|
277
333
|
if (payload.taskId === taskId && payload.content && !resultPrinted) {
|
|
278
334
|
resultPrinted = true;
|
|
279
|
-
|
|
280
|
-
|
|
335
|
+
finalResult = payload.content;
|
|
336
|
+
if (format === 'text') {
|
|
337
|
+
this.terminal.log('\nResult:');
|
|
338
|
+
this.terminal.log(payload.content);
|
|
339
|
+
}
|
|
281
340
|
}
|
|
282
341
|
}),
|
|
283
342
|
// llmservice:toolCall - tool invocation (stop showing after response)
|
|
@@ -285,7 +344,11 @@ export class QueryUseCase {
|
|
|
285
344
|
if (payload.taskId === taskId && !resultPrinted) {
|
|
286
345
|
const detail = payload.args ? this.formatToolArgs(payload.toolName, payload.args) : '';
|
|
287
346
|
const suffix = detail ? `: ${detail}` : '';
|
|
288
|
-
|
|
347
|
+
if (format === 'text') {
|
|
348
|
+
this.terminal.log(`🔧 ${payload.toolName}${suffix}`);
|
|
349
|
+
}
|
|
350
|
+
// Track tool call for JSON output
|
|
351
|
+
toolCalls.push({ status: 'started', summary: suffix, tool: payload.toolName });
|
|
289
352
|
}
|
|
290
353
|
}),
|
|
291
354
|
// llmservice:toolResult - tool result with summary (stop showing after response)
|
|
@@ -293,7 +356,15 @@ export class QueryUseCase {
|
|
|
293
356
|
if (payload.taskId === taskId && !resultPrinted) {
|
|
294
357
|
const status = payload.success ? '✓' : '✗';
|
|
295
358
|
const resultSummary = this.formatToolResult(payload);
|
|
296
|
-
|
|
359
|
+
if (format === 'text') {
|
|
360
|
+
this.terminal.log(` ${status} ${resultSummary}`);
|
|
361
|
+
}
|
|
362
|
+
// Update last tool call with result
|
|
363
|
+
const lastCall = toolCalls.at(-1);
|
|
364
|
+
if (lastCall) {
|
|
365
|
+
lastCall.status = payload.success ? 'success' : 'failed';
|
|
366
|
+
lastCall.summary = resultSummary;
|
|
367
|
+
}
|
|
297
368
|
}
|
|
298
369
|
}),
|
|
299
370
|
// task:completed - task finished (chunks already streamed, just resolve)
|
|
@@ -301,8 +372,16 @@ export class QueryUseCase {
|
|
|
301
372
|
if (payload.taskId === taskId && !completed) {
|
|
302
373
|
completed = true;
|
|
303
374
|
cleanup();
|
|
304
|
-
|
|
305
|
-
|
|
375
|
+
if (format === 'json') {
|
|
376
|
+
this.outputJsonResult({
|
|
377
|
+
result: finalResult,
|
|
378
|
+
status: 'completed',
|
|
379
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
this.terminal.log(''); // Final newline for clean output
|
|
384
|
+
}
|
|
306
385
|
resolve();
|
|
307
386
|
}
|
|
308
387
|
}),
|
|
@@ -311,7 +390,13 @@ export class QueryUseCase {
|
|
|
311
390
|
if (payload.taskId === taskId && !completed) {
|
|
312
391
|
completed = true;
|
|
313
392
|
cleanup();
|
|
314
|
-
|
|
393
|
+
if (format === 'json') {
|
|
394
|
+
this.outputJsonResult({ error: payload.error.message, status: 'error' });
|
|
395
|
+
resolve();
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
reject(new Error(payload.error.message));
|
|
399
|
+
}
|
|
315
400
|
}
|
|
316
401
|
}),
|
|
317
402
|
// Clear timeout when done
|
|
@@ -29,7 +29,7 @@ export class SpaceListUseCase {
|
|
|
29
29
|
}
|
|
30
30
|
// Fetch spaces for the team from project config
|
|
31
31
|
this.terminal.actionStart(`Fetching spaces for ${projectConfig.teamName}`);
|
|
32
|
-
const result = await this.spaceService.getSpaces(token.
|
|
32
|
+
const result = await this.spaceService.getSpaces(token.sessionKey, projectConfig.teamId, this.flags.all ? { fetchAll: true } : { limit: this.flags.limit, offset: this.flags.offset });
|
|
33
33
|
this.terminal.actionStop();
|
|
34
34
|
// Handle empty results
|
|
35
35
|
if (result.spaces.length === 0) {
|
|
@@ -67,7 +67,7 @@ export class SpaceSwitchUseCase {
|
|
|
67
67
|
}
|
|
68
68
|
// Fetch all teams
|
|
69
69
|
this.terminal.actionStart('Fetching all teams');
|
|
70
|
-
const teamResult = await this.teamService.getTeams(token.
|
|
70
|
+
const teamResult = await this.teamService.getTeams(token.sessionKey, { fetchAll: true });
|
|
71
71
|
this.terminal.actionStop();
|
|
72
72
|
if (teamResult.teams.length === 0) {
|
|
73
73
|
this.terminal.log('No teams found. Please create a team in the ByteRover dashboard first.');
|
|
@@ -80,7 +80,7 @@ export class SpaceSwitchUseCase {
|
|
|
80
80
|
return;
|
|
81
81
|
// Fetch spaces for selected team
|
|
82
82
|
this.terminal.actionStart('Fetching all spaces');
|
|
83
|
-
const spaceResult = await this.spaceService.getSpaces(token.
|
|
83
|
+
const spaceResult = await this.spaceService.getSpaces(token.sessionKey, selectedTeam.id, {
|
|
84
84
|
fetchAll: true,
|
|
85
85
|
});
|
|
86
86
|
this.terminal.actionStop();
|
|
@@ -6,6 +6,25 @@ import type { ITokenStore } from '../../core/interfaces/i-token-store.js';
|
|
|
6
6
|
import type { ITrackingService } from '../../core/interfaces/i-tracking-service.js';
|
|
7
7
|
import type { IInstanceDiscovery } from '../../core/interfaces/instance/i-instance-discovery.js';
|
|
8
8
|
import type { IStatusUseCase } from '../../core/interfaces/usecase/i-status-use-case.js';
|
|
9
|
+
/**
|
|
10
|
+
* Structured status data for JSON output.
|
|
11
|
+
*/
|
|
12
|
+
export interface StatusData {
|
|
13
|
+
authStatus: 'expired' | 'logged_in' | 'not_logged_in' | 'unknown';
|
|
14
|
+
cliVersion: string;
|
|
15
|
+
contextTreeChanges?: {
|
|
16
|
+
added: string[];
|
|
17
|
+
deleted: string[];
|
|
18
|
+
modified: string[];
|
|
19
|
+
};
|
|
20
|
+
contextTreeStatus: 'has_changes' | 'no_changes' | 'not_initialized' | 'unknown';
|
|
21
|
+
currentDirectory: string;
|
|
22
|
+
mcpStatus: 'connected' | 'crashed' | 'error' | 'no_instance' | 'not_responsive';
|
|
23
|
+
projectInitialized: boolean;
|
|
24
|
+
spaceName?: string;
|
|
25
|
+
teamName?: string;
|
|
26
|
+
userEmail?: string;
|
|
27
|
+
}
|
|
9
28
|
export interface StatusUseCaseOptions {
|
|
10
29
|
contextTreeService: IContextTreeService;
|
|
11
30
|
contextTreeSnapshotService: IContextTreeSnapshotService;
|
|
@@ -26,6 +45,7 @@ export declare class StatusUseCase implements IStatusUseCase {
|
|
|
26
45
|
constructor(options: StatusUseCaseOptions);
|
|
27
46
|
run(options: {
|
|
28
47
|
cliVersion: string;
|
|
48
|
+
format?: 'json' | 'text';
|
|
29
49
|
}): Promise<void>;
|
|
30
50
|
/**
|
|
31
51
|
* Checks the MCP connection status by:
|
|
@@ -34,4 +54,20 @@ export declare class StatusUseCase implements IStatusUseCase {
|
|
|
34
54
|
* 3. Verifying bidirectional communication with ping
|
|
35
55
|
*/
|
|
36
56
|
private checkMcpStatus;
|
|
57
|
+
/**
|
|
58
|
+
* Check MCP status and return structured data.
|
|
59
|
+
*/
|
|
60
|
+
private checkMcpStatusData;
|
|
61
|
+
/**
|
|
62
|
+
* Collect all status data into a structured object for JSON output.
|
|
63
|
+
*/
|
|
64
|
+
private collectStatusData;
|
|
65
|
+
/**
|
|
66
|
+
* Output status data as JSON.
|
|
67
|
+
*/
|
|
68
|
+
private outputJsonStatus;
|
|
69
|
+
/**
|
|
70
|
+
* Original text format output.
|
|
71
|
+
*/
|
|
72
|
+
private runTextFormat;
|
|
37
73
|
}
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { BRV_DIR, CONTEXT_TREE_DIR } from '../../constants.js';
|
|
4
4
|
import { getErrorMessage } from '../../utils/error-helpers.js';
|
|
5
5
|
import { FileInstanceDiscovery } from '../instance/file-instance-discovery.js';
|
|
6
|
+
import { HeadlessTerminal } from '../terminal/headless-terminal.js';
|
|
6
7
|
import { SocketIOTransportClient } from '../transport/socket-io-transport-client.js';
|
|
7
8
|
export class StatusUseCase {
|
|
8
9
|
contextTreeService;
|
|
@@ -22,7 +23,190 @@ export class StatusUseCase {
|
|
|
22
23
|
this.trackingService = options.trackingService;
|
|
23
24
|
}
|
|
24
25
|
async run(options) {
|
|
25
|
-
|
|
26
|
+
const format = options.format ?? 'text';
|
|
27
|
+
if (format === 'json') {
|
|
28
|
+
const statusData = await this.collectStatusData(options.cliVersion);
|
|
29
|
+
this.outputJsonStatus(statusData);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Text format output (original behavior)
|
|
33
|
+
await this.runTextFormat(options.cliVersion);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Checks the MCP connection status by:
|
|
37
|
+
* 1. Discovering running brv instance
|
|
38
|
+
* 2. Connecting to it via Socket.IO
|
|
39
|
+
* 3. Verifying bidirectional communication with ping
|
|
40
|
+
*/
|
|
41
|
+
async checkMcpStatus() {
|
|
42
|
+
try {
|
|
43
|
+
// Step 1: Discover running instance
|
|
44
|
+
const discoveryResult = await this.instanceDiscovery.discover(process.cwd());
|
|
45
|
+
if (!discoveryResult.found) {
|
|
46
|
+
if (discoveryResult.reason === 'instance_crashed') {
|
|
47
|
+
this.terminal.log(`MCP Status: ${chalk.red('Instance crashed')} (stale instance file found)`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.terminal.log(`MCP Status: ${chalk.yellow('No instance running')}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const { instance, projectRoot } = discoveryResult;
|
|
55
|
+
this.terminal.log(`MCP Status: Instance found (PID: ${instance.pid}, Port: ${instance.port})`);
|
|
56
|
+
// Step 2: Connect to instance
|
|
57
|
+
const client = new SocketIOTransportClient();
|
|
58
|
+
const url = instance.getTransportUrl();
|
|
59
|
+
try {
|
|
60
|
+
await client.connect(url);
|
|
61
|
+
}
|
|
62
|
+
catch (connectError) {
|
|
63
|
+
this.terminal.log(`MCP Status: ${chalk.red('Connection failed')} - ${getErrorMessage(connectError)}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Step 3: Verify bidirectional communication with ping
|
|
67
|
+
const isResponsive = await client.isConnected(2000);
|
|
68
|
+
if (isResponsive) {
|
|
69
|
+
this.terminal.log(`MCP Status: ${chalk.green('Connected and responsive')} (${projectRoot})`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.terminal.log(`MCP Status: ${chalk.yellow('Connected but not responsive')} (ping timeout)`);
|
|
73
|
+
}
|
|
74
|
+
// Clean up
|
|
75
|
+
await client.disconnect();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
this.terminal.log(`MCP Status: ${chalk.red('Error checking status')}`);
|
|
79
|
+
this.terminal.warn(`Warning: ${getErrorMessage(error)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check MCP status and return structured data.
|
|
84
|
+
*/
|
|
85
|
+
async checkMcpStatusData() {
|
|
86
|
+
try {
|
|
87
|
+
const discoveryResult = await this.instanceDiscovery.discover(process.cwd());
|
|
88
|
+
if (!discoveryResult.found) {
|
|
89
|
+
return discoveryResult.reason === 'instance_crashed' ? 'crashed' : 'no_instance';
|
|
90
|
+
}
|
|
91
|
+
const { instance } = discoveryResult;
|
|
92
|
+
const client = new SocketIOTransportClient();
|
|
93
|
+
const url = instance.getTransportUrl();
|
|
94
|
+
try {
|
|
95
|
+
await client.connect(url);
|
|
96
|
+
const isResponsive = await client.isConnected(2000);
|
|
97
|
+
await client.disconnect();
|
|
98
|
+
return isResponsive ? 'connected' : 'not_responsive';
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return 'error';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return 'error';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Collect all status data into a structured object for JSON output.
|
|
110
|
+
*/
|
|
111
|
+
async collectStatusData(cliVersion) {
|
|
112
|
+
const statusData = {
|
|
113
|
+
authStatus: 'unknown',
|
|
114
|
+
cliVersion,
|
|
115
|
+
contextTreeStatus: 'unknown',
|
|
116
|
+
currentDirectory: process.cwd(),
|
|
117
|
+
mcpStatus: 'no_instance',
|
|
118
|
+
projectInitialized: false,
|
|
119
|
+
};
|
|
120
|
+
// Auth status
|
|
121
|
+
try {
|
|
122
|
+
const token = await this.tokenStore.load();
|
|
123
|
+
if (token !== undefined && token.isValid()) {
|
|
124
|
+
statusData.authStatus = 'logged_in';
|
|
125
|
+
statusData.userEmail = token.userEmail;
|
|
126
|
+
}
|
|
127
|
+
else if (token === undefined) {
|
|
128
|
+
statusData.authStatus = 'not_logged_in';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
statusData.authStatus = 'expired';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
statusData.authStatus = 'unknown';
|
|
136
|
+
}
|
|
137
|
+
// Project status
|
|
138
|
+
try {
|
|
139
|
+
const isInitialized = await this.projectConfigStore.exists();
|
|
140
|
+
statusData.projectInitialized = isInitialized;
|
|
141
|
+
if (isInitialized) {
|
|
142
|
+
const config = await this.projectConfigStore.read();
|
|
143
|
+
if (config) {
|
|
144
|
+
statusData.teamName = config.teamName;
|
|
145
|
+
statusData.spaceName = config.spaceName;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
statusData.projectInitialized = false;
|
|
151
|
+
}
|
|
152
|
+
// MCP status
|
|
153
|
+
statusData.mcpStatus = await this.checkMcpStatusData();
|
|
154
|
+
// Context tree status
|
|
155
|
+
try {
|
|
156
|
+
const contextTreeExists = await this.contextTreeService.exists();
|
|
157
|
+
if (contextTreeExists) {
|
|
158
|
+
const hasSnapshot = await this.contextTreeSnapshotService.hasSnapshot();
|
|
159
|
+
if (!hasSnapshot) {
|
|
160
|
+
await this.contextTreeSnapshotService.initEmptySnapshot();
|
|
161
|
+
}
|
|
162
|
+
const changes = await this.contextTreeSnapshotService.getChanges();
|
|
163
|
+
const hasChanges = changes.added.length > 0 || changes.modified.length > 0 || changes.deleted.length > 0;
|
|
164
|
+
if (hasChanges) {
|
|
165
|
+
statusData.contextTreeStatus = 'has_changes';
|
|
166
|
+
statusData.contextTreeChanges = {
|
|
167
|
+
added: changes.added,
|
|
168
|
+
deleted: changes.deleted,
|
|
169
|
+
modified: changes.modified,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
statusData.contextTreeStatus = 'no_changes';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
statusData.contextTreeStatus = 'not_initialized';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
statusData.contextTreeStatus = 'unknown';
|
|
182
|
+
}
|
|
183
|
+
await this.trackingService.track('mem:status');
|
|
184
|
+
return statusData;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Output status data as JSON.
|
|
188
|
+
*/
|
|
189
|
+
outputJsonStatus(statusData) {
|
|
190
|
+
const response = {
|
|
191
|
+
command: 'status',
|
|
192
|
+
data: statusData,
|
|
193
|
+
success: true,
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
};
|
|
196
|
+
// Write directly to stdout for clean JSON output
|
|
197
|
+
if (this.terminal instanceof HeadlessTerminal) {
|
|
198
|
+
this.terminal.writeFinalResponse(response);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Fallback for non-headless terminal
|
|
202
|
+
this.terminal.log(JSON.stringify(response));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Original text format output.
|
|
207
|
+
*/
|
|
208
|
+
async runTextFormat(cliVersion) {
|
|
209
|
+
this.terminal.log(`CLI Version: ${cliVersion}`);
|
|
26
210
|
try {
|
|
27
211
|
const token = await this.tokenStore.load();
|
|
28
212
|
if (token !== undefined && token.isValid()) {
|
|
@@ -100,51 +284,4 @@ export class StatusUseCase {
|
|
|
100
284
|
this.terminal.warn(`Warning: ${error instanceof Error ? error.message : 'Context Tree unable to check status'}`);
|
|
101
285
|
}
|
|
102
286
|
}
|
|
103
|
-
/**
|
|
104
|
-
* Checks the MCP connection status by:
|
|
105
|
-
* 1. Discovering running brv instance
|
|
106
|
-
* 2. Connecting to it via Socket.IO
|
|
107
|
-
* 3. Verifying bidirectional communication with ping
|
|
108
|
-
*/
|
|
109
|
-
async checkMcpStatus() {
|
|
110
|
-
try {
|
|
111
|
-
// Step 1: Discover running instance
|
|
112
|
-
const discoveryResult = await this.instanceDiscovery.discover(process.cwd());
|
|
113
|
-
if (!discoveryResult.found) {
|
|
114
|
-
if (discoveryResult.reason === 'instance_crashed') {
|
|
115
|
-
this.terminal.log(`MCP Status: ${chalk.red('Instance crashed')} (stale instance file found)`);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
this.terminal.log(`MCP Status: ${chalk.yellow('No instance running')}`);
|
|
119
|
-
}
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const { instance, projectRoot } = discoveryResult;
|
|
123
|
-
this.terminal.log(`MCP Status: Instance found (PID: ${instance.pid}, Port: ${instance.port})`);
|
|
124
|
-
// Step 2: Connect to instance
|
|
125
|
-
const client = new SocketIOTransportClient();
|
|
126
|
-
const url = instance.getTransportUrl();
|
|
127
|
-
try {
|
|
128
|
-
await client.connect(url);
|
|
129
|
-
}
|
|
130
|
-
catch (connectError) {
|
|
131
|
-
this.terminal.log(`MCP Status: ${chalk.red('Connection failed')} - ${getErrorMessage(connectError)}`);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
// Step 3: Verify bidirectional communication with ping
|
|
135
|
-
const isResponsive = await client.isConnected(2000);
|
|
136
|
-
if (isResponsive) {
|
|
137
|
-
this.terminal.log(`MCP Status: ${chalk.green('Connected and responsive')} (${projectRoot})`);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
this.terminal.log(`MCP Status: ${chalk.yellow('Connected but not responsive')} (ping timeout)`);
|
|
141
|
-
}
|
|
142
|
-
// Clean up
|
|
143
|
-
await client.disconnect();
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
this.terminal.log(`MCP Status: ${chalk.red('Error checking status')}`);
|
|
147
|
-
this.terminal.warn(`Warning: ${getErrorMessage(error)}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
287
|
}
|
|
@@ -7,6 +7,6 @@ export type UserServiceConfig = {
|
|
|
7
7
|
export declare class HttpUserService implements IUserService {
|
|
8
8
|
private readonly config;
|
|
9
9
|
constructor(config: UserServiceConfig);
|
|
10
|
-
getCurrentUser(
|
|
10
|
+
getCurrentUser(sessionKey: string): Promise<User>;
|
|
11
11
|
private mapToUser;
|
|
12
12
|
}
|
|
@@ -8,9 +8,9 @@ export class HttpUserService {
|
|
|
8
8
|
timeout: config.timeout ?? 10_000, // Default 10 seconds timeout
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
async getCurrentUser(
|
|
11
|
+
async getCurrentUser(sessionKey) {
|
|
12
12
|
// IMPORTANT: Do not try-catch here - let callers handle errors (e.g., distinguish 401 from network errors)
|
|
13
|
-
const httpClient = new AuthenticatedHttpClient(
|
|
13
|
+
const httpClient = new AuthenticatedHttpClient(sessionKey);
|
|
14
14
|
const response = await httpClient.get(`${this.config.apiBaseUrl}/user/me`, {
|
|
15
15
|
timeout: this.config.timeout,
|
|
16
16
|
});
|
|
@@ -9,7 +9,12 @@ export default class Curate extends Command {
|
|
|
9
9
|
static flags: {
|
|
10
10
|
verbose?: import("@oclif/core/interfaces").BooleanFlag<boolean> | undefined;
|
|
11
11
|
files: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
headless: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
14
|
};
|
|
13
|
-
protected createUseCase(
|
|
15
|
+
protected createUseCase(options: {
|
|
16
|
+
format: 'json' | 'text';
|
|
17
|
+
headless: boolean;
|
|
18
|
+
}): ICurateUseCase;
|
|
14
19
|
run(): Promise<void>;
|
|
15
20
|
}
|
|
@@ -2,6 +2,7 @@ import { Args, Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import { isDevelopment } from '../../config/environment.js';
|
|
3
3
|
import { FileGlobalConfigStore } from '../../infra/storage/file-global-config-store.js';
|
|
4
4
|
import { createTokenStore } from '../../infra/storage/token-store.js';
|
|
5
|
+
import { HeadlessTerminal } from '../../infra/terminal/headless-terminal.js';
|
|
5
6
|
import { OclifTerminal } from '../../infra/terminal/oclif-terminal.js';
|
|
6
7
|
import { MixpanelTrackingService } from '../../infra/tracking/mixpanel-tracking-service.js';
|
|
7
8
|
import { CurateUseCase } from '../../infra/usecase/curate-use-case.js';
|
|
@@ -38,6 +39,15 @@ Bad examples:
|
|
|
38
39
|
description: 'Include specific file paths for critical context (max 5 files)',
|
|
39
40
|
multiple: true,
|
|
40
41
|
}),
|
|
42
|
+
format: Flags.string({
|
|
43
|
+
default: 'text',
|
|
44
|
+
description: 'Output format (text or json)',
|
|
45
|
+
options: ['text', 'json'],
|
|
46
|
+
}),
|
|
47
|
+
headless: Flags.boolean({
|
|
48
|
+
default: false,
|
|
49
|
+
description: 'Run in headless mode (no TTY required, suitable for automation)',
|
|
50
|
+
}),
|
|
41
51
|
...(isDevelopment()
|
|
42
52
|
? {
|
|
43
53
|
verbose: Flags.boolean({
|
|
@@ -48,16 +58,27 @@ Bad examples:
|
|
|
48
58
|
}
|
|
49
59
|
: {}),
|
|
50
60
|
};
|
|
51
|
-
createUseCase() {
|
|
61
|
+
createUseCase(options) {
|
|
52
62
|
const tokenStore = createTokenStore();
|
|
53
63
|
const globalConfigStore = new FileGlobalConfigStore();
|
|
54
|
-
const terminal = new OclifTerminal(this);
|
|
55
64
|
const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore });
|
|
65
|
+
// Use HeadlessTerminal for headless mode or JSON format
|
|
66
|
+
const terminal = options.headless || options.format === 'json'
|
|
67
|
+
? new HeadlessTerminal({ failOnPrompt: true, outputFormat: options.format })
|
|
68
|
+
: new OclifTerminal(this);
|
|
56
69
|
return new CurateUseCase({ terminal, trackingService });
|
|
57
70
|
}
|
|
58
71
|
async run() {
|
|
59
72
|
const { args, flags: rawFlags } = await this.parse(Curate);
|
|
60
73
|
const flags = rawFlags;
|
|
61
|
-
|
|
74
|
+
const format = (flags.format ?? 'text');
|
|
75
|
+
const headless = flags.headless ?? false;
|
|
76
|
+
return this.createUseCase({ format, headless }).run({
|
|
77
|
+
context: args.context,
|
|
78
|
+
files: flags.files,
|
|
79
|
+
format,
|
|
80
|
+
headless,
|
|
81
|
+
verbose: flags.verbose,
|
|
82
|
+
});
|
|
62
83
|
}
|
|
63
84
|
}
|