@vdntio/clai 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/ai/index.d.ts +24 -0
- package/dist/ai/index.js +78 -0
- package/dist/ai/mock.d.ts +17 -0
- package/dist/ai/mock.js +49 -0
- package/dist/ai/parser.d.ts +14 -0
- package/dist/ai/parser.js +109 -0
- package/dist/ai/prompt.d.ts +12 -0
- package/dist/ai/prompt.js +76 -0
- package/dist/ai/providers/index.d.ts +1 -0
- package/dist/ai/providers/index.js +2 -0
- package/dist/ai/providers/openrouter.d.ts +31 -0
- package/dist/ai/providers/openrouter.js +142 -0
- package/dist/ai/types.d.ts +46 -0
- package/dist/ai/types.js +15 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +71 -0
- package/dist/config/index.d.ts +12 -0
- package/dist/config/index.js +363 -0
- package/dist/config/types.d.ts +76 -0
- package/dist/config/types.js +40 -0
- package/dist/context/directory.d.ts +18 -0
- package/dist/context/directory.js +71 -0
- package/dist/context/history.d.ts +16 -0
- package/dist/context/history.js +89 -0
- package/dist/context/index.d.ts +24 -0
- package/dist/context/index.js +61 -0
- package/dist/context/redaction.d.ts +14 -0
- package/dist/context/redaction.js +57 -0
- package/dist/context/stdin.d.ts +13 -0
- package/dist/context/stdin.js +86 -0
- package/dist/context/system.d.ts +11 -0
- package/dist/context/system.js +56 -0
- package/dist/context/types.d.ts +31 -0
- package/dist/context/types.js +10 -0
- package/dist/error/index.d.ts +30 -0
- package/dist/error/index.js +50 -0
- package/dist/logging/file-logger.d.ts +12 -0
- package/dist/logging/file-logger.js +66 -0
- package/dist/logging/index.d.ts +15 -0
- package/dist/logging/index.js +33 -0
- package/dist/logging/logger.d.ts +15 -0
- package/dist/logging/logger.js +60 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +192 -0
- package/dist/output/execute.d.ts +30 -0
- package/dist/output/execute.js +144 -0
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +7 -0
- package/dist/output/types.d.ts +48 -0
- package/dist/output/types.js +34 -0
- package/dist/output/validate.d.ts +23 -0
- package/dist/output/validate.js +42 -0
- package/dist/safety/index.d.ts +34 -0
- package/dist/safety/index.js +59 -0
- package/dist/safety/patterns.d.ts +23 -0
- package/dist/safety/patterns.js +96 -0
- package/dist/safety/types.d.ts +20 -0
- package/dist/safety/types.js +18 -0
- package/dist/signals/index.d.ts +4 -0
- package/dist/signals/index.js +35 -0
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.js +57 -0
- package/dist/ui/components/ActionPrompt.d.ts +8 -0
- package/dist/ui/components/ActionPrompt.js +9 -0
- package/dist/ui/components/CommandDisplay.d.ts +9 -0
- package/dist/ui/components/CommandDisplay.js +13 -0
- package/dist/ui/components/DangerousWarning.d.ts +6 -0
- package/dist/ui/components/DangerousWarning.js +6 -0
- package/dist/ui/components/Spinner.d.ts +15 -0
- package/dist/ui/components/Spinner.js +17 -0
- package/dist/ui/hooks/useAnimation.d.ts +27 -0
- package/dist/ui/hooks/useAnimation.js +85 -0
- package/dist/ui/hooks/useTerminalSize.d.ts +12 -0
- package/dist/ui/hooks/useTerminalSize.js +56 -0
- package/dist/ui/hooks/useTimeout.d.ts +9 -0
- package/dist/ui/hooks/useTimeout.js +31 -0
- package/dist/ui/index.d.ts +25 -0
- package/dist/ui/index.js +80 -0
- package/dist/ui/output.d.ts +20 -0
- package/dist/ui/output.js +56 -0
- package/dist/ui/spinner.d.ts +13 -0
- package/dist/ui/spinner.js +58 -0
- package/dist/ui/types.d.ts +105 -0
- package/dist/ui/types.js +60 -0
- package/dist/ui/utils/formatCommand.d.ts +50 -0
- package/dist/ui/utils/formatCommand.js +113 -0
- package/package.json +68 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// src/output/index.ts
|
|
2
|
+
// Re-export all output-related functionality
|
|
3
|
+
export { ExecutionError, Errors, } from './types.js';
|
|
4
|
+
export { isRecursiveCall, validateCommand, } from './validate.js';
|
|
5
|
+
export { getShell, executeCommand, } from './execute.js';
|
|
6
|
+
// Re-export print functions from ui/output for convenience
|
|
7
|
+
export { printCommand, printWarning, printError, printSuccess, printInfo, } from '../ui/output.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ClaiError } from '../error/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Execution result type (functional Result pattern)
|
|
4
|
+
* Either success with exit code, or failure with typed error
|
|
5
|
+
*/
|
|
6
|
+
export type ExecutionResult = {
|
|
7
|
+
success: true;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
} | {
|
|
10
|
+
success: false;
|
|
11
|
+
error: ExecutionError;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Validation result for command validation
|
|
15
|
+
*/
|
|
16
|
+
export type ValidationResult = {
|
|
17
|
+
valid: true;
|
|
18
|
+
} | {
|
|
19
|
+
valid: false;
|
|
20
|
+
error: ExecutionError;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* ExecutionError represents failures during command execution.
|
|
24
|
+
* Each error type has a specific exit code following shell conventions.
|
|
25
|
+
*/
|
|
26
|
+
export declare class ExecutionError extends ClaiError {
|
|
27
|
+
constructor(message: string, code: number, cause?: Error);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Error factory functions (pure, testable)
|
|
31
|
+
* Exit codes follow shell conventions:
|
|
32
|
+
* - 1: General error
|
|
33
|
+
* - 5: Safety abort (clai-specific)
|
|
34
|
+
* - 124: Timeout (following GNU coreutils timeout)
|
|
35
|
+
* - 126: Permission denied (POSIX)
|
|
36
|
+
* - 127: Command not found (POSIX)
|
|
37
|
+
* - 128+N: Killed by signal N
|
|
38
|
+
*/
|
|
39
|
+
export declare const Errors: {
|
|
40
|
+
readonly emptyCommand: () => ExecutionError;
|
|
41
|
+
readonly spawnFailed: (cmd: string, err: Error) => ExecutionError;
|
|
42
|
+
readonly shellNotFound: (shell: string) => ExecutionError;
|
|
43
|
+
readonly commandNotFound: (cmd: string) => ExecutionError;
|
|
44
|
+
readonly permissionDenied: (cmd: string) => ExecutionError;
|
|
45
|
+
readonly signalKilled: (signal: string) => ExecutionError;
|
|
46
|
+
readonly timeout: (timeoutMs: number) => ExecutionError;
|
|
47
|
+
readonly recursiveCall: () => ExecutionError;
|
|
48
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/output/types.ts
|
|
2
|
+
// Types for command execution and result handling
|
|
3
|
+
import { ClaiError } from '../error/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* ExecutionError represents failures during command execution.
|
|
6
|
+
* Each error type has a specific exit code following shell conventions.
|
|
7
|
+
*/
|
|
8
|
+
export class ExecutionError extends ClaiError {
|
|
9
|
+
constructor(message, code, cause) {
|
|
10
|
+
super(message, code, cause);
|
|
11
|
+
this.name = 'ExecutionError';
|
|
12
|
+
Object.setPrototypeOf(this, ExecutionError.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Error factory functions (pure, testable)
|
|
17
|
+
* Exit codes follow shell conventions:
|
|
18
|
+
* - 1: General error
|
|
19
|
+
* - 5: Safety abort (clai-specific)
|
|
20
|
+
* - 124: Timeout (following GNU coreutils timeout)
|
|
21
|
+
* - 126: Permission denied (POSIX)
|
|
22
|
+
* - 127: Command not found (POSIX)
|
|
23
|
+
* - 128+N: Killed by signal N
|
|
24
|
+
*/
|
|
25
|
+
export const Errors = {
|
|
26
|
+
emptyCommand: () => new ExecutionError('Empty command', 1),
|
|
27
|
+
spawnFailed: (cmd, err) => new ExecutionError(`Failed to spawn: ${cmd}`, 1, err),
|
|
28
|
+
shellNotFound: (shell) => new ExecutionError(`Shell not found: ${shell}`, 127),
|
|
29
|
+
commandNotFound: (cmd) => new ExecutionError(`Command not found: ${cmd}`, 127),
|
|
30
|
+
permissionDenied: (cmd) => new ExecutionError(`Permission denied: ${cmd}`, 126),
|
|
31
|
+
signalKilled: (signal) => new ExecutionError(`Killed by signal: ${signal}`, 128),
|
|
32
|
+
timeout: (timeoutMs) => new ExecutionError(`Command timed out after ${timeoutMs}ms`, 124),
|
|
33
|
+
recursiveCall: () => new ExecutionError('Refusing to execute clai recursively (would cause infinite loop)', 5),
|
|
34
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ValidationResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detect recursive clai invocation (AI suggesting clai commands → infinite loop)
|
|
4
|
+
*
|
|
5
|
+
* Matches:
|
|
6
|
+
* - "clai" at start of command
|
|
7
|
+
* - "./clai" at start of command
|
|
8
|
+
* - "/path/to/clai" at start of command
|
|
9
|
+
* - "clai" after pipe: "echo | clai"
|
|
10
|
+
* - "clai" after &&: "cd foo && clai"
|
|
11
|
+
* - "clai" after ;: "echo hi; clai"
|
|
12
|
+
*
|
|
13
|
+
* Does NOT match (no false positives):
|
|
14
|
+
* - "claimant" (clai is substring of word)
|
|
15
|
+
* - "echo clai" (clai is argument, not command)
|
|
16
|
+
* - "/usr/bin/claim" (different command)
|
|
17
|
+
*/
|
|
18
|
+
export declare function isRecursiveCall(command: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Validate command before execution
|
|
21
|
+
* Returns validation result with typed error if invalid
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateCommand(command: string): ValidationResult;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/output/validate.ts
|
|
2
|
+
// Command validation before execution
|
|
3
|
+
import { Errors } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Detect recursive clai invocation (AI suggesting clai commands → infinite loop)
|
|
6
|
+
*
|
|
7
|
+
* Matches:
|
|
8
|
+
* - "clai" at start of command
|
|
9
|
+
* - "./clai" at start of command
|
|
10
|
+
* - "/path/to/clai" at start of command
|
|
11
|
+
* - "clai" after pipe: "echo | clai"
|
|
12
|
+
* - "clai" after &&: "cd foo && clai"
|
|
13
|
+
* - "clai" after ;: "echo hi; clai"
|
|
14
|
+
*
|
|
15
|
+
* Does NOT match (no false positives):
|
|
16
|
+
* - "claimant" (clai is substring of word)
|
|
17
|
+
* - "echo clai" (clai is argument, not command)
|
|
18
|
+
* - "/usr/bin/claim" (different command)
|
|
19
|
+
*/
|
|
20
|
+
export function isRecursiveCall(command) {
|
|
21
|
+
// Pattern explanation:
|
|
22
|
+
// (?:^|[|;&]\s*) - Start of string OR after pipe/&&/; with optional whitespace
|
|
23
|
+
// (?:\.\/|\/[\w\/]*)? - Optional ./ prefix or absolute path
|
|
24
|
+
// clai - The literal "clai"
|
|
25
|
+
// (?:\s|$) - Followed by whitespace or end of string (word boundary)
|
|
26
|
+
const claiPattern = /(?:^|[|;&]\s*)(?:\.\/|\/[\w/]*)?clai(?:\s|$)/;
|
|
27
|
+
return claiPattern.test(command);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validate command before execution
|
|
31
|
+
* Returns validation result with typed error if invalid
|
|
32
|
+
*/
|
|
33
|
+
export function validateCommand(command) {
|
|
34
|
+
const trimmed = command.trim();
|
|
35
|
+
if (!trimmed) {
|
|
36
|
+
return { valid: false, error: Errors.emptyCommand() };
|
|
37
|
+
}
|
|
38
|
+
if (isRecursiveCall(trimmed)) {
|
|
39
|
+
return { valid: false, error: Errors.recursiveCall() };
|
|
40
|
+
}
|
|
41
|
+
return { valid: true };
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Config } from '../config/types.js';
|
|
2
|
+
import type { CompiledPattern } from './types.js';
|
|
3
|
+
export { SafetyError } from './types.js';
|
|
4
|
+
export type { CompiledPattern } from './types.js';
|
|
5
|
+
export { DEFAULT_DANGEROUS_PATTERNS, compilePatterns, isDangerous } from './patterns.js';
|
|
6
|
+
/**
|
|
7
|
+
* Load and compile dangerous patterns from config or defaults
|
|
8
|
+
*
|
|
9
|
+
* @param config - Application config
|
|
10
|
+
* @returns Compiled patterns ready for matching
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadPatterns(config: Config): CompiledPattern[];
|
|
13
|
+
/**
|
|
14
|
+
* Determine if we should show an interactive safety prompt
|
|
15
|
+
* Returns true only when all conditions are met:
|
|
16
|
+
* - Safety confirmation is enabled in config
|
|
17
|
+
* - Force flag is not set
|
|
18
|
+
* - Running in interactive TTY mode
|
|
19
|
+
*
|
|
20
|
+
* @param config - Application config
|
|
21
|
+
* @returns true if safety prompt should be shown
|
|
22
|
+
*/
|
|
23
|
+
export declare function shouldPrompt(config: Config): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Check commands for dangerous patterns and determine UI behavior
|
|
26
|
+
*
|
|
27
|
+
* @param commands - Commands to check
|
|
28
|
+
* @param config - Application config
|
|
29
|
+
* @returns Object with danger status and prompt requirement
|
|
30
|
+
*/
|
|
31
|
+
export declare function checkSafety(commands: string[], config: Config): {
|
|
32
|
+
isDangerous: boolean;
|
|
33
|
+
shouldPrompt: boolean;
|
|
34
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/safety/index.ts
|
|
2
|
+
// Safety module entry point
|
|
3
|
+
import { DEFAULT_DANGEROUS_PATTERNS, compilePatterns, isDangerous } from './patterns.js';
|
|
4
|
+
// Re-export types and functions
|
|
5
|
+
export { SafetyError } from './types.js';
|
|
6
|
+
export { DEFAULT_DANGEROUS_PATTERNS, compilePatterns, isDangerous } from './patterns.js';
|
|
7
|
+
/**
|
|
8
|
+
* Load and compile dangerous patterns from config or defaults
|
|
9
|
+
*
|
|
10
|
+
* @param config - Application config
|
|
11
|
+
* @returns Compiled patterns ready for matching
|
|
12
|
+
*/
|
|
13
|
+
export function loadPatterns(config) {
|
|
14
|
+
const patterns = config.safety.dangerousPatterns.length > 0
|
|
15
|
+
? config.safety.dangerousPatterns
|
|
16
|
+
: DEFAULT_DANGEROUS_PATTERNS;
|
|
17
|
+
return compilePatterns(patterns);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Determine if we should show an interactive safety prompt
|
|
21
|
+
* Returns true only when all conditions are met:
|
|
22
|
+
* - Safety confirmation is enabled in config
|
|
23
|
+
* - Force flag is not set
|
|
24
|
+
* - Running in interactive TTY mode
|
|
25
|
+
*
|
|
26
|
+
* @param config - Application config
|
|
27
|
+
* @returns true if safety prompt should be shown
|
|
28
|
+
*/
|
|
29
|
+
export function shouldPrompt(config) {
|
|
30
|
+
// Don't prompt if safety confirmation is disabled
|
|
31
|
+
if (!config.safety.confirmDangerous) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Don't prompt if force flag is set
|
|
35
|
+
if (config.force) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Don't prompt in non-interactive mode (piped)
|
|
39
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check commands for dangerous patterns and determine UI behavior
|
|
46
|
+
*
|
|
47
|
+
* @param commands - Commands to check
|
|
48
|
+
* @param config - Application config
|
|
49
|
+
* @returns Object with danger status and prompt requirement
|
|
50
|
+
*/
|
|
51
|
+
export function checkSafety(commands, config) {
|
|
52
|
+
const patterns = loadPatterns(config);
|
|
53
|
+
// Check if any command is dangerous
|
|
54
|
+
const dangerous = commands.some((cmd) => isDangerous(cmd, patterns));
|
|
55
|
+
return {
|
|
56
|
+
isDangerous: dangerous,
|
|
57
|
+
shouldPrompt: dangerous && shouldPrompt(config),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CompiledPattern } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default dangerous patterns - regex strings that match potentially destructive commands
|
|
4
|
+
* These are compiled to RegExp at runtime for matching
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_DANGEROUS_PATTERNS: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* Compile pattern strings into RegExp objects
|
|
9
|
+
* Invalid patterns are marked with isValid: false
|
|
10
|
+
*
|
|
11
|
+
* @param patterns - Array of regex pattern strings
|
|
12
|
+
* @returns Array of compiled patterns with validity status
|
|
13
|
+
*/
|
|
14
|
+
export declare function compilePatterns(patterns: readonly string[]): CompiledPattern[];
|
|
15
|
+
/**
|
|
16
|
+
* Check if a command matches any dangerous pattern
|
|
17
|
+
* Fail-safe: if any pattern is invalid, treat all commands as dangerous
|
|
18
|
+
*
|
|
19
|
+
* @param command - The command string to check
|
|
20
|
+
* @param compiledPatterns - Array of compiled patterns
|
|
21
|
+
* @returns true if command is dangerous, false if safe
|
|
22
|
+
*/
|
|
23
|
+
export declare function isDangerous(command: string, compiledPatterns: CompiledPattern[]): boolean;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/safety/patterns.ts
|
|
2
|
+
// Dangerous command pattern detection
|
|
3
|
+
/**
|
|
4
|
+
* Default dangerous patterns - regex strings that match potentially destructive commands
|
|
5
|
+
* These are compiled to RegExp at runtime for matching
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_DANGEROUS_PATTERNS = [
|
|
8
|
+
// File deletion patterns
|
|
9
|
+
'rm\\s+(-[^\\s]*)?\\s*-rf\\s+/', // rm -rf / (root wipe)
|
|
10
|
+
'rm\\s+(-[^\\s]*)?\\s*-f', // rm with force flag
|
|
11
|
+
'rm\\s+(-[^\\s]*r[^\\s]*|[^\\s]*r)\\s+', // rm with recursive
|
|
12
|
+
'find\\s+.*-exec\\s+(rm|del)', // find ... -exec rm/del
|
|
13
|
+
// Disk/device operations
|
|
14
|
+
'dd\\s+.*if=/dev/(zero|random|urandom)', // disk wipe with dd
|
|
15
|
+
'dd\\s+.*of=/dev/', // write to raw device
|
|
16
|
+
'mkfs\\.\\w+\\s+/dev/', // format filesystem
|
|
17
|
+
'mkfs\\s+-t\\s+\\w+\\s+/dev/', // mkfs with type flag
|
|
18
|
+
'shred\\s+', // secure delete
|
|
19
|
+
// Privileged destruction
|
|
20
|
+
'sudo\\s+rm\\s+(-[^\\s]*)?\\s*-rf', // sudo rm -rf
|
|
21
|
+
'sudo\\s+dd\\s+', // sudo dd
|
|
22
|
+
'sudo\\s+mkfs', // sudo mkfs
|
|
23
|
+
// Redirects to devices
|
|
24
|
+
'>\\s*/dev/sd[a-z]', // redirect to disk device
|
|
25
|
+
'>\\s*/dev/nvme', // redirect to nvme
|
|
26
|
+
'>\\s*/dev/null.*<', // suspicious null redirect
|
|
27
|
+
// Database destruction
|
|
28
|
+
'drop\\s+database', // SQL drop database
|
|
29
|
+
'drop\\s+table', // SQL drop table
|
|
30
|
+
'truncate\\s+table', // SQL truncate
|
|
31
|
+
'delete\\s+from\\s+\\w+\\s*;?$', // DELETE without WHERE
|
|
32
|
+
// Git destructive operations
|
|
33
|
+
'git\\s+reset\\s+--hard', // discard all changes
|
|
34
|
+
'git\\s+clean\\s+-fd', // remove untracked files
|
|
35
|
+
'git\\s+push\\s+.*--force', // force push
|
|
36
|
+
// System modification
|
|
37
|
+
'chmod\\s+(-R\\s+)?777\\s+/', // world-writable root
|
|
38
|
+
'chown\\s+-R\\s+.*:\\s*/', // recursive chown on root
|
|
39
|
+
':(){ :|:& };:', // fork bomb
|
|
40
|
+
// Windows-specific (for cross-platform awareness)
|
|
41
|
+
'format\\s+[a-z]:', // format drive
|
|
42
|
+
'del\\s+/[fqs]', // del with force/quiet/subdirs
|
|
43
|
+
'rd\\s+/s\\s+/q', // rmdir /s /q
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Compile pattern strings into RegExp objects
|
|
47
|
+
* Invalid patterns are marked with isValid: false
|
|
48
|
+
*
|
|
49
|
+
* @param patterns - Array of regex pattern strings
|
|
50
|
+
* @returns Array of compiled patterns with validity status
|
|
51
|
+
*/
|
|
52
|
+
export function compilePatterns(patterns) {
|
|
53
|
+
return patterns.map((pattern) => {
|
|
54
|
+
try {
|
|
55
|
+
return {
|
|
56
|
+
pattern,
|
|
57
|
+
regex: new RegExp(pattern, 'i'),
|
|
58
|
+
isValid: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Invalid regex - mark as invalid (will trigger fail-safe)
|
|
63
|
+
return {
|
|
64
|
+
pattern,
|
|
65
|
+
regex: null,
|
|
66
|
+
isValid: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check if a command matches any dangerous pattern
|
|
73
|
+
* Fail-safe: if any pattern is invalid, treat all commands as dangerous
|
|
74
|
+
*
|
|
75
|
+
* @param command - The command string to check
|
|
76
|
+
* @param compiledPatterns - Array of compiled patterns
|
|
77
|
+
* @returns true if command is dangerous, false if safe
|
|
78
|
+
*/
|
|
79
|
+
export function isDangerous(command, compiledPatterns) {
|
|
80
|
+
// Empty command is safe
|
|
81
|
+
if (!command.trim()) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// Fail-safe: if any pattern failed to compile, treat as dangerous
|
|
85
|
+
const hasInvalidPattern = compiledPatterns.some((p) => !p.isValid);
|
|
86
|
+
if (hasInvalidPattern) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
// Check each valid pattern for a match
|
|
90
|
+
for (const { regex } of compiledPatterns) {
|
|
91
|
+
if (regex && regex.test(command)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ClaiError } from '../error/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* SafetyError is thrown when:
|
|
4
|
+
* - User aborts a dangerous command
|
|
5
|
+
* - Confirmation timeout expires
|
|
6
|
+
* - Safety check fails
|
|
7
|
+
*
|
|
8
|
+
* Exit code: 5
|
|
9
|
+
*/
|
|
10
|
+
export declare class SafetyError extends ClaiError {
|
|
11
|
+
constructor(message: string, cause?: Error);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compiled pattern with validity status
|
|
15
|
+
*/
|
|
16
|
+
export interface CompiledPattern {
|
|
17
|
+
pattern: string;
|
|
18
|
+
regex: RegExp | null;
|
|
19
|
+
isValid: boolean;
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/safety/types.ts
|
|
2
|
+
// Safety error type for abort/timeout scenarios
|
|
3
|
+
import { ClaiError } from '../error/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* SafetyError is thrown when:
|
|
6
|
+
* - User aborts a dangerous command
|
|
7
|
+
* - Confirmation timeout expires
|
|
8
|
+
* - Safety check fails
|
|
9
|
+
*
|
|
10
|
+
* Exit code: 5
|
|
11
|
+
*/
|
|
12
|
+
export class SafetyError extends ClaiError {
|
|
13
|
+
constructor(message, cause) {
|
|
14
|
+
super(message, 5, cause);
|
|
15
|
+
this.name = 'SafetyError';
|
|
16
|
+
Object.setPrototypeOf(this, SafetyError.prototype);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { InterruptError } from '../error/index.js';
|
|
2
|
+
// Interrupt flag set by signal handlers
|
|
3
|
+
let interrupted = false;
|
|
4
|
+
// Register signal handlers
|
|
5
|
+
export function registerSignalHandlers() {
|
|
6
|
+
process.on('SIGINT', handleInterrupt);
|
|
7
|
+
process.on('SIGTERM', handleInterrupt);
|
|
8
|
+
process.on('SIGPIPE', () => {
|
|
9
|
+
// Ignore broken pipe errors
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function handleInterrupt() {
|
|
13
|
+
interrupted = true;
|
|
14
|
+
process.exit(130);
|
|
15
|
+
}
|
|
16
|
+
// Check if interrupted and throw if so
|
|
17
|
+
export function checkInterrupt() {
|
|
18
|
+
if (interrupted) {
|
|
19
|
+
throw new InterruptError('Interrupted');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// TTY detection utilities
|
|
23
|
+
export function isTTY(stream) {
|
|
24
|
+
switch (stream) {
|
|
25
|
+
case 'stdin':
|
|
26
|
+
return process.stdin.isTTY === true;
|
|
27
|
+
case 'stdout':
|
|
28
|
+
return process.stdout.isTTY === true;
|
|
29
|
+
case 'stderr':
|
|
30
|
+
return process.stderr.isTTY === true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function isInteractive() {
|
|
34
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
35
|
+
}
|
package/dist/ui/App.d.ts
ADDED
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/ui/App.tsx
|
|
3
|
+
// Clean, minimal interactive UI
|
|
4
|
+
import { useState, useCallback } from 'react';
|
|
5
|
+
import { Box, useInput, useApp, Text } from 'ink';
|
|
6
|
+
import { UserAction } from './types.js';
|
|
7
|
+
import { useTimeout } from './hooks/useTimeout.js';
|
|
8
|
+
import { CommandDisplay } from './components/CommandDisplay.js';
|
|
9
|
+
import { DangerousWarning } from './components/DangerousWarning.js';
|
|
10
|
+
import { ActionPrompt } from './components/ActionPrompt.js';
|
|
11
|
+
export function App({ commands, isDangerous, config, onComplete, }) {
|
|
12
|
+
const { exit } = useApp();
|
|
13
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
const [selectedAction, setSelectedAction] = useState(UserAction.Execute);
|
|
15
|
+
const currentCommand = commands[selectedIndex] ?? '';
|
|
16
|
+
const hasMultiple = commands.length > 1;
|
|
17
|
+
// Note: Avoid console.error inside Ink components - it interferes with rendering
|
|
18
|
+
// Debug output is handled in renderUI before Ink mounts
|
|
19
|
+
// Handle completion
|
|
20
|
+
const handleComplete = useCallback((action) => {
|
|
21
|
+
onComplete(action, currentCommand);
|
|
22
|
+
exit();
|
|
23
|
+
}, [currentCommand, onComplete, exit]);
|
|
24
|
+
// Auto-abort timeout
|
|
25
|
+
useTimeout(() => handleComplete(UserAction.Abort), config.ui.promptTimeout > 0 ? config.ui.promptTimeout : null);
|
|
26
|
+
// Keyboard handling
|
|
27
|
+
useInput((input, key) => {
|
|
28
|
+
// Escape or Ctrl+C: cancel
|
|
29
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
30
|
+
handleComplete(UserAction.Abort);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Tab or arrows: cycle commands
|
|
34
|
+
if (hasMultiple && (key.tab || key.leftArrow || key.rightArrow)) {
|
|
35
|
+
const dir = key.leftArrow ? -1 : 1;
|
|
36
|
+
setSelectedIndex((i) => (i + dir + commands.length) % commands.length);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Up/Down: toggle action
|
|
40
|
+
if (key.upArrow || key.downArrow) {
|
|
41
|
+
setSelectedAction((a) => a === UserAction.Execute ? UserAction.Abort : UserAction.Execute);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Enter: confirm
|
|
45
|
+
if (key.return) {
|
|
46
|
+
handleComplete(selectedAction);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Number keys for direct selection
|
|
50
|
+
const num = parseInt(input, 10);
|
|
51
|
+
if (hasMultiple && num >= 1 && num <= commands.length) {
|
|
52
|
+
setSelectedIndex(num - 1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [_jsx(CommandDisplay, { command: currentCommand, currentIndex: selectedIndex, totalCommands: commands.length, isDangerous: isDangerous }), hasMultiple && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: next command" }) })), isDangerous && _jsx(DangerousWarning, {}), _jsx(ActionPrompt, { selectedAction: selectedAction, isDangerous: isDangerous })] }));
|
|
56
|
+
}
|
|
57
|
+
export default App;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { UserAction } from '../types.js';
|
|
3
|
+
export interface ActionPromptProps {
|
|
4
|
+
selectedAction: UserAction;
|
|
5
|
+
isDangerous: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function ActionPrompt({ selectedAction, isDangerous, }: ActionPromptProps): React.ReactElement;
|
|
8
|
+
export default ActionPrompt;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { UserAction } from '../types.js';
|
|
4
|
+
export function ActionPrompt({ selectedAction, isDangerous, }) {
|
|
5
|
+
const isExecute = selectedAction === UserAction.Execute;
|
|
6
|
+
const executeColor = isDangerous ? 'red' : 'green';
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: isExecute ? 'black' : executeColor, backgroundColor: isExecute ? executeColor : undefined, bold: isExecute, children: isExecute ? ' ▶ Run ' : ' Run ' }), _jsx(Text, { color: !isExecute ? 'black' : 'yellow', backgroundColor: !isExecute ? 'yellow' : undefined, bold: !isExecute, children: !isExecute ? ' ▶ Cancel ' : ' Cancel ' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 select Enter confirm Esc cancel" }) })] }));
|
|
8
|
+
}
|
|
9
|
+
export default ActionPrompt;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface CommandDisplayProps {
|
|
3
|
+
command: string;
|
|
4
|
+
currentIndex: number;
|
|
5
|
+
totalCommands: number;
|
|
6
|
+
isDangerous?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function CommandDisplay({ command, currentIndex, totalCommands, isDangerous, }: CommandDisplayProps): React.ReactElement;
|
|
9
|
+
export default CommandDisplay;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
4
|
+
import { formatCommand, formatCounter } from '../utils/formatCommand.js';
|
|
5
|
+
export function CommandDisplay({ command, currentIndex, totalCommands, isDangerous = false, }) {
|
|
6
|
+
const { width } = useTerminalSize();
|
|
7
|
+
const counterWidth = totalCommands > 1 ? 8 : 0;
|
|
8
|
+
const availableWidth = width - counterWidth - 4;
|
|
9
|
+
const formattedCommand = formatCommand(command, availableWidth);
|
|
10
|
+
const counter = formatCounter(currentIndex, totalCommands);
|
|
11
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isDangerous ? 'red' : 'green', bold: true, children: "$" }), _jsx(Text, { children: " " }), _jsx(Text, { color: isDangerous ? 'red' : 'cyan', bold: true, children: formattedCommand }), counter && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: counter })] }))] }));
|
|
12
|
+
}
|
|
13
|
+
export default CommandDisplay;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function DangerousWarning({ message = 'This command may modify or delete files', }) {
|
|
4
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0\uFE0F ", message] }) }));
|
|
5
|
+
}
|
|
6
|
+
export default DangerousWarning;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface SpinnerProps {
|
|
3
|
+
/** Message to display next to spinner */
|
|
4
|
+
message?: string;
|
|
5
|
+
/** Color of the spinner */
|
|
6
|
+
color?: string;
|
|
7
|
+
/** Animation speed in ms */
|
|
8
|
+
speed?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Animated spinner component for loading states
|
|
12
|
+
* Uses braille characters for smooth animation
|
|
13
|
+
*/
|
|
14
|
+
export declare function Spinner({ message, color, speed, }: SpinnerProps): React.ReactElement;
|
|
15
|
+
export default Spinner;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useAnimation } from '../hooks/useAnimation.js';
|
|
4
|
+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
|
5
|
+
import { SPINNER_FRAMES, SPINNER_DOTS, COLORS } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Animated spinner component for loading states
|
|
8
|
+
* Uses braille characters for smooth animation
|
|
9
|
+
*/
|
|
10
|
+
export function Spinner({ message = 'Processing...', color = COLORS.command, speed = 80, }) {
|
|
11
|
+
const { isWide } = useTerminalSize();
|
|
12
|
+
// Use fancier dots animation on wide terminals
|
|
13
|
+
const frames = isWide ? SPINNER_DOTS : SPINNER_FRAMES;
|
|
14
|
+
const frame = useAnimation(frames, speed);
|
|
15
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: frame }), _jsxs(Text, { color: color, children: [" ", message] })] }));
|
|
16
|
+
}
|
|
17
|
+
export default Spinner;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook that cycles through animation frames at a specified interval
|
|
3
|
+
*
|
|
4
|
+
* @param frames - Array of frame strings to cycle through
|
|
5
|
+
* @param interval - Time between frames in milliseconds
|
|
6
|
+
* @param enabled - Whether animation is active (default true)
|
|
7
|
+
* @returns Current frame string
|
|
8
|
+
*/
|
|
9
|
+
export declare function useAnimation(frames: readonly string[], interval?: number, enabled?: boolean): string;
|
|
10
|
+
/**
|
|
11
|
+
* Hook for a pulsing animation effect (alternates between two states)
|
|
12
|
+
*
|
|
13
|
+
* @param interval - Time between pulses in milliseconds
|
|
14
|
+
* @param enabled - Whether pulsing is active
|
|
15
|
+
* @returns Boolean indicating current pulse state
|
|
16
|
+
*/
|
|
17
|
+
export declare function usePulse(interval?: number, enabled?: boolean): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Hook for a typewriter-style reveal animation
|
|
20
|
+
*
|
|
21
|
+
* @param text - Full text to reveal
|
|
22
|
+
* @param speed - Characters per second
|
|
23
|
+
* @param enabled - Whether animation is active
|
|
24
|
+
* @returns Currently visible portion of text
|
|
25
|
+
*/
|
|
26
|
+
export declare function useTypewriter(text: string, speed?: number, enabled?: boolean): string;
|
|
27
|
+
export default useAnimation;
|