byterover-cli 1.4.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 (174) hide show
  1. package/README.md +193 -12
  2. package/dist/core/domain/cipher/process/types.d.ts +1 -1
  3. package/dist/core/domain/entities/provider-config.d.ts +92 -0
  4. package/dist/core/domain/entities/provider-config.js +181 -0
  5. package/dist/core/domain/entities/provider-registry.d.ts +55 -0
  6. package/dist/core/domain/entities/provider-registry.js +74 -0
  7. package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
  8. package/dist/core/domain/errors/headless-prompt-error.js +18 -0
  9. package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
  10. package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
  11. package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
  12. package/dist/core/interfaces/cipher/message-factory.js +5 -0
  13. package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
  14. package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
  15. package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
  16. package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
  17. package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
  18. package/dist/core/interfaces/i-provider-config-store.js +1 -0
  19. package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
  20. package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
  21. package/dist/core/interfaces/i-space-service.d.ts +1 -2
  22. package/dist/core/interfaces/i-team-service.d.ts +1 -2
  23. package/dist/core/interfaces/i-user-service.d.ts +1 -2
  24. package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
  25. package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
  26. package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
  27. package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
  28. package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
  29. package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
  30. package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
  31. package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
  32. package/dist/infra/cipher/agent/service-initializer.js +0 -1
  33. package/dist/infra/cipher/file-system/file-system-service.js +5 -5
  34. package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -1
  35. package/dist/infra/cipher/http/internal-llm-http-service.js +153 -4
  36. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
  37. package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
  38. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
  39. package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
  40. package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
  41. package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
  42. package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
  43. package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
  44. package/dist/infra/cipher/llm/model-capabilities.js +157 -0
  45. package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
  46. package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
  47. package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
  48. package/dist/infra/cipher/llm/stream-processor.js +78 -4
  49. package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
  50. package/dist/infra/cipher/llm/thought-parser.js +5 -5
  51. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
  52. package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
  53. package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
  54. package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
  55. package/dist/infra/cipher/process/process-service.js +1 -1
  56. package/dist/infra/cipher/session/chat-session.d.ts +2 -0
  57. package/dist/infra/cipher/session/chat-session.js +13 -2
  58. package/dist/infra/cipher/storage/message-storage-service.js +4 -0
  59. package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
  60. package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
  61. package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
  62. package/dist/infra/cogit/http-cogit-push-service.js +0 -1
  63. package/dist/infra/http/authenticated-http-client.d.ts +1 -3
  64. package/dist/infra/http/authenticated-http-client.js +1 -5
  65. package/dist/infra/http/openrouter-api-client.d.ts +148 -0
  66. package/dist/infra/http/openrouter-api-client.js +161 -0
  67. package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
  68. package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
  69. package/dist/infra/memory/http-memory-storage-service.js +2 -2
  70. package/dist/infra/process/agent-worker.js +178 -70
  71. package/dist/infra/process/inline-agent-executor.d.ts +32 -0
  72. package/dist/infra/process/inline-agent-executor.js +259 -0
  73. package/dist/infra/process/transport-handlers.d.ts +25 -4
  74. package/dist/infra/process/transport-handlers.js +57 -10
  75. package/dist/infra/repl/commands/connectors-command.js +2 -2
  76. package/dist/infra/repl/commands/index.js +5 -0
  77. package/dist/infra/repl/commands/model-command.d.ts +13 -0
  78. package/dist/infra/repl/commands/model-command.js +212 -0
  79. package/dist/infra/repl/commands/provider-command.d.ts +13 -0
  80. package/dist/infra/repl/commands/provider-command.js +181 -0
  81. package/dist/infra/repl/transport-client-helper.js +6 -2
  82. package/dist/infra/space/http-space-service.d.ts +1 -1
  83. package/dist/infra/space/http-space-service.js +2 -2
  84. package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
  85. package/dist/infra/storage/file-provider-config-store.js +157 -0
  86. package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
  87. package/dist/infra/storage/provider-keychain-store.js +75 -0
  88. package/dist/infra/storage/token-store.d.ts +4 -3
  89. package/dist/infra/storage/token-store.js +6 -5
  90. package/dist/infra/team/http-team-service.d.ts +1 -1
  91. package/dist/infra/team/http-team-service.js +2 -2
  92. package/dist/infra/terminal/headless-terminal.d.ts +91 -0
  93. package/dist/infra/terminal/headless-terminal.js +211 -0
  94. package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
  95. package/dist/infra/transport/socket-io-transport-client.js +88 -1
  96. package/dist/infra/usecase/curate-use-case.d.ts +40 -1
  97. package/dist/infra/usecase/curate-use-case.js +176 -15
  98. package/dist/infra/usecase/init-use-case.d.ts +27 -5
  99. package/dist/infra/usecase/init-use-case.js +200 -34
  100. package/dist/infra/usecase/login-use-case.d.ts +10 -8
  101. package/dist/infra/usecase/login-use-case.js +35 -2
  102. package/dist/infra/usecase/pull-use-case.d.ts +19 -5
  103. package/dist/infra/usecase/pull-use-case.js +71 -13
  104. package/dist/infra/usecase/push-use-case.d.ts +18 -5
  105. package/dist/infra/usecase/push-use-case.js +81 -14
  106. package/dist/infra/usecase/query-use-case.d.ts +21 -0
  107. package/dist/infra/usecase/query-use-case.js +114 -29
  108. package/dist/infra/usecase/space-list-use-case.js +1 -1
  109. package/dist/infra/usecase/space-switch-use-case.js +2 -2
  110. package/dist/infra/usecase/status-use-case.d.ts +36 -0
  111. package/dist/infra/usecase/status-use-case.js +185 -48
  112. package/dist/infra/user/http-user-service.d.ts +1 -1
  113. package/dist/infra/user/http-user-service.js +2 -2
  114. package/dist/oclif/commands/curate.d.ts +6 -1
  115. package/dist/oclif/commands/curate.js +24 -3
  116. package/dist/oclif/commands/init.d.ts +18 -0
  117. package/dist/oclif/commands/init.js +129 -0
  118. package/dist/oclif/commands/login.d.ts +9 -0
  119. package/dist/oclif/commands/login.js +45 -0
  120. package/dist/oclif/commands/pull.d.ts +16 -0
  121. package/dist/oclif/commands/pull.js +78 -0
  122. package/dist/oclif/commands/push.d.ts +17 -0
  123. package/dist/oclif/commands/push.js +87 -0
  124. package/dist/oclif/commands/query.d.ts +6 -1
  125. package/dist/oclif/commands/query.js +29 -4
  126. package/dist/oclif/commands/status.d.ts +5 -1
  127. package/dist/oclif/commands/status.js +17 -5
  128. package/dist/resources/tools/bash_exec.txt +1 -1
  129. package/dist/tui/components/api-key-dialog.d.ts +39 -0
  130. package/dist/tui/components/api-key-dialog.js +94 -0
  131. package/dist/tui/components/execution/execution-changes.d.ts +3 -1
  132. package/dist/tui/components/execution/execution-changes.js +4 -4
  133. package/dist/tui/components/execution/execution-content.d.ts +1 -1
  134. package/dist/tui/components/execution/execution-content.js +4 -12
  135. package/dist/tui/components/execution/execution-input.js +1 -1
  136. package/dist/tui/components/execution/execution-progress.d.ts +10 -13
  137. package/dist/tui/components/execution/execution-progress.js +70 -17
  138. package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
  139. package/dist/tui/components/execution/execution-reasoning.js +34 -0
  140. package/dist/tui/components/execution/execution-tool.d.ts +23 -0
  141. package/dist/tui/components/execution/execution-tool.js +125 -0
  142. package/dist/tui/components/execution/expanded-log-view.js +3 -3
  143. package/dist/tui/components/execution/log-item.d.ts +2 -0
  144. package/dist/tui/components/execution/log-item.js +6 -4
  145. package/dist/tui/components/index.d.ts +2 -0
  146. package/dist/tui/components/index.js +2 -0
  147. package/dist/tui/components/inline-prompts/inline-select.js +3 -2
  148. package/dist/tui/components/model-dialog.d.ts +63 -0
  149. package/dist/tui/components/model-dialog.js +89 -0
  150. package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
  151. package/dist/tui/components/provider-dialog.d.ts +27 -0
  152. package/dist/tui/components/provider-dialog.js +31 -0
  153. package/dist/tui/components/reasoning-text.d.ts +26 -0
  154. package/dist/tui/components/reasoning-text.js +49 -0
  155. package/dist/tui/components/selectable-list.d.ts +54 -0
  156. package/dist/tui/components/selectable-list.js +180 -0
  157. package/dist/tui/components/streaming-text.d.ts +30 -0
  158. package/dist/tui/components/streaming-text.js +52 -0
  159. package/dist/tui/contexts/tasks-context.d.ts +15 -0
  160. package/dist/tui/contexts/tasks-context.js +224 -40
  161. package/dist/tui/contexts/theme-context.d.ts +1 -0
  162. package/dist/tui/contexts/theme-context.js +3 -2
  163. package/dist/tui/hooks/use-activity-logs.js +7 -1
  164. package/dist/tui/hooks/use-auth-polling.js +1 -1
  165. package/dist/tui/types/messages.d.ts +32 -5
  166. package/dist/tui/utils/index.d.ts +1 -1
  167. package/dist/tui/utils/index.js +1 -1
  168. package/dist/tui/utils/log.d.ts +0 -9
  169. package/dist/tui/utils/log.js +2 -53
  170. package/dist/tui/views/command-view.js +4 -1
  171. package/dist/utils/environment-detector.d.ts +15 -0
  172. package/dist/utils/environment-detector.js +62 -1
  173. package/oclif.manifest.json +287 -5
  174. package/package.json +1 -1
