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
@@ -0,0 +1,259 @@
1
+ /**
2
+ * InlineAgent - Ephemeral in-process CipherAgent for headless commands.
3
+ *
4
+ * Used by `brv curate --headless` and `brv query --headless` to execute tasks
5
+ * without requiring a running REPL instance or Transport/Socket.IO infrastructure.
6
+ *
7
+ * Exposes a `transportClient` property (ITransportClient) so use cases can use it
8
+ * as a drop-in replacement for SocketIOTransportClient.
9
+ *
10
+ * Lifecycle:
11
+ * 1. InlineAgent.create() — loads auth, config, starts CipherAgent
12
+ * 2. Use case gets inlineAgent.transportClient and calls on()/request() as normal
13
+ * 3. transportClient.disconnect() — stops CipherAgent and cleans up
14
+ */
15
+ import { randomUUID } from 'node:crypto';
16
+ import { getCurrentConfig } from '../../config/environment.js';
17
+ import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
18
+ import { NotAuthenticatedError, serializeTaskError } from '../../core/domain/errors/task-error.js';
19
+ import { CipherAgent } from '../cipher/agent/index.js';
20
+ import { ProjectConfigStore } from '../config/file-config-store.js';
21
+ import { CurateExecutor } from '../core/executors/curate-executor.js';
22
+ import { QueryExecutor } from '../core/executors/query-executor.js';
23
+ import { createTokenStore } from '../storage/token-store.js';
24
+ /**
25
+ * Ephemeral in-process CipherAgent for headless CLI commands.
26
+ *
27
+ * Creates and owns a CipherAgent, and exposes an ITransportClient that
28
+ * use cases interact with exactly like a SocketIOTransportClient.
29
+ */
30
+ export class InlineAgent {
31
+ transportClient;
32
+ constructor(agent) {
33
+ this.transportClient = new InlineTransportClient(agent);
34
+ }
35
+ /**
36
+ * Async factory — loads auth/config, creates and starts CipherAgent.
37
+ *
38
+ * @throws NotAuthenticatedError if no auth token or token is expired
39
+ * @throws Error if no project config (.brv/config.json) exists
40
+ */
41
+ static async create() {
42
+ const tokenStore = createTokenStore();
43
+ const configStore = new ProjectConfigStore();
44
+ const authToken = await tokenStore.load();
45
+ if (!authToken || authToken.isExpired()) {
46
+ throw new NotAuthenticatedError();
47
+ }
48
+ const brvConfig = await configStore.read();
49
+ if (!brvConfig) {
50
+ throw new Error('Project not initialized. Run `brv` then `/init` first.');
51
+ }
52
+ const envConfig = getCurrentConfig();
53
+ const agentConfig = {
54
+ accessToken: authToken.accessToken,
55
+ apiBaseUrl: envConfig.llmApiBaseUrl,
56
+ fileSystem: { workingDirectory: process.cwd() },
57
+ llm: {
58
+ maxIterations: 10,
59
+ maxTokens: 4096,
60
+ temperature: 0.7,
61
+ topK: 10,
62
+ topP: 0.95,
63
+ verbose: false,
64
+ },
65
+ model: DEFAULT_LLM_MODEL,
66
+ projectId: PROJECT,
67
+ sessionKey: authToken.sessionKey,
68
+ };
69
+ const agent = new CipherAgent(agentConfig, brvConfig);
70
+ await agent.start();
71
+ const sessionId = `inline-session-${randomUUID()}`;
72
+ await agent.createSession(sessionId);
73
+ return new InlineAgent(agent);
74
+ }
75
+ }
76
+ /**
77
+ * ITransportClient backed by an in-process CipherAgent.
78
+ *
79
+ * Translates transport events (task:create, task:completed, llmservice:*) into
80
+ * direct CipherAgent execution via CurateExecutor/QueryExecutor.
81
+ */
82
+ class InlineTransportClient {
83
+ activeTask;
84
+ agent;
85
+ clientId = `inline-${randomUUID()}`;
86
+ curateExecutor;
87
+ handlers = new Map();
88
+ queryExecutor;
89
+ constructor(agent) {
90
+ this.agent = agent;
91
+ this.curateExecutor = new CurateExecutor();
92
+ this.queryExecutor = new QueryExecutor();
93
+ }
94
+ // ===========================================================================
95
+ // ITransportClient implementation
96
+ // ===========================================================================
97
+ async connect() {
98
+ // No-op — initialization done in InlineAgent.create()
99
+ }
100
+ async disconnect() {
101
+ this.handlers.clear();
102
+ // Stop the agent first — this causes any in-flight execute() to fail,
103
+ // which settles the activeTask promise.
104
+ try {
105
+ await this.agent.stop();
106
+ }
107
+ catch {
108
+ // Best-effort cleanup
109
+ }
110
+ // Wait for the task to settle (will resolve/reject quickly after agent.stop())
111
+ if (this.activeTask) {
112
+ await this.activeTask;
113
+ this.activeTask = undefined;
114
+ }
115
+ }
116
+ getClientId() {
117
+ return this.clientId;
118
+ }
119
+ getState() {
120
+ return 'connected';
121
+ }
122
+ async isConnected() {
123
+ return true;
124
+ }
125
+ async joinRoom() {
126
+ // No-op
127
+ }
128
+ async leaveRoom() {
129
+ // No-op
130
+ }
131
+ on(event, handler) {
132
+ if (!this.handlers.has(event)) {
133
+ this.handlers.set(event, new Set());
134
+ }
135
+ const handlerSet = this.handlers.get(event);
136
+ handlerSet.add(handler);
137
+ return () => {
138
+ handlerSet.delete(handler);
139
+ };
140
+ }
141
+ once(event, handler) {
142
+ const unsubscribe = this.on(event, (data) => {
143
+ unsubscribe();
144
+ handler(data);
145
+ });
146
+ }
147
+ onStateChange(_handler) {
148
+ // No-op — state never changes
149
+ return () => { };
150
+ }
151
+ async request(event, data, _options) {
152
+ if (event === 'task:create') {
153
+ // Returns immediately with {taskId}; execution runs asynchronously
154
+ return this.handleTaskCreate(data);
155
+ }
156
+ // Other events are no-ops for inline execution
157
+ return undefined;
158
+ }
159
+ // ===========================================================================
160
+ // Internal task execution
161
+ // ===========================================================================
162
+ /**
163
+ * Emit an event to all registered handlers.
164
+ */
165
+ emit(event, data) {
166
+ const handlerSet = this.handlers.get(event);
167
+ if (handlerSet) {
168
+ for (const handler of handlerSet) {
169
+ handler(data);
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Execute the task in-process, emitting transport-shaped events as it progresses.
175
+ */
176
+ async executeTask(data) {
177
+ const taskId = data.taskId;
178
+ const type = data.type;
179
+ const content = data.content;
180
+ const files = data.files;
181
+ const clientCwd = data.clientCwd;
182
+ // Emit task:ack
183
+ this.emit('task:ack', { taskId });
184
+ // Emit task:started
185
+ this.emit('task:started', { taskId });
186
+ // Subscribe to agentEventBus and forward events to registered handlers
187
+ const cleanupForwarders = this.setupEventForwarding(taskId);
188
+ try {
189
+ const result = await (type === 'curate'
190
+ ? this.curateExecutor.executeWithAgent(this.agent, {
191
+ clientCwd,
192
+ content,
193
+ files,
194
+ taskId,
195
+ })
196
+ : this.queryExecutor.executeWithAgent(this.agent, {
197
+ query: content,
198
+ taskId,
199
+ }));
200
+ // Emit task:completed
201
+ this.emit('task:completed', { result, taskId });
202
+ }
203
+ catch (error) {
204
+ // Emit task:error
205
+ const errorData = serializeTaskError(error);
206
+ this.emit('task:error', { error: errorData, taskId });
207
+ }
208
+ finally {
209
+ cleanupForwarders();
210
+ }
211
+ }
212
+ /**
213
+ * Handle task:create request — fire execution asynchronously and return immediately.
214
+ *
215
+ * This matches SocketIOTransportClient behavior: request('task:create') resolves
216
+ * with {taskId} right away, while execution runs in the background emitting events.
217
+ * The use case registers on() handlers after request() returns, before events arrive.
218
+ */
219
+ handleTaskCreate(data) {
220
+ const taskId = data.taskId;
221
+ // Fire execution asynchronously — do not await.
222
+ // Errors are handled internally (emitted as task:error), so the promise never rejects.
223
+ this.activeTask = this.executeTask(data);
224
+ return { taskId };
225
+ }
226
+ /**
227
+ * Forward agentEventBus events to registered transport-style handlers.
228
+ * Returns a cleanup function to remove all forwarders.
229
+ */
230
+ setupEventForwarding(taskId) {
231
+ const eventBus = this.agent.agentEventBus;
232
+ if (!eventBus) {
233
+ return () => { };
234
+ }
235
+ const forwarders = [];
236
+ const forward = (busEvent, transportEvent, transform) => {
237
+ const handler = (payload) => {
238
+ const data = payload;
239
+ if (data?.taskId === taskId) {
240
+ this.emit(transportEvent, transform ? transform(data) : data);
241
+ }
242
+ };
243
+ eventBus.on(busEvent, handler);
244
+ forwarders.push({ event: busEvent, handler });
245
+ };
246
+ forward('llmservice:toolCall', 'llmservice:toolCall');
247
+ forward('llmservice:toolResult', 'llmservice:toolResult');
248
+ forward('llmservice:response', 'llmservice:response');
249
+ forward('llmservice:error', 'llmservice:error');
250
+ forward('llmservice:thinking', 'llmservice:thinking');
251
+ forward('llmservice:chunk', 'llmservice:chunk');
252
+ forward('llmservice:unsupportedInput', 'llmservice:unsupportedInput');
253
+ return () => {
254
+ for (const { event, handler } of forwarders) {
255
+ eventBus.off(event, handler);
256
+ }
257
+ };
258
+ }
259
+ }
@@ -7,7 +7,7 @@ export type SpaceServiceConfig = {
7
7
  export declare class HttpSpaceService implements ISpaceService {
8
8
  private readonly config;
9
9
  constructor(config: SpaceServiceConfig);
10
- getSpaces(accessToken: string, sessionKey: string, teamId: string, option?: {
10
+ getSpaces(sessionKey: string, teamId: string, option?: {
11
11
  fetchAll?: boolean;
12
12
  limit?: number;
13
13
  offset?: number;
@@ -9,9 +9,9 @@ export class HttpSpaceService {
9
9
  timeout: 10_000, // Default 10 seconds timeout
10
10
  };
11
11
  }
12
- async getSpaces(accessToken, sessionKey, teamId, option) {
12
+ async getSpaces(sessionKey, teamId, option) {
13
13
  try {
14
- const httpClient = new AuthenticatedHttpClient(accessToken, sessionKey);
14
+ const httpClient = new AuthenticatedHttpClient(sessionKey);
15
15
  // Scenario 1: Fetch all automatically via auto-pagination
16
16
  if (option?.fetchAll === true) {
17
17
  return await this.fetchAllSpaces(httpClient, teamId);
@@ -3,8 +3,9 @@ import type { ITokenStore } from '../../core/interfaces/i-token-store.js';
3
3
  * Creates the appropriate token store for the current platform.
4
4
  *
5
5
  * - WSL: FileTokenStore (encrypted file-based, keychain not available)
6
- * - macOS/Linux/Windows: KeychainTokenStore (system keychain via keytar)
6
+ * - Headless Linux: FileTokenStore (no D-Bus/keyring daemon)
7
+ * - macOS/Windows/Linux with GUI: KeychainTokenStore (system keychain via keytar)
7
8
  *
8
- * @param isWslFn - Optional function to detect WSL (for testing)
9
+ * @param shouldUseFileFn - Optional function for environment detection (for testing)
9
10
  */
10
- export declare function createTokenStore(isWslFn?: () => boolean): ITokenStore;
11
+ export declare function createTokenStore(shouldUseFileFn?: () => boolean): ITokenStore;
@@ -1,14 +1,15 @@
1
- import { isWsl } from '../../utils/environment-detector.js';
1
+ import { shouldUseFileTokenStore } from '../../utils/environment-detector.js';
2
2
  import { FileTokenStore } from './file-token-store.js';
3
3
  import { KeychainTokenStore } from './keychain-token-store.js';
4
4
  /**
5
5
  * Creates the appropriate token store for the current platform.
6
6
  *
7
7
  * - WSL: FileTokenStore (encrypted file-based, keychain not available)
8
- * - macOS/Linux/Windows: KeychainTokenStore (system keychain via keytar)
8
+ * - Headless Linux: FileTokenStore (no D-Bus/keyring daemon)
9
+ * - macOS/Windows/Linux with GUI: KeychainTokenStore (system keychain via keytar)
9
10
  *
10
- * @param isWslFn - Optional function to detect WSL (for testing)
11
+ * @param shouldUseFileFn - Optional function for environment detection (for testing)
11
12
  */
12
- export function createTokenStore(isWslFn = isWsl) {
13
- return isWslFn() ? new FileTokenStore() : new KeychainTokenStore();
13
+ export function createTokenStore(shouldUseFileFn = shouldUseFileTokenStore) {
14
+ return shouldUseFileFn() ? new FileTokenStore() : new KeychainTokenStore();
14
15
  }
@@ -7,7 +7,7 @@ export type TeamServiceConfig = {
7
7
  export declare class HttpTeamService implements ITeamService {
8
8
  private readonly config;
9
9
  constructor(config: TeamServiceConfig);
10
- getTeams(accessToken: string, sessionKey: string, option?: {
10
+ getTeams(sessionKey: string, option?: {
11
11
  fetchAll?: boolean;
12
12
  isActive?: boolean;
13
13
  limit?: number;
@@ -9,9 +9,9 @@ export class HttpTeamService {
9
9
  timeout: 10_000, // Default 10 seconds timeout
10
10
  };
11
11
  }
12
- async getTeams(accessToken, sessionKey, option) {
12
+ async getTeams(sessionKey, option) {
13
13
  try {
14
- const httpClient = new AuthenticatedHttpClient(accessToken, sessionKey);
14
+ const httpClient = new AuthenticatedHttpClient(sessionKey);
15
15
  // Scenario 1: Fetch all automatically via auto-pagination
16
16
  if (option?.fetchAll === true) {
17
17
  return await this.fetchAllTeams(httpClient, option?.isActive);
@@ -0,0 +1,91 @@
1
+ import type { ConfirmOptions, FileSelectorItem, FileSelectorOptions, InputOptions, ITerminal, SearchOptions, SelectOptions } from '../../core/interfaces/i-terminal.js';
2
+ /**
3
+ * Output format for headless terminal.
4
+ * - 'text': Human-readable text output
5
+ * - 'json': NDJSON (newline-delimited JSON) for machine parsing
6
+ */
7
+ export type HeadlessOutputFormat = 'json' | 'text';
8
+ /**
9
+ * JSON message types for structured output.
10
+ */
11
+ export type HeadlessMessageType = 'action_start' | 'action_stop' | 'error' | 'log' | 'result' | 'warning';
12
+ /**
13
+ * Structured JSON output message.
14
+ */
15
+ export interface HeadlessJsonMessage {
16
+ actionId?: string;
17
+ id: string;
18
+ message: string;
19
+ timestamp: string;
20
+ type: HeadlessMessageType;
21
+ }
22
+ /**
23
+ * Options for creating a HeadlessTerminal.
24
+ */
25
+ export interface HeadlessTerminalOptions {
26
+ /**
27
+ * Stream for errors (defaults to process.stderr).
28
+ */
29
+ errorStream?: NodeJS.WritableStream;
30
+ /**
31
+ * If true, throw HeadlessPromptError when a prompt cannot be answered.
32
+ * If false, use sensible defaults (first choice, false for confirm, etc.)
33
+ * @default true
34
+ */
35
+ failOnPrompt?: boolean;
36
+ /**
37
+ * Output format: 'text' for human readable, 'json' for machine parsing.
38
+ * @default 'text'
39
+ */
40
+ outputFormat?: HeadlessOutputFormat;
41
+ /**
42
+ * Stream for output (defaults to process.stdout).
43
+ */
44
+ outputStream?: NodeJS.WritableStream;
45
+ /**
46
+ * Default values for prompts, keyed by prompt message or prompt type.
47
+ * Used to answer prompts automatically in headless mode.
48
+ */
49
+ promptDefaults?: Record<string, unknown>;
50
+ }
51
+ /**
52
+ * Terminal implementation for headless/non-interactive mode.
53
+ * Outputs to stdout/stderr and handles prompts via defaults or fails gracefully.
54
+ */
55
+ export declare class HeadlessTerminal implements ITerminal {
56
+ private currentActionId;
57
+ private readonly errorOutput;
58
+ private readonly failOnPrompt;
59
+ private readonly output;
60
+ private readonly outputFormat;
61
+ private readonly promptDefaults;
62
+ constructor(options?: HeadlessTerminalOptions);
63
+ actionStart(message: string): void;
64
+ actionStop(message?: string): void;
65
+ confirm(options: ConfirmOptions): Promise<boolean>;
66
+ error(message: string): void;
67
+ fileSelector(options: FileSelectorOptions): Promise<FileSelectorItem | null>;
68
+ input(options: InputOptions): Promise<string>;
69
+ log(message?: string): void;
70
+ search<T>(options: SearchOptions<T>): Promise<T>;
71
+ select<T>(options: SelectOptions<T>): Promise<T>;
72
+ warn(message: string): void;
73
+ /**
74
+ * Write final response with success/error status.
75
+ */
76
+ writeFinalResponse(response: {
77
+ command: string;
78
+ data?: unknown;
79
+ error?: {
80
+ code: string;
81
+ message: string;
82
+ };
83
+ success: boolean;
84
+ }): void;
85
+ /**
86
+ * Write final result in JSON format (convenience method for commands).
87
+ */
88
+ writeResult(data: Record<string, unknown>): void;
89
+ private getDefault;
90
+ private writeJson;
91
+ }
@@ -0,0 +1,211 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { HeadlessPromptError } from '../../core/domain/errors/headless-prompt-error.js';
3
+ /**
4
+ * Terminal implementation for headless/non-interactive mode.
5
+ * Outputs to stdout/stderr and handles prompts via defaults or fails gracefully.
6
+ */
7
+ export class HeadlessTerminal {
8
+ currentActionId = null;
9
+ errorOutput;
10
+ failOnPrompt;
11
+ output;
12
+ outputFormat;
13
+ promptDefaults;
14
+ constructor(options = {}) {
15
+ this.outputFormat = options.outputFormat ?? 'text';
16
+ this.promptDefaults = options.promptDefaults ?? {};
17
+ this.failOnPrompt = options.failOnPrompt ?? true;
18
+ this.output = options.outputStream ?? process.stdout;
19
+ this.errorOutput = options.errorStream ?? process.stderr;
20
+ }
21
+ // ==================== Output Methods ====================
22
+ actionStart(message) {
23
+ this.currentActionId = randomUUID();
24
+ if (this.outputFormat === 'json') {
25
+ this.writeJson({
26
+ actionId: this.currentActionId,
27
+ id: randomUUID(),
28
+ message,
29
+ timestamp: new Date().toISOString(),
30
+ type: 'action_start',
31
+ });
32
+ }
33
+ // In text mode, suppress action start for cleaner output
34
+ }
35
+ actionStop(message) {
36
+ if (this.outputFormat === 'json' && this.currentActionId) {
37
+ this.writeJson({
38
+ actionId: this.currentActionId,
39
+ id: randomUUID(),
40
+ message: message ?? '',
41
+ timestamp: new Date().toISOString(),
42
+ type: 'action_stop',
43
+ });
44
+ }
45
+ this.currentActionId = null;
46
+ }
47
+ async confirm(options) {
48
+ // Check for explicit default in promptDefaults
49
+ const defaultValue = this.getDefault('confirm', options.message);
50
+ if (defaultValue !== undefined) {
51
+ return Boolean(defaultValue);
52
+ }
53
+ // Use options.default if provided
54
+ if (options.default !== undefined) {
55
+ return options.default;
56
+ }
57
+ // Fail or return false
58
+ if (this.failOnPrompt) {
59
+ throw new HeadlessPromptError('confirm', options.message);
60
+ }
61
+ return false;
62
+ }
63
+ error(message) {
64
+ if (this.outputFormat === 'json') {
65
+ this.writeJson({
66
+ id: randomUUID(),
67
+ message,
68
+ timestamp: new Date().toISOString(),
69
+ type: 'error',
70
+ });
71
+ }
72
+ else {
73
+ this.errorOutput.write(`Error: ${message}\n`);
74
+ }
75
+ }
76
+ async fileSelector(options) {
77
+ // Check for explicit default in promptDefaults
78
+ const defaultValue = this.getDefault('file_selector', options.message);
79
+ if (defaultValue !== undefined && typeof defaultValue === 'string') {
80
+ return {
81
+ isDirectory: options.type === 'directory',
82
+ name: defaultValue.split('/').pop() ?? defaultValue,
83
+ path: defaultValue,
84
+ };
85
+ }
86
+ // Allow cancel if specified
87
+ if (options.allowCancel) {
88
+ return null;
89
+ }
90
+ // Fail
91
+ if (this.failOnPrompt) {
92
+ throw new HeadlessPromptError('file_selector', options.message);
93
+ }
94
+ return null;
95
+ }
96
+ // ==================== Input Methods ====================
97
+ async input(options) {
98
+ // Check for explicit default in promptDefaults
99
+ const defaultValue = this.getDefault('input', options.message);
100
+ if (defaultValue !== undefined) {
101
+ const value = String(defaultValue);
102
+ // Validate if validator is provided
103
+ if (options.validate) {
104
+ const validationResult = options.validate(value);
105
+ if (validationResult !== true) {
106
+ const errorMsg = typeof validationResult === 'string' ? validationResult : 'Validation failed';
107
+ throw new HeadlessPromptError('input', `${options.message} (validation error: ${errorMsg})`);
108
+ }
109
+ }
110
+ return value;
111
+ }
112
+ // Fail
113
+ if (this.failOnPrompt) {
114
+ throw new HeadlessPromptError('input', options.message);
115
+ }
116
+ return '';
117
+ }
118
+ log(message) {
119
+ if (this.outputFormat === 'json') {
120
+ this.writeJson({
121
+ id: randomUUID(),
122
+ message: message ?? '',
123
+ timestamp: new Date().toISOString(),
124
+ type: 'log',
125
+ });
126
+ }
127
+ else {
128
+ this.output.write((message ?? '') + '\n');
129
+ }
130
+ }
131
+ async search(options) {
132
+ // Search prompts require user interaction - always fail in headless mode
133
+ // unless a default is explicitly provided
134
+ const defaultValue = this.getDefault('search', options.message);
135
+ if (defaultValue !== undefined) {
136
+ return defaultValue;
137
+ }
138
+ throw new HeadlessPromptError('search', options.message);
139
+ }
140
+ async select(options) {
141
+ // Check for explicit default in promptDefaults (by value or name)
142
+ const defaultValue = this.getDefault('select', options.message);
143
+ if (defaultValue !== undefined) {
144
+ const choice = options.choices.find((c) => c.value === defaultValue || c.name === defaultValue);
145
+ if (choice) {
146
+ return choice.value;
147
+ }
148
+ }
149
+ // Fail or return first choice
150
+ if (this.failOnPrompt) {
151
+ throw new HeadlessPromptError('select', options.message, options.choices.map((c) => c.name));
152
+ }
153
+ // Return first choice as fallback
154
+ if (options.choices.length > 0) {
155
+ return options.choices[0].value;
156
+ }
157
+ throw new HeadlessPromptError('select', options.message, []);
158
+ }
159
+ warn(message) {
160
+ if (this.outputFormat === 'json') {
161
+ this.writeJson({
162
+ id: randomUUID(),
163
+ message,
164
+ timestamp: new Date().toISOString(),
165
+ type: 'warning',
166
+ });
167
+ }
168
+ else {
169
+ this.errorOutput.write(`Warning: ${message}\n`);
170
+ }
171
+ }
172
+ // ==================== Helper Methods ====================
173
+ /**
174
+ * Write final response with success/error status.
175
+ */
176
+ writeFinalResponse(response) {
177
+ if (this.outputFormat === 'json') {
178
+ this.output.write(JSON.stringify({
179
+ ...response,
180
+ timestamp: new Date().toISOString(),
181
+ }) + '\n');
182
+ }
183
+ }
184
+ /**
185
+ * Write final result in JSON format (convenience method for commands).
186
+ */
187
+ writeResult(data) {
188
+ if (this.outputFormat === 'json') {
189
+ this.writeJson({
190
+ id: randomUUID(),
191
+ message: JSON.stringify(data),
192
+ timestamp: new Date().toISOString(),
193
+ type: 'result',
194
+ });
195
+ }
196
+ }
197
+ getDefault(promptType, promptMessage) {
198
+ // First check by exact message
199
+ if (this.promptDefaults[promptMessage] !== undefined) {
200
+ return this.promptDefaults[promptMessage];
201
+ }
202
+ // Then check by prompt type
203
+ if (this.promptDefaults[promptType] !== undefined) {
204
+ return this.promptDefaults[promptType];
205
+ }
206
+ return undefined;
207
+ }
208
+ writeJson(data) {
209
+ this.output.write(JSON.stringify(data) + '\n');
210
+ }
211
+ }