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
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Provider Keychain Store
3
+ *
4
+ * Stores provider API keys securely in the system keychain.
5
+ * Uses keytar for cross-platform keychain access.
6
+ */
7
+ import type { IProviderKeychainStore } from '../../core/interfaces/i-provider-keychain-store.js';
8
+ /**
9
+ * Keychain-based storage for provider API keys.
10
+ * Uses the system keychain for secure storage:
11
+ * - macOS: Keychain
12
+ * - Linux: Secret Service (or encrypted file fallback)
13
+ * - Windows: Credential Manager
14
+ */
15
+ export declare class ProviderKeychainStore implements IProviderKeychainStore {
16
+ /**
17
+ * Deletes the API key for a provider.
18
+ */
19
+ deleteApiKey(providerId: string): Promise<void>;
20
+ /**
21
+ * Gets the API key for a provider.
22
+ */
23
+ getApiKey(providerId: string): Promise<string | undefined>;
24
+ /**
25
+ * Checks if an API key exists for a provider.
26
+ */
27
+ hasApiKey(providerId: string): Promise<boolean>;
28
+ /**
29
+ * Sets the API key for a provider.
30
+ */
31
+ setApiKey(providerId: string, apiKey: string): Promise<void>;
32
+ }
33
+ /**
34
+ * Creates a provider keychain store instance.
35
+ * This factory function allows for future platform-specific implementations.
36
+ */
37
+ export declare function createProviderKeychainStore(): IProviderKeychainStore;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Provider Keychain Store
3
+ *
4
+ * Stores provider API keys securely in the system keychain.
5
+ * Uses keytar for cross-platform keychain access.
6
+ */
7
+ import keytar from 'keytar';
8
+ const SERVICE_NAME = 'byterover-cli-providers';
9
+ /**
10
+ * Creates the account name for a provider.
11
+ * Format: provider:<providerId>
12
+ */
13
+ function getAccountName(providerId) {
14
+ return `provider:${providerId}`;
15
+ }
16
+ /**
17
+ * Keychain-based storage for provider API keys.
18
+ * Uses the system keychain for secure storage:
19
+ * - macOS: Keychain
20
+ * - Linux: Secret Service (or encrypted file fallback)
21
+ * - Windows: Credential Manager
22
+ */
23
+ export class ProviderKeychainStore {
24
+ /**
25
+ * Deletes the API key for a provider.
26
+ */
27
+ async deleteApiKey(providerId) {
28
+ try {
29
+ const accountName = getAccountName(providerId);
30
+ await keytar.deletePassword(SERVICE_NAME, accountName);
31
+ }
32
+ catch {
33
+ // Ignore errors (key may not exist, permissions, etc.)
34
+ }
35
+ }
36
+ /**
37
+ * Gets the API key for a provider.
38
+ */
39
+ async getApiKey(providerId) {
40
+ try {
41
+ const accountName = getAccountName(providerId);
42
+ const apiKey = await keytar.getPassword(SERVICE_NAME, accountName);
43
+ return apiKey ?? undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ /**
50
+ * Checks if an API key exists for a provider.
51
+ */
52
+ async hasApiKey(providerId) {
53
+ const apiKey = await this.getApiKey(providerId);
54
+ return apiKey !== undefined;
55
+ }
56
+ /**
57
+ * Sets the API key for a provider.
58
+ */
59
+ async setApiKey(providerId, apiKey) {
60
+ try {
61
+ const accountName = getAccountName(providerId);
62
+ await keytar.setPassword(SERVICE_NAME, accountName, apiKey);
63
+ }
64
+ catch (error) {
65
+ throw new Error(`Failed to save API key to keychain: ${error instanceof Error ? error.message : 'Unknown error'}`);
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Creates a provider keychain store instance.
71
+ * This factory function allows for future platform-specific implementations.
72
+ */
73
+ export function createProviderKeychainStore() {
74
+ return new ProviderKeychainStore();
75
+ }
@@ -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
+ }
@@ -11,6 +11,8 @@ import type { ConnectionState, ConnectionStateHandler, EventHandler, ITransportC
11
11
  * - Auto-rejoins rooms after reconnect
12
12
  */
13
13
  export declare class SocketIOTransportClient implements ITransportClient {
14
+ /** Buffer for events that arrive before handlers are registered */
15
+ private bufferedEvents;
14
16
  private readonly config;
15
17
  private eventHandlers;
16
18
  /** Track force reconnect attempt count for backoff calculation */
@@ -86,6 +88,12 @@ export declare class SocketIOTransportClient implements ITransportClient {
86
88
  * Re-triggers reconnection if not connected and force reconnect has given up.
87
89
  */
88
90
  private handleWakeFromSleep;
91
+ /**
92
+ * Register socket listeners for all events that should be buffered.
93
+ * Called on connect to ensure critical events can be captured even before
94
+ * handlers subscribe. This prevents race conditions in TUI initialization.
95
+ */
96
+ private registerBufferedEventListeners;
89
97
  /**
90
98
  * Register all pending event handlers on the socket.
91
99
  * Called after successful connection to handle handlers added before connect().
@@ -94,6 +102,10 @@ export declare class SocketIOTransportClient implements ITransportClient {
94
102
  /**
95
103
  * Register a socket listener for an event if not already registered.
96
104
  * Uses registeredSocketEvents set to prevent duplicates.
105
+ *
106
+ * For critical events (task lifecycle, LLM events), buffers incoming events
107
+ * if no handlers are registered yet. This prevents race conditions where
108
+ * broadcast-room events arrive before TUI components subscribe.
97
109
  */
98
110
  private registerSocketEventIfNeeded;
99
111
  /**
@@ -111,6 +123,14 @@ export declare class SocketIOTransportClient implements ITransportClient {
111
123
  * Remove socket listener for an event and clear tracking.
112
124
  */
113
125
  private removeSocketEventListener;
126
+ /**
127
+ * Replay buffered events for a specific event type to a handler.
128
+ * Called when a new handler subscribes, to deliver any events that arrived
129
+ * before the handler was registered.
130
+ *
131
+ * Removes replayed events from the buffer and cleans up stale events.
132
+ */
133
+ private replayBufferedEvents;
114
134
  /**
115
135
  * Schedule force reconnect with exponential backoff.
116
136
  * Called after Socket.IO's built-in reconnection gives up.
@@ -8,6 +8,26 @@ import { processLog } from '../../utils/process-logger.js';
8
8
  function clientLog(message) {
9
9
  processLog(`[TransportClient] ${message}`);
10
10
  }
11
+ /**
12
+ * Events to buffer when received before handlers are registered.
13
+ * These are critical task lifecycle events that must not be lost.
14
+ */
15
+ const BUFFERED_EVENTS = new Set([
16
+ 'llmservice:chunk',
17
+ 'llmservice:response',
18
+ 'llmservice:toolCall',
19
+ 'llmservice:toolResult',
20
+ 'task:cancelled',
21
+ 'task:completed',
22
+ 'task:created',
23
+ 'task:error',
24
+ 'task:started',
25
+ ]);
26
+ /**
27
+ * Maximum age of buffered events in milliseconds.
28
+ * Events older than this are discarded to prevent memory leaks.
29
+ */
30
+ const BUFFERED_EVENT_MAX_AGE_MS = 10_000;
11
31
  /**
12
32
  * Force reconnect delays after Socket.IO gives up (exponential backoff).
13
33
  * Used when all built-in reconnection attempts fail.
@@ -39,6 +59,8 @@ const WAKE_TIME_JUMP_THRESHOLD_MS = 10_000;
39
59
  * - Auto-rejoins rooms after reconnect
40
60
  */
41
61
  export class SocketIOTransportClient {
62
+ /** Buffer for events that arrive before handlers are registered */
63
+ bufferedEvents = [];
42
64
  config;
43
65
  eventHandlers = new Map();
44
66
  /** Track force reconnect attempt count for backoff calculation */
@@ -102,6 +124,9 @@ export class SocketIOTransportClient {
102
124
  const onConnect = () => {
103
125
  this.setState('connected');
104
126
  cleanup();
127
+ // Register socket listeners for critical events that should be buffered.
128
+ // This allows events to be captured even before handlers subscribe.
129
+ this.registerBufferedEventListeners();
105
130
  // Register any handlers that were added before connect() was called.
106
131
  // This fixes the issue where on() called before connect() would not
107
132
  // actually register handlers on the socket.
@@ -149,6 +174,7 @@ export class SocketIOTransportClient {
149
174
  // listener accumulation. Socket.IO preserves the Socket instance across
150
175
  // internal reconnects, so old listeners remain attached if not removed.
151
176
  this.clearSocketEventListeners();
177
+ this.registerBufferedEventListeners();
152
178
  this.registerPendingEventHandlers();
153
179
  // Auto-rejoin rooms after reconnect
154
180
  // Use process.nextTick to ensure socket.connected is true
@@ -185,6 +211,7 @@ export class SocketIOTransportClient {
185
211
  this.eventHandlers.clear();
186
212
  this.registeredSocketEvents.clear();
187
213
  this.joinedRooms.clear();
214
+ this.bufferedEvents = [];
188
215
  resolve();
189
216
  });
190
217
  }
@@ -290,6 +317,9 @@ export class SocketIOTransportClient {
290
317
  const wrappedHandler = (data) => handler(data);
291
318
  const handlers = this.eventHandlers.get(event);
292
319
  handlers?.add(wrappedHandler);
320
+ // Replay any buffered events for this event type
321
+ // This handles the race condition where events arrive before handlers subscribe
322
+ this.replayBufferedEvents(event, wrappedHandler);
293
323
  // Return unsubscribe function that also cleans up socket listener if no handlers remain
294
324
  return () => {
295
325
  handlers?.delete(wrappedHandler);
@@ -447,6 +477,16 @@ export class SocketIOTransportClient {
447
477
  this.scheduleForceReconnect();
448
478
  }
449
479
  }
480
+ /**
481
+ * Register socket listeners for all events that should be buffered.
482
+ * Called on connect to ensure critical events can be captured even before
483
+ * handlers subscribe. This prevents race conditions in TUI initialization.
484
+ */
485
+ registerBufferedEventListeners() {
486
+ for (const event of BUFFERED_EVENTS) {
487
+ this.registerSocketEventIfNeeded(event);
488
+ }
489
+ }
450
490
  /**
451
491
  * Register all pending event handlers on the socket.
452
492
  * Called after successful connection to handle handlers added before connect().
@@ -459,6 +499,10 @@ export class SocketIOTransportClient {
459
499
  /**
460
500
  * Register a socket listener for an event if not already registered.
461
501
  * Uses registeredSocketEvents set to prevent duplicates.
502
+ *
503
+ * For critical events (task lifecycle, LLM events), buffers incoming events
504
+ * if no handlers are registered yet. This prevents race conditions where
505
+ * broadcast-room events arrive before TUI components subscribe.
462
506
  */
463
507
  registerSocketEventIfNeeded(event) {
464
508
  const { socket } = this;
@@ -468,11 +512,21 @@ export class SocketIOTransportClient {
468
512
  // Register the dispatch listener on socket
469
513
  socket.on(event, (data) => {
470
514
  const handlers = this.eventHandlers.get(event);
471
- if (handlers) {
515
+ if (handlers && handlers.size > 0) {
472
516
  for (const h of handlers) {
473
517
  h(data);
474
518
  }
475
519
  }
520
+ else if (BUFFERED_EVENTS.has(event)) {
521
+ // Buffer critical events when no handlers are registered
522
+ // This handles race condition where events arrive before TUI subscribes
523
+ this.bufferedEvents.push({
524
+ data,
525
+ event,
526
+ timestamp: Date.now(),
527
+ });
528
+ clientLog(`Buffered event '${event}' (no handlers yet), buffer size: ${this.bufferedEvents.length}`);
529
+ }
476
530
  });
477
531
  this.registeredSocketEvents.add(event);
478
532
  }
@@ -532,6 +586,39 @@ export class SocketIOTransportClient {
532
586
  this.registeredSocketEvents.delete(event);
533
587
  }
534
588
  }
589
+ /**
590
+ * Replay buffered events for a specific event type to a handler.
591
+ * Called when a new handler subscribes, to deliver any events that arrived
592
+ * before the handler was registered.
593
+ *
594
+ * Removes replayed events from the buffer and cleans up stale events.
595
+ */
596
+ replayBufferedEvents(event, handler) {
597
+ const now = Date.now();
598
+ const toReplay = [];
599
+ const toKeep = [];
600
+ for (const buffered of this.bufferedEvents) {
601
+ // Discard stale events
602
+ if (now - buffered.timestamp > BUFFERED_EVENT_MAX_AGE_MS) {
603
+ continue;
604
+ }
605
+ if (buffered.event === event) {
606
+ toReplay.push(buffered);
607
+ }
608
+ else {
609
+ toKeep.push(buffered);
610
+ }
611
+ }
612
+ // Update buffer with remaining events
613
+ this.bufferedEvents = toKeep;
614
+ // Replay events in order
615
+ if (toReplay.length > 0) {
616
+ clientLog(`Replaying ${toReplay.length} buffered '${event}' events`);
617
+ for (const buffered of toReplay) {
618
+ handler(buffered.data);
619
+ }
620
+ }
621
+ }
535
622
  /**
536
623
  * Schedule force reconnect with exponential backoff.
537
624
  * Called after Socket.IO's built-in reconnection gives up.