@@ -1,7 +1,31 @@
1
+ import { z } from 'zod';
1
2
  import type { ITerminal } from '../../core/interfaces/i-terminal.js';
2
3
  import type { CurateUseCaseRunOptions, ICurateUseCase } from '../../core/interfaces/usecase/i-curate-use-case.js';
3
4
  import { ITrackingService } from '../../core/interfaces/i-tracking-service.js';
4
5
  import { type TransportClientFactory } from '../transport/transport-client-factory.js';
6
+ declare const CurateOperationSchema: z.ZodObject<{
7
+ filePath: z.ZodString;
8
+ path: z.ZodString;
9
+ status: z.ZodEnum<["success", "failed"]>;
10
+ type: z.ZodEnum<["ADD", "UPDATE", "MERGE", "DELETE"]>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ path: string;
13
+ type: "DELETE" | "ADD" | "UPDATE" | "MERGE";
14
+ status: "failed" | "success";
15
+ filePath: string;
16
+ }, {
17
+ path: string;
18
+ type: "DELETE" | "ADD" | "UPDATE" | "MERGE";
19
+ status: "failed" | "success";
20
+ filePath: string;
21
+ }>;
22
+ type CurateOperation = z.infer<typeof CurateOperationSchema>;
23
+ export interface CurateResult {
24
+ message: string;
25
+ operations?: CurateOperation[];
26
+ status: 'completed' | 'error' | 'queued';
27
+ taskId?: string;
28
+ }
5
29
  export type TransportClientFactoryCreator = () => TransportClientFactory;
