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.
Files changed (73) hide show
  1. package/README.md +132 -11
  2. package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
  3. package/dist/core/domain/errors/headless-prompt-error.js +18 -0
  4. package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
  5. package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
  6. package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
  7. package/dist/core/interfaces/i-space-service.d.ts +1 -2
  8. package/dist/core/interfaces/i-team-service.d.ts +1 -2
  9. package/dist/core/interfaces/i-user-service.d.ts +1 -2
  10. package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
  11. package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
  12. package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
  13. package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
  14. package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
  15. package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
  16. package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
  17. package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
  18. package/dist/infra/cipher/agent/service-initializer.js +0 -1
  19. package/dist/infra/cipher/http/internal-llm-http-service.d.ts +0 -1
  20. package/dist/infra/cipher/http/internal-llm-http-service.js +1 -2
  21. package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
  22. package/dist/infra/cogit/http-cogit-push-service.js +0 -1
  23. package/dist/infra/http/authenticated-http-client.d.ts +1 -3
  24. package/dist/infra/http/authenticated-http-client.js +1 -5
  25. package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
  26. package/dist/infra/memory/http-memory-storage-service.js +2 -2
  27. package/dist/infra/process/inline-agent-executor.d.ts +32 -0
  28. package/dist/infra/process/inline-agent-executor.js +259 -0
  29. package/dist/infra/space/http-space-service.d.ts +1 -1
  30. package/dist/infra/space/http-space-service.js +2 -2
  31. package/dist/infra/storage/token-store.d.ts +4 -3
  32. package/dist/infra/storage/token-store.js +6 -5
  33. package/dist/infra/team/http-team-service.d.ts +1 -1
  34. package/dist/infra/team/http-team-service.js +2 -2
  35. package/dist/infra/terminal/headless-terminal.d.ts +91 -0
  36. package/dist/infra/terminal/headless-terminal.js +211 -0
  37. package/dist/infra/usecase/curate-use-case.d.ts +40 -1
  38. package/dist/infra/usecase/curate-use-case.js +176 -15
  39. package/dist/infra/usecase/init-use-case.d.ts +27 -5
  40. package/dist/infra/usecase/init-use-case.js +200 -34
  41. package/dist/infra/usecase/login-use-case.d.ts +10 -8
  42. package/dist/infra/usecase/login-use-case.js +35 -2
  43. package/dist/infra/usecase/pull-use-case.d.ts +19 -5
  44. package/dist/infra/usecase/pull-use-case.js +71 -13
  45. package/dist/infra/usecase/push-use-case.d.ts +18 -5
  46. package/dist/infra/usecase/push-use-case.js +81 -14
  47. package/dist/infra/usecase/query-use-case.d.ts +21 -0
  48. package/dist/infra/usecase/query-use-case.js +114 -29
  49. package/dist/infra/usecase/space-list-use-case.js +1 -1
  50. package/dist/infra/usecase/space-switch-use-case.js +2 -2
  51. package/dist/infra/usecase/status-use-case.d.ts +36 -0
  52. package/dist/infra/usecase/status-use-case.js +185 -48
  53. package/dist/infra/user/http-user-service.d.ts +1 -1
  54. package/dist/infra/user/http-user-service.js +2 -2
  55. package/dist/oclif/commands/curate.d.ts +6 -1
  56. package/dist/oclif/commands/curate.js +24 -3
  57. package/dist/oclif/commands/init.d.ts +18 -0
  58. package/dist/oclif/commands/init.js +129 -0
  59. package/dist/oclif/commands/login.d.ts +9 -0
  60. package/dist/oclif/commands/login.js +45 -0
  61. package/dist/oclif/commands/pull.d.ts +16 -0
  62. package/dist/oclif/commands/pull.js +78 -0
  63. package/dist/oclif/commands/push.d.ts +17 -0
  64. package/dist/oclif/commands/push.js +87 -0
  65. package/dist/oclif/commands/query.d.ts +6 -1
  66. package/dist/oclif/commands/query.js +29 -4
  67. package/dist/oclif/commands/status.d.ts +5 -1
  68. package/dist/oclif/commands/status.js +17 -5
  69. package/dist/tui/hooks/use-auth-polling.js +1 -1
  70. package/dist/utils/environment-detector.d.ts +15 -0
  71. package/dist/utils/environment-detector.js +62 -1
  72. package/oclif.manifest.json +287 -5
  73. 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
- this.terminal.log('Query argument is required.');
34
- this.terminal.log('Usage: brv query "your question here"');
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
- const transportClientFactory = this.transportClientFactoryCreator();
42
- if (verbose) {
43
- this.terminal.log('Discovering running instance...');
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
- this.handleConnectionError(error);
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
- reject(new Error('Task timed out after 5 minutes'));
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
- this.terminal.log('\nResult:');
280
- this.terminal.log(payload.content);
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
- this.terminal.log(`🔧 ${payload.toolName}${suffix}`);
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
- this.terminal.log(` ${status} ${resultSummary}`);
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
- // Note: Don't log result here - chunks already streamed it
305
- this.terminal.log(''); // Final newline for clean output
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
- reject(new Error(payload.error.message));
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.accessToken, token.sessionKey, projectConfig.teamId, this.flags.all ? { fetchAll: true } : { limit: this.flags.limit, offset: this.flags.offset });
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.accessToken, token.sessionKey, { fetchAll: true });
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.accessToken, token.sessionKey, selectedTeam.id, {
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
- this.terminal.log(`CLI Version: ${options.cliVersion}`);
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(accessToken: string, sessionKey: string): Promise<User>;
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(accessToken, sessionKey) {
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(accessToken, sessionKey);
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(): ICurateUseCase;
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
- return this.createUseCase().run({ context: args.context, files: flags.files, verbose: flags.verbose });
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
  }