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.
- package/README.md +193 -12
- package/dist/core/domain/cipher/process/types.d.ts +1 -1
- package/dist/core/domain/entities/provider-config.d.ts +92 -0
- package/dist/core/domain/entities/provider-config.js +181 -0
- package/dist/core/domain/entities/provider-registry.d.ts +55 -0
- package/dist/core/domain/entities/provider-registry.js +74 -0
- package/dist/core/domain/errors/headless-prompt-error.d.ts +11 -0
- package/dist/core/domain/errors/headless-prompt-error.js +18 -0
- package/dist/core/interfaces/cipher/i-content-generator.d.ts +30 -0
- package/dist/core/interfaces/cipher/i-content-generator.js +12 -1
- package/dist/core/interfaces/cipher/message-factory.d.ts +4 -1
- package/dist/core/interfaces/cipher/message-factory.js +5 -0
- package/dist/core/interfaces/cipher/message-types.d.ts +19 -1
- package/dist/core/interfaces/i-cogit-pull-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-retrieval-service.d.ts +0 -1
- package/dist/core/interfaces/i-memory-storage-service.d.ts +0 -2
- package/dist/core/interfaces/i-provider-config-store.d.ts +88 -0
- package/dist/core/interfaces/i-provider-config-store.js +1 -0
- package/dist/core/interfaces/i-provider-keychain-store.d.ts +33 -0
- package/dist/core/interfaces/i-provider-keychain-store.js +1 -0
- package/dist/core/interfaces/i-space-service.d.ts +1 -2
- package/dist/core/interfaces/i-team-service.d.ts +1 -2
- package/dist/core/interfaces/i-user-service.d.ts +1 -2
- package/dist/core/interfaces/usecase/i-curate-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-init-use-case.d.ts +9 -3
- package/dist/core/interfaces/usecase/i-login-use-case.d.ts +4 -1
- package/dist/core/interfaces/usecase/i-pull-use-case.d.ts +5 -3
- package/dist/core/interfaces/usecase/i-push-use-case.d.ts +6 -4
- package/dist/core/interfaces/usecase/i-query-use-case.d.ts +2 -0
- package/dist/core/interfaces/usecase/i-status-use-case.d.ts +1 -0
- package/dist/infra/cipher/agent/service-initializer.d.ts +1 -1
- package/dist/infra/cipher/agent/service-initializer.js +0 -1
- package/dist/infra/cipher/file-system/file-system-service.js +5 -5
- package/dist/infra/cipher/http/internal-llm-http-service.d.ts +40 -1
- package/dist/infra/cipher/http/internal-llm-http-service.js +153 -4
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +8 -1
- package/dist/infra/cipher/llm/generators/byterover-content-generator.d.ts +2 -3
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +20 -11
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.d.ts +1 -0
- package/dist/infra/cipher/llm/generators/openrouter-content-generator.js +26 -0
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +13 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +75 -4
- package/dist/infra/cipher/llm/model-capabilities.d.ts +74 -0
- package/dist/infra/cipher/llm/model-capabilities.js +157 -0
- package/dist/infra/cipher/llm/openrouter-llm-service.d.ts +35 -1
- package/dist/infra/cipher/llm/openrouter-llm-service.js +216 -28
- package/dist/infra/cipher/llm/stream-processor.d.ts +22 -2
- package/dist/infra/cipher/llm/stream-processor.js +78 -4
- package/dist/infra/cipher/llm/thought-parser.d.ts +1 -1
- package/dist/infra/cipher/llm/thought-parser.js +5 -5
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.d.ts +49 -0
- package/dist/infra/cipher/llm/transformers/openrouter-stream-transformer.js +272 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.d.ts +71 -0
- package/dist/infra/cipher/llm/transformers/reasoning-extractor.js +253 -0
- package/dist/infra/cipher/process/process-service.js +1 -1
- package/dist/infra/cipher/session/chat-session.d.ts +2 -0
- package/dist/infra/cipher/session/chat-session.js +13 -2
- package/dist/infra/cipher/storage/message-storage-service.js +4 -0
- package/dist/infra/cipher/tools/implementations/bash-exec-tool.js +3 -3
- package/dist/infra/cipher/tools/implementations/task-tool.js +1 -1
- package/dist/infra/cogit/http-cogit-pull-service.js +1 -1
- package/dist/infra/cogit/http-cogit-push-service.js +0 -1
- package/dist/infra/http/authenticated-http-client.d.ts +1 -3
- package/dist/infra/http/authenticated-http-client.js +1 -5
- package/dist/infra/http/openrouter-api-client.d.ts +148 -0
- package/dist/infra/http/openrouter-api-client.js +161 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +9 -1
- package/dist/infra/memory/http-memory-retrieval-service.js +1 -1
- package/dist/infra/memory/http-memory-storage-service.js +2 -2
- package/dist/infra/process/agent-worker.js +178 -70
- package/dist/infra/process/inline-agent-executor.d.ts +32 -0
- package/dist/infra/process/inline-agent-executor.js +259 -0
- package/dist/infra/process/transport-handlers.d.ts +25 -4
- package/dist/infra/process/transport-handlers.js +57 -10
- package/dist/infra/repl/commands/connectors-command.js +2 -2
- package/dist/infra/repl/commands/index.js +5 -0
- package/dist/infra/repl/commands/model-command.d.ts +13 -0
- package/dist/infra/repl/commands/model-command.js +212 -0
- package/dist/infra/repl/commands/provider-command.d.ts +13 -0
- package/dist/infra/repl/commands/provider-command.js +181 -0
- package/dist/infra/repl/transport-client-helper.js +6 -2
- package/dist/infra/space/http-space-service.d.ts +1 -1
- package/dist/infra/space/http-space-service.js +2 -2
- package/dist/infra/storage/file-provider-config-store.d.ts +83 -0
- package/dist/infra/storage/file-provider-config-store.js +157 -0
- package/dist/infra/storage/provider-keychain-store.d.ts +37 -0
- package/dist/infra/storage/provider-keychain-store.js +75 -0
- package/dist/infra/storage/token-store.d.ts +4 -3
- package/dist/infra/storage/token-store.js +6 -5
- package/dist/infra/team/http-team-service.d.ts +1 -1
- package/dist/infra/team/http-team-service.js +2 -2
- package/dist/infra/terminal/headless-terminal.d.ts +91 -0
- package/dist/infra/terminal/headless-terminal.js +211 -0
- package/dist/infra/transport/socket-io-transport-client.d.ts +20 -0
- package/dist/infra/transport/socket-io-transport-client.js +88 -1
- package/dist/infra/usecase/curate-use-case.d.ts +40 -1
- package/dist/infra/usecase/curate-use-case.js +176 -15
- package/dist/infra/usecase/init-use-case.d.ts +27 -5
- package/dist/infra/usecase/init-use-case.js +200 -34
- package/dist/infra/usecase/login-use-case.d.ts +10 -8
- package/dist/infra/usecase/login-use-case.js +35 -2
- package/dist/infra/usecase/pull-use-case.d.ts +19 -5
- package/dist/infra/usecase/pull-use-case.js +71 -13
- package/dist/infra/usecase/push-use-case.d.ts +18 -5
- package/dist/infra/usecase/push-use-case.js +81 -14
- package/dist/infra/usecase/query-use-case.d.ts +21 -0
- package/dist/infra/usecase/query-use-case.js +114 -29
- package/dist/infra/usecase/space-list-use-case.js +1 -1
- package/dist/infra/usecase/space-switch-use-case.js +2 -2
- package/dist/infra/usecase/status-use-case.d.ts +36 -0
- package/dist/infra/usecase/status-use-case.js +185 -48
- package/dist/infra/user/http-user-service.d.ts +1 -1
- package/dist/infra/user/http-user-service.js +2 -2
- package/dist/oclif/commands/curate.d.ts +6 -1
- package/dist/oclif/commands/curate.js +24 -3
- package/dist/oclif/commands/init.d.ts +18 -0
- package/dist/oclif/commands/init.js +129 -0
- package/dist/oclif/commands/login.d.ts +9 -0
- package/dist/oclif/commands/login.js +45 -0
- package/dist/oclif/commands/pull.d.ts +16 -0
- package/dist/oclif/commands/pull.js +78 -0
- package/dist/oclif/commands/push.d.ts +17 -0
- package/dist/oclif/commands/push.js +87 -0
- package/dist/oclif/commands/query.d.ts +6 -1
- package/dist/oclif/commands/query.js +29 -4
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +17 -5
- package/dist/resources/tools/bash_exec.txt +1 -1
- package/dist/tui/components/api-key-dialog.d.ts +39 -0
- package/dist/tui/components/api-key-dialog.js +94 -0
- package/dist/tui/components/execution/execution-changes.d.ts +3 -1
- package/dist/tui/components/execution/execution-changes.js +4 -4
- package/dist/tui/components/execution/execution-content.d.ts +1 -1
- package/dist/tui/components/execution/execution-content.js +4 -12
- package/dist/tui/components/execution/execution-input.js +1 -1
- package/dist/tui/components/execution/execution-progress.d.ts +10 -13
- package/dist/tui/components/execution/execution-progress.js +70 -17
- package/dist/tui/components/execution/execution-reasoning.d.ts +16 -0
- package/dist/tui/components/execution/execution-reasoning.js +34 -0
- package/dist/tui/components/execution/execution-tool.d.ts +23 -0
- package/dist/tui/components/execution/execution-tool.js +125 -0
- package/dist/tui/components/execution/expanded-log-view.js +3 -3
- package/dist/tui/components/execution/log-item.d.ts +2 -0
- package/dist/tui/components/execution/log-item.js +6 -4
- package/dist/tui/components/index.d.ts +2 -0
- package/dist/tui/components/index.js +2 -0
- package/dist/tui/components/inline-prompts/inline-select.js +3 -2
- package/dist/tui/components/model-dialog.d.ts +63 -0
- package/dist/tui/components/model-dialog.js +89 -0
- package/dist/tui/components/onboarding/onboarding-flow.js +8 -2
- package/dist/tui/components/provider-dialog.d.ts +27 -0
- package/dist/tui/components/provider-dialog.js +31 -0
- package/dist/tui/components/reasoning-text.d.ts +26 -0
- package/dist/tui/components/reasoning-text.js +49 -0
- package/dist/tui/components/selectable-list.d.ts +54 -0
- package/dist/tui/components/selectable-list.js +180 -0
- package/dist/tui/components/streaming-text.d.ts +30 -0
- package/dist/tui/components/streaming-text.js +52 -0
- package/dist/tui/contexts/tasks-context.d.ts +15 -0
- package/dist/tui/contexts/tasks-context.js +224 -40
- package/dist/tui/contexts/theme-context.d.ts +1 -0
- package/dist/tui/contexts/theme-context.js +3 -2
- package/dist/tui/hooks/use-activity-logs.js +7 -1
- package/dist/tui/hooks/use-auth-polling.js +1 -1
- package/dist/tui/types/messages.d.ts +32 -5
- package/dist/tui/utils/index.d.ts +1 -1
- package/dist/tui/utils/index.js +1 -1
- package/dist/tui/utils/log.d.ts +0 -9
- package/dist/tui/utils/log.js +2 -53
- package/dist/tui/views/command-view.js +4 -1
- package/dist/utils/environment-detector.d.ts +15 -0
- package/dist/utils/environment-detector.js +62 -1
- package/oclif.manifest.json +287 -5
- package/package.json +1 -1
|
@@ -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
|
-
* -
|
|
6
|
+
* - Headless Linux: FileTokenStore (no D-Bus/keyring daemon)
|
|
7
|
+
* - macOS/Windows/Linux with GUI: KeychainTokenStore (system keychain via keytar)
|
|
7
8
|
*
|
|
8
|
-
* @param
|
|
9
|
+
* @param shouldUseFileFn - Optional function for environment detection (for testing)
|
|
9
10
|
*/
|
|
10
|
-
export declare function createTokenStore(
|
|
11
|
+
export declare function createTokenStore(shouldUseFileFn?: () => boolean): ITokenStore;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
-
* -
|
|
8
|
+
* - Headless Linux: FileTokenStore (no D-Bus/keyring daemon)
|
|
9
|
+
* - macOS/Windows/Linux with GUI: KeychainTokenStore (system keychain via keytar)
|
|
9
10
|
*
|
|
10
|
-
* @param
|
|
11
|
+
* @param shouldUseFileFn - Optional function for environment detection (for testing)
|
|
11
12
|
*/
|
|
12
|
-
export function createTokenStore(
|
|
13
|
-
return
|
|
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(
|
|
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(
|
|
12
|
+
async getTeams(sessionKey, option) {
|
|
13
13
|
try {
|
|
14
|
-
const httpClient = new AuthenticatedHttpClient(
|
|
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.
|