6
30
  export interface CurateUseCaseOptions {
7
31
  terminal: ITerminal;
@@ -14,6 +38,21 @@ export declare class CurateUseCase implements ICurateUseCase {
14
38
  private readonly trackingService;
15
39
  private readonly transportClientFactoryCreator;
16
40
  constructor(options: CurateUseCaseOptions);
17
- run({ context, files, verbose }: CurateUseCaseRunOptions): Promise<void>;
41
+ run({ context, files, format, headless, verbose, }: CurateUseCaseRunOptions): Promise<void>;
42
+ private createClient;
18
43
  private handleConnectionError;
44
+ /**
45
+ * Handle connection errors with JSON output.
46
+ */
47
+ private handleConnectionErrorJson;
48
+ /**
49
+ * Output JSON result for headless mode.
50
+ */
51
+ private outputJsonResult;
52
+ /**
53
+ * Wait for task completion in headless mode.
54
+ * Listens for task:completed or task:error events before returning.
55
+ */
56
+ private waitForTaskCompletion;
19
57
  }
58
+ export {};
@@ -1,8 +1,25 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { ConnectionError, ConnectionFailedError, InstanceCrashedError, NoInstanceRunningError } from '../../core/domain/errors/connection-error.js';
2
+ import { z } from 'zod';
3
+ import { ConnectionError, ConnectionFailedError, InstanceCrashedError, NoInstanceRunningError, } from '../../core/domain/errors/connection-error.js';
3
4
  import { formatError } from '../../utils/error-handler.js';
4
5
  import { getSandboxEnvironmentName, isSandboxEnvironment, isSandboxNetworkError } from '../../utils/sandbox-detector.js';
6
+ import { ToolName } from '../cipher/tools/index.js';
7
+ import { InlineAgent } from '../process/inline-agent-executor.js';
8
+ import { HeadlessTerminal } from '../terminal/headless-terminal.js';
5
9
  import { createTransportClientFactory } from '../transport/transport-client-factory.js';
10
+ const CurateOperationSchema = z.object({
11
+ filePath: z.string(),
12
+ path: z.string(),
13
+ status: z.enum(['success', 'failed']),
14
+ type: z.enum(['ADD', 'UPDATE', 'MERGE', 'DELETE']),
15
+ });
16
+ const CurateResultSchema = z.object({
17
+ result: z
18
+ .object({
19
+ applied: z.array(CurateOperationSchema).optional(),
20
+ })
21
+ .optional(),
22
+ });
6
23
  export class CurateUseCase {
7
24
  terminal;
8
25
  trackingService;
@@ -12,27 +29,27 @@ export class CurateUseCase {
12
29
  this.trackingService = options.trackingService;
13
30
  this.transportClientFactoryCreator = options.transportClientFactoryCreator ?? createTransportClientFactory;
14
31
  }
15
- async run({ context, files, verbose = false }) {
32
+ async run({ context, files, format = 'text', headless = false, verbose = false, }) {
16
33
  await this.trackingService.track('mem:curate', { status: 'started' });
17
34
  const hasContext = Boolean(context?.trim());
18
35
  const hasFiles = Boolean(files?.length);
19
36
  if (!hasContext && !hasFiles) {
20
- this.terminal.log('Either a context argument or file reference is required.');
21
- this.terminal.log('Usage:');
22
- this.terminal.log(' brv curate "your context here"');
23
- this.terminal.log(' brv curate @src/file.ts');
24
- this.terminal.log(' brv curate "context with files" @src/file.ts');
37
+ if (format === 'json') {
38
+ this.outputJsonResult({ message: 'Either a context argument or file reference is required.', status: 'error' });
39
+ }
40
+ else {
41
+ this.terminal.log('Either a context argument or file reference is required.');
42
+ this.terminal.log('Usage:');
43
+ this.terminal.log(' brv curate "your context here"');
44
+ this.terminal.log(' brv curate @src/file.ts');
45
+ this.terminal.log(' brv curate "context with files" @src/file.ts');
46
+ }
25
47
  return;
26
48
  }
27
49
  const resolvedContent = context?.trim() ? context : '';
28
50
  let client;
29
51
  try {
30
- const transportClientFactory = this.transportClientFactoryCreator();
31
- if (verbose) {
32
- this.terminal.log('Discovering running instance...');
33
- }
34
- const { client: connectedClient } = await transportClientFactory.connect();
35
- client = connectedClient;
52
+ client = await this.createClient({ headless, verbose });
36
53
  if (verbose) {
37
54
  this.terminal.log(`Connected to instance (clientId: ${client.getClientId()})`);
38
55
  }
@@ -45,11 +62,25 @@ export class CurateUseCase {
45
62
  taskId,
46
63
  type: 'curate',
47
64
  });
48
- this.terminal.log('✓ Context queued for processing.');
65
+ if (headless) {
66
+ // In headless mode, wait for the in-process task to complete
67
+ await this.waitForTaskCompletion(client, taskId, format);
68
+ }
69
+ else if (format === 'json') {
70
+ this.outputJsonResult({ message: 'Context queued for processing', status: 'queued', taskId });
71
+ }
72
+ else {
73
+ this.terminal.log('✓ Context queued for processing.');
74
+ }
49
75
  await this.trackingService.track('mem:curate', { status: 'finished' });
50
76
  }
51
77
  catch (error) {
52
- this.handleConnectionError(error);
78
+ if (format === 'json') {
79
+ this.handleConnectionErrorJson(error);
80
+ }
81
+ else {
82
+ this.handleConnectionError(error);
83
+ }
53
84
  await this.trackingService.track('mem:curate', { message: formatError(error), status: 'error' });
54
85
  }
55
86
  finally {
@@ -58,6 +89,18 @@ export class CurateUseCase {
58
89
  }
59
90
  }
60
91
  }
92
+ async createClient(options) {
93
+ if (options.headless) {
94
+ const inlineAgent = await InlineAgent.create();
95
+ return inlineAgent.transportClient;
96
+ }
97
+ const transportClientFactory = this.transportClientFactoryCreator();
98
+ if (options.verbose) {
99
+ this.terminal.log('Discovering running instance...');
100
+ }
101
+ const { client: connectedClient } = await transportClientFactory.connect();
102
+ return connectedClient;
103
+ }
61
104
  handleConnectionError(error) {
62
105
  if (error instanceof NoInstanceRunningError) {
63
106
  // Check if running in sandbox environment
@@ -100,4 +143,122 @@ export class CurateUseCase {
100
143
  const message = error instanceof Error ? error.message : String(error);
101
144
  this.terminal.log(`Unexpected error: ${message}`);
102
145
  }
146
+ /**
147
+ * Handle connection errors with JSON output.
148
+ */
149
+ handleConnectionErrorJson(error) {
150
+ let errorMessage = 'An unexpected error occurred';
151
+ if (error instanceof NoInstanceRunningError) {
152
+ errorMessage = 'No ByteRover instance is running. Start one with: brv';
153
+ }
154
+ else if (error instanceof InstanceCrashedError) {
155
+ errorMessage = 'ByteRover instance has crashed. Please restart with: brv';
156
+ }
157
+ else if (error instanceof ConnectionFailedError) {
158
+ errorMessage = `Failed to connect to ByteRover instance: ${error.message}`;
159
+ }
160
+ else if (error instanceof ConnectionError) {
161
+ errorMessage = `Connection error: ${error.message}`;
162
+ }
163
+ else if (error instanceof Error) {
164
+ errorMessage = error.message;
165
+ }
166
+ this.outputJsonResult({ message: errorMessage, status: 'error' });
167
+ }
168
+ /**
169
+ * Output JSON result for headless mode.
170
+ */
171
+ outputJsonResult(result) {
172
+ const response = {
173
+ command: 'curate',
174
+ data: result,
175
+ success: result.status !== 'error',
176
+ timestamp: new Date().toISOString(),
177
+ };
178
+ if (this.terminal instanceof HeadlessTerminal) {
179
+ this.terminal.writeFinalResponse(response);
180
+ }
181
+ else {
182
+ this.terminal.log(JSON.stringify(response));
183
+ }
184
+ }
185
+ /**
186
+ * Wait for task completion in headless mode.
187
+ * Listens for task:completed or task:error events before returning.
188
+ */
189
+ async waitForTaskCompletion(client, taskId, format) {
190
+ return new Promise((resolve, reject) => {
191
+ let completed = false;
192
+ const operations = [];
193
+ const timeout = setTimeout(() => {
194
+ if (!completed) {
195
+ completed = true;
196
+ cleanup();
197
+ if (format === 'json') {
198
+ this.outputJsonResult({ message: 'Task timed out after 5 minutes', status: 'error' });
199
+ resolve();
200
+ }
201
+ else {
202
+ reject(new Error('Task timed out after 5 minutes'));
203
+ }
204
+ }
205
+ }, 5 * 60 * 1000);
206
+ const unsubscribers = [
207
+ client.on('llmservice:toolResult', (payload) => {
208
+ if (payload.success && payload.toolName === ToolName.CURATE && payload.result) {
209
+ try {
210
+ const parsed = CurateResultSchema.parse(JSON.parse(payload.result));
211
+ for (const op of parsed.result?.applied ?? []) {
212
+ if (op.status === 'success') {
213
+ operations.push(op);
214
+ }
215
+ }
216
+ }
217
+ catch {
218
+ // Ignore parse errors
219
+ }
220
+ }
221
+ }),
222
+ client.on('task:completed', (payload) => {
223
+ if (payload.taskId === taskId && !completed) {
224
+ completed = true;
225
+ cleanup();
226
+ if (format === 'json') {
227
+ this.outputJsonResult({
228
+ message: 'Context curated successfully',
229
+ operations: operations.length > 0 ? operations : undefined,
230
+ status: 'completed',
231
+ taskId,
232
+ });
233
+ }
234
+ else {
235
+ for (const op of operations) {
236
+ this.terminal.log(` ${op.type.toLowerCase()} ${op.filePath}`);
237
+ }
238
+ this.terminal.log('✓ Context curated successfully.');
239
+ }
240
+ resolve();
241
+ }
242
+ }),
243
+ client.on('task:error', (payload) => {
244
+ if (payload.taskId === taskId && !completed) {
245
+ completed = true;
246
+ cleanup();
247
+ if (format === 'json') {
248
+ this.outputJsonResult({ message: payload.error.message, status: 'error' });
249
+ resolve();
250
+ }
251
+ else {
252
+ reject(new Error(payload.error.message));
253
+ }
254
+ }
255
+ }),
256
+ () => clearTimeout(timeout),
257
+ ];
258
+ const cleanup = () => {
259
+ for (const unsub of unsubscribers)
260
+ unsub();
261
+ };
262
+ });
263
+ }
103
264
  }
@@ -13,9 +13,19 @@ import type { ITeamService } from '../../core/interfaces/i-team-service.js';
13
13
  import type { ITerminal } from '../../core/interfaces/i-terminal.js';
14
14
  import type { ITokenStore } from '../../core/interfaces/i-token-store.js';
15
15
  import type { ITrackingService } from '../../core/interfaces/i-tracking-service.js';
16
- import type { IInitUseCase } from '../../core/interfaces/usecase/i-init-use-case.js';
16
+ import type { IInitUseCase, InitUseCaseRunOptions } from '../../core/interfaces/usecase/i-init-use-case.js';
17
17
  import { type Agent } from '../../core/domain/entities/agent.js';
18
18
  import { BrvConfig } from '../../core/domain/entities/brv-config.js';
19
+ /**
20
+ * Structured init result for JSON output.
21
+ */
22
+ export interface InitResult {
23
+ configPath?: string;
24
+ error?: string;
25
+ spaceName?: string;
26
+ status: 'cancelled' | 'error' | 'success';
27
+ teamName?: string;
28
+ }
19
29
  /**
20
30
  * Represents a legacy config that exists but has version issues.
21
31
  * Used to display config info during re-initialization prompt.
@@ -64,9 +74,19 @@ export declare class InitUseCase implements IInitUseCase {
64
74
  chatLogPath: string;
65
75
  cwd: string;
66
76
  };
67
- protected ensureAuthenticated(): Promise<AuthToken | undefined>;
77
+ protected ensureAuthenticated(format: 'json' | 'text'): Promise<AuthToken | undefined>;
68
78
  protected fetchAndSelectSpace(token: AuthToken, team: Team): Promise<Space | undefined>;
69
79
  protected fetchAndSelectTeam(token: AuthToken): Promise<Team | undefined>;
80
+ /**
81
+ * Fetch space by ID or name for headless mode.
82
+ * First tries to match by ID, then by name (case-insensitive).
83
+ */
84
+ protected fetchSpaceById(token: AuthToken, team: Team, spaceIdOrName: string, format: 'json' | 'text'): Promise<Space | undefined>;
85
+ /**
86
+ * Fetch team by ID or name for headless mode.
87
+ * First tries to match by ID, then by name (case-insensitive).
88
+ */
89
+ protected fetchTeamById(token: AuthToken, teamIdOrName: string, format: 'json' | 'text'): Promise<Team | undefined>;
70
90
  protected getExistingConfig(): Promise<BrvConfig | LegacyProjectConfigInfo | undefined>;
71
91
  protected initializeMemoryContextDir(name: string, initFn: () => Promise<string>): Promise<void>;
72
92
  /**
@@ -90,9 +110,7 @@ export declare class InitUseCase implements IInitUseCase {
90
110
  protected promptForSpaceSelection(spaces: Space[]): Promise<Space | undefined>;
91
111
  protected promptForTeamSelection(teams: Team[]): Promise<Team | undefined>;
92
112
  protected removeAceDirectory(baseDir?: string): Promise<void>;
93
- run(options: {
94
- force: boolean;
95
- }): Promise<void>;
113
+ run(options: InitUseCaseRunOptions): Promise<void>;
96
114
  protected syncFromRemoteOrInitialize(params: {
97
115
  projectConfig: {
98
116
  spaceId: string;
@@ -101,4 +119,8 @@ export declare class InitUseCase implements IInitUseCase {
101
119
  token: AuthToken;
102
120
  }): Promise<void>;
103
121
  private logSuccess;
122
+ /**
123
+ * Output JSON result for headless mode.
124
+ */
125
+ private outputJsonResult;
104
126
  }
@@ -5,6 +5,7 @@ import { ACE_DIR, BRV_CONFIG_VERSION, BRV_DIR, DEFAULT_BRANCH, PROJECT_CONFIG_FI
5
5
  import { AGENT_VALUES } from '../../core/domain/entities/agent.js';
6
6
  import { BrvConfig } from '../../core/domain/entities/brv-config.js';
7
7
  import { BrvConfigVersionError } from '../../core/domain/errors/brv-config-version-error.js';
8
+ import { HeadlessTerminal } from '../terminal/headless-terminal.js';
8
9
  import { WorkspaceDetectorService } from '../workspace/workspace-detector-service.js';
9
10
  export class InitUseCase {
10
11
  cogitPullService;
@@ -88,21 +89,31 @@ export class InitUseCase {
88
89
  cwd: result.cwd,
89
90
  };
90
91
  }
91
- async ensureAuthenticated() {
92
+ async ensureAuthenticated(format) {
92
93
  const token = await this.tokenStore.load();
93
94
  if (token === undefined) {
94
- this.terminal.log('Not authenticated. Please run "/login" first.');
95
+ if (format === 'json') {
96
+ this.outputJsonResult({ error: 'Not authenticated. Run login first.', status: 'error' });
97
+ }
98
+ else {
99
+ this.terminal.log('Not authenticated. Please run "/login" first.');
100
+ }
95
101
  return undefined;
96
102
  }
97
103
  if (!token.isValid()) {
98
- this.terminal.log('Authentication token expired. Please run "/login" again.');
104
+ if (format === 'json') {
105
+ this.outputJsonResult({ error: 'Authentication token expired. Run login again.', status: 'error' });
106
+ }
107
+ else {
108
+ this.terminal.log('Authentication token expired. Please run "/login" again.');
109
+ }
99
110
  return undefined;
100
111
  }
101
112
  return token;
102
113
  }
103
114
  async fetchAndSelectSpace(token, team) {
104
115
  this.terminal.actionStart('Fetching all spaces');
105
- const { spaces } = await this.spaceService.getSpaces(token.accessToken, token.sessionKey, team.id, { fetchAll: true });
116
+ const { spaces } = await this.spaceService.getSpaces(token.sessionKey, team.id, { fetchAll: true });
106
117
  this.terminal.actionStop();
107
118
  if (spaces.length === 0) {
108
119
  this.terminal.error(`No spaces found in team "${team.getDisplayName()}"\nPlease visit ${getCurrentConfig().webAppUrl} to create your first space for ${team.getDisplayName()}.`);
@@ -113,7 +124,7 @@ export class InitUseCase {
113
124
  }
114
125
  async fetchAndSelectTeam(token) {
115
126
  this.terminal.actionStart('Fetching all teams');
116
- const { teams } = await this.teamService.getTeams(token.accessToken, token.sessionKey, { fetchAll: true });
127
+ const { teams } = await this.teamService.getTeams(token.sessionKey, { fetchAll: true });
117
128
  this.terminal.actionStop();
118
129
  if (teams.length === 0) {
119
130
  this.terminal.error(`No teams found.\nPlease visit ${getCurrentConfig().webAppUrl} to create your first team.`);
@@ -122,6 +133,82 @@ export class InitUseCase {
122
133
  this.terminal.log();
123
134
  return this.promptForTeamSelection(teams);
124
135
  }
136
+ /**
137
+ * Fetch space by ID or name for headless mode.
138
+ * First tries to match by ID, then by name (case-insensitive).
139
+ */
140
+ async fetchSpaceById(token, team, spaceIdOrName, format) {
141
+ this.terminal.actionStart('Fetching space');
142
+ try {
143
+ const { spaces } = await this.spaceService.getSpaces(token.sessionKey, team.id, { fetchAll: true });
144
+ this.terminal.actionStop();
145
+ // First try to find by ID
146
+ let space = spaces.find((s) => s.id === spaceIdOrName);
147
+ // If not found by ID, try to find by name (case-insensitive)
148
+ if (!space) {
149
+ space = spaces.find((s) => s.name.toLowerCase() === spaceIdOrName.toLowerCase());
150
+ }
151
+ if (!space) {
152
+ if (format === 'json') {
153
+ this.outputJsonResult({ error: `Space "${spaceIdOrName}" not found in team "${team.name}"`, status: 'error' });
154
+ }
155
+ else {
156
+ this.terminal.error(`Space "${spaceIdOrName}" not found in team "${team.name}"`);
157
+ }
158
+ return undefined;
159
+ }
160
+ return space;
161
+ }
162
+ catch (error) {
163
+ this.terminal.actionStop();
164
+ const message = error instanceof Error ? error.message : 'Failed to fetch space';
165
+ if (format === 'json') {
166
+ this.outputJsonResult({ error: message, status: 'error' });
167
+ }
168
+ else {
169
+ this.terminal.error(message);
170
+ }
171
+ return undefined;
172
+ }
173
+ }
174
+ /**
175
+ * Fetch team by ID or name for headless mode.
176
+ * First tries to match by ID, then by name (case-insensitive).
177
+ */
178
+ async fetchTeamById(token, teamIdOrName, format) {
179
+ this.terminal.actionStart('Fetching team');
180
+ try {
181
+ const { teams } = await this.teamService.getTeams(token.sessionKey, { fetchAll: true });
182
+ this.terminal.actionStop();
183
+ // First try to find by ID
184
+ let team = teams.find((t) => t.id === teamIdOrName);
185
+ // If not found by ID, try to find by name (case-insensitive)
186
+ if (!team) {
187
+ team = teams.find((t) => t.name.toLowerCase() === teamIdOrName.toLowerCase());
188
+ }
189
+ if (!team) {
190
+ if (format === 'json') {
191
+ this.outputJsonResult({ error: `Team "${teamIdOrName}" not found`, status: 'error' });
192
+ }
193
+ else {
194
+ this.terminal.error(`Team "${teamIdOrName}" not found`);
195
+ }
196
+ return undefined;
197
+ }
198
+ return team;
199
+ }
200
+ catch (error) {
201
+ this.terminal.actionStop();
202
+ const message = error instanceof Error ? error.message : 'Failed to fetch team';
203
+ if (format === 'json') {
204
+ this.outputJsonResult({ error: message, status: 'error' });
205
+ }
206
+ else {
207
+ this.terminal.error(message);
208
+ }
209
+ return undefined;
210
+ }
211
+ }
125
212
  async getExistingConfig() {
126
213
  const exists = await this.projectConfigStore.exists();
127
214
  if (!exists)
@@ -266,37 +353,80 @@ export class InitUseCase {
266
353
  await rm(acePath, { force: true, recursive: true });
267
354
  }
268
355
  async run(options) {
356
+ const format = options.format ?? 'text';
357
+ const isHeadless = Boolean(options.teamId && options.spaceId);
269
358
  try {
270
359
  await this.trackingService.track('init', { status: 'started' });
271
- const authToken = await this.ensureAuthenticated();
360
+ const authToken = await this.ensureAuthenticated(format);
272
361
  if (!authToken)
273
362
  return;
274
363
  const existingConfig = await this.getExistingConfig();
275
364
  if (existingConfig) {
276
- const shouldCleanup = options.force ? true : await this.confirmReInitialization(existingConfig);
277
- if (shouldCleanup) {
278
- await this.cleanupBeforeReInitialization();
279
- this.terminal.log('\n');
365
+ // In headless mode with force, always cleanup
366
+ // In headless mode without force, fail
367
+ // In interactive mode, prompt for confirmation
368
+ if (isHeadless) {
369
+ if (options.force) {
370
+ await this.cleanupBeforeReInitialization();
371
+ }
372
+ else {
373
+ if (format === 'json') {
374
+ this.outputJsonResult({
375
+ error: 'Project already initialized. Use --force to re-initialize.',
376
+ status: 'error',
377
+ });
378
+ }
379
+ else {
380
+ this.terminal.error('Project already initialized. Use --force to re-initialize.');
381
+ }
382
+ return;
383
+ }
280
384
  }
281
385
  else {
282
- this.terminal.log('\nCancelled. Project configuration unchanged.');
283
- return;
386
+ const shouldCleanup = options.force ? true : await this.confirmReInitialization(existingConfig);
387
+ if (shouldCleanup) {
388
+ await this.cleanupBeforeReInitialization();
389
+ this.terminal.log('\n');
390
+ }
391
+ else {
392
+ if (format === 'json') {
393
+ this.outputJsonResult({ status: 'cancelled' });
394
+ }
395
+ else {
396
+ this.terminal.log('\nCancelled. Project configuration unchanged.');
397
+ }
398
+ return;
399
+ }
284
400
  }
285
401
  }
286
402
  this.terminal.log('Initializing ByteRover project...\n');
287
- const selectedTeam = await this.fetchAndSelectTeam(authToken);
288
- if (!selectedTeam)
289
- return;
290
- const selectedSpace = await this.fetchAndSelectSpace(authToken, selectedTeam);
291
- if (!selectedSpace)
292
- return;
293
- // Handle ACE deprecation - check for existing ACE folder and offer removal
294
- const aceExists = await this.aceDirectoryExists();
295
- if (aceExists) {
296
- const shouldRemoveAce = await this.promptAceDeprecationRemoval();
297
- if (shouldRemoveAce) {
298
- await this.removeAceDirectory();
299
- this.terminal.log('✓ ACE folder removed');
403
+ let selectedTeam;
404
+ let selectedSpace;
405
+ if (isHeadless && options.teamId && options.spaceId) {
406
+ // Headless mode: fetch team and space by ID
407
+ selectedTeam = await this.fetchTeamById(authToken, options.teamId, format);
408
+ if (!selectedTeam)
409
+ return;
410
+ selectedSpace = await this.fetchSpaceById(authToken, selectedTeam, options.spaceId, format);
411
+ if (!selectedSpace)
412
+ return;
413
+ }
414
+ else {
415
+ // Interactive mode: prompt for selection
416
+ selectedTeam = await this.fetchAndSelectTeam(authToken);
417
+ if (!selectedTeam)
418
+ return;
419
+ selectedSpace = await this.fetchAndSelectSpace(authToken, selectedTeam);
420
+ if (!selectedSpace)
421
+ return;
422
+ // Handle ACE deprecation - check for existing ACE folder and offer removal
423
+ const aceExists = await this.aceDirectoryExists();
424
+ if (aceExists) {
425
+ const shouldRemoveAce = await this.promptAceDeprecationRemoval();
426
+ if (shouldRemoveAce) {
427
+ await this.removeAceDirectory();
428
+ this.terminal.log('✓ ACE folder removed');
429
+ }
300
430
  }
301
431
  }
302
432
  // Sync from remote or initialize context tree with templates
@@ -304,8 +434,11 @@ export class InitUseCase {
304
434
  projectConfig: { spaceId: selectedSpace.id, teamId: selectedTeam.id },
305
435
  token: authToken,
306
436
  });
307
- this.terminal.log();
308
- const selectedAgent = await this.promptForAgentSelection();
437
+ let selectedAgent = 'Claude Code';
438
+ if (!isHeadless) {
439
+ this.terminal.log();
440
+ selectedAgent = await this.promptForAgentSelection();
441
+ }
309
442
  const { chatLogPath, cwd } = this.detectWorkspacesForAgent(selectedAgent);
310
443
  this.terminal.log(`✓ Detected workspace: ${cwd}`);
311
444
  const config = BrvConfig.fromSpace({
@@ -315,18 +448,35 @@ export class InitUseCase {
315
448
  space: selectedSpace,
316
449
  });
317
450
  await this.projectConfigStore.write(config);
318
- this.terminal.log();
319
- await this.installConnectorForAgent(selectedAgent);
451
+ if (!isHeadless) {
452
+ this.terminal.log();
453
+ await this.installConnectorForAgent(selectedAgent);
454
+ }
320
455
  await this.trackingService.track('space:init');
321
- this.logSuccess(selectedSpace);
456
+ if (format === 'json') {
457
+ this.outputJsonResult({
458
+ configPath: join(process.cwd(), BRV_DIR, PROJECT_CONFIG_FILE),
459
+ spaceName: selectedSpace.getDisplayName(),
460
+ status: 'success',
461
+ teamName: selectedTeam.name,
462
+ });
463
+ }
464
+ else {
465
+ this.logSuccess(selectedSpace);
466
+ }
322
467
  await this.trackingService.track('init', { status: 'finished' });
323
468
  }
324
469
  catch (error) {
325
470
  // Stop action if it's in progress
326
471
  this.terminal.actionStop();
327
- const initErr = `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
328
- await this.trackingService.track('init', { message: initErr, status: 'error' });
329
- this.terminal.error(initErr);
472
+ const initErr = error instanceof Error ? error.message : 'Unknown error';
473
+ await this.trackingService.track('init', { message: `Initialization failed: ${initErr}`, status: 'error' });
474
+ if (format === 'json') {
475
+ this.outputJsonResult({ error: initErr, status: 'error' });
476
+ }
477
+ else {
478
+ this.terminal.error(`Initialization failed: ${initErr}`);
479
+ }
330
480
  }
331
481
  }
332
482
  async syncFromRemoteOrInitialize(params) {
@@ -334,7 +484,6 @@ export class InitUseCase {
334
484
  this.terminal.actionStart('Syncing from ByteRover...');
335
485
  try {
336
486
  const coGitSnapshot = await this.cogitPullService.pull({
337
- accessToken: params.token.accessToken,
338
487
  branch: DEFAULT_BRANCH,
339
488
  sessionKey: params.token.sessionKey,
340
489
  spaceId: params.projectConfig.spaceId,
@@ -369,4 +518,21 @@ export class InitUseCase {
369
518
  this.terminal.log(`✓ Configuration saved to: ${BRV_DIR}/${PROJECT_CONFIG_FILE}`);
370
519
  this.terminal.log("NOTE: It's recommended to add .brv/ to your .gitignore file since ByteRover already takes care of memory/context versioning for you.");
371
520
  }
521
+ /**
522
+ * Output JSON result for headless mode.
523
+ */
524
+ outputJsonResult(result) {
525
+ const response = {
526
+ command: 'init',
527
+ data: result,
528
+ success: result.status === 'success',
529
+ timestamp: new Date().toISOString(),
530
+ };
531
+ if (this.terminal instanceof HeadlessTerminal) {
532
+ this.terminal.writeFinalResponse(response);
533
+ }
534
+ else {
535
+ this.terminal.log(JSON.stringify(response));
536
+ }
537
+ }
372
538
  }