@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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/ai/index.d.ts +24 -0
  4. package/dist/ai/index.js +78 -0
  5. package/dist/ai/mock.d.ts +17 -0
  6. package/dist/ai/mock.js +49 -0
  7. package/dist/ai/parser.d.ts +14 -0
  8. package/dist/ai/parser.js +109 -0
  9. package/dist/ai/prompt.d.ts +12 -0
  10. package/dist/ai/prompt.js +76 -0
  11. package/dist/ai/providers/index.d.ts +1 -0
  12. package/dist/ai/providers/index.js +2 -0
  13. package/dist/ai/providers/openrouter.d.ts +31 -0
  14. package/dist/ai/providers/openrouter.js +142 -0
  15. package/dist/ai/types.d.ts +46 -0
  16. package/dist/ai/types.js +15 -0
  17. package/dist/cli/index.d.ts +19 -0
  18. package/dist/cli/index.js +71 -0
  19. package/dist/config/index.d.ts +12 -0
  20. package/dist/config/index.js +363 -0
  21. package/dist/config/types.d.ts +76 -0
  22. package/dist/config/types.js +40 -0
  23. package/dist/context/directory.d.ts +18 -0
  24. package/dist/context/directory.js +71 -0
  25. package/dist/context/history.d.ts +16 -0
  26. package/dist/context/history.js +89 -0
  27. package/dist/context/index.d.ts +24 -0
  28. package/dist/context/index.js +61 -0
  29. package/dist/context/redaction.d.ts +14 -0
  30. package/dist/context/redaction.js +57 -0
  31. package/dist/context/stdin.d.ts +13 -0
  32. package/dist/context/stdin.js +86 -0
  33. package/dist/context/system.d.ts +11 -0
  34. package/dist/context/system.js +56 -0
  35. package/dist/context/types.d.ts +31 -0
  36. package/dist/context/types.js +10 -0
  37. package/dist/error/index.d.ts +30 -0
  38. package/dist/error/index.js +50 -0
  39. package/dist/logging/file-logger.d.ts +12 -0
  40. package/dist/logging/file-logger.js +66 -0
  41. package/dist/logging/index.d.ts +15 -0
  42. package/dist/logging/index.js +33 -0
  43. package/dist/logging/logger.d.ts +15 -0
  44. package/dist/logging/logger.js +60 -0
  45. package/dist/main.d.ts +2 -0
  46. package/dist/main.js +192 -0
  47. package/dist/output/execute.d.ts +30 -0
  48. package/dist/output/execute.js +144 -0
  49. package/dist/output/index.d.ts +4 -0
  50. package/dist/output/index.js +7 -0
  51. package/dist/output/types.d.ts +48 -0
  52. package/dist/output/types.js +34 -0
  53. package/dist/output/validate.d.ts +23 -0
  54. package/dist/output/validate.js +42 -0
  55. package/dist/safety/index.d.ts +34 -0
  56. package/dist/safety/index.js +59 -0
  57. package/dist/safety/patterns.d.ts +23 -0
  58. package/dist/safety/patterns.js +96 -0
  59. package/dist/safety/types.d.ts +20 -0
  60. package/dist/safety/types.js +18 -0
  61. package/dist/signals/index.d.ts +4 -0
  62. package/dist/signals/index.js +35 -0
  63. package/dist/ui/App.d.ts +4 -0
  64. package/dist/ui/App.js +57 -0
  65. package/dist/ui/components/ActionPrompt.d.ts +8 -0
  66. package/dist/ui/components/ActionPrompt.js +9 -0
  67. package/dist/ui/components/CommandDisplay.d.ts +9 -0
  68. package/dist/ui/components/CommandDisplay.js +13 -0
  69. package/dist/ui/components/DangerousWarning.d.ts +6 -0
  70. package/dist/ui/components/DangerousWarning.js +6 -0
  71. package/dist/ui/components/Spinner.d.ts +15 -0
  72. package/dist/ui/components/Spinner.js +17 -0
  73. package/dist/ui/hooks/useAnimation.d.ts +27 -0
  74. package/dist/ui/hooks/useAnimation.js +85 -0
  75. package/dist/ui/hooks/useTerminalSize.d.ts +12 -0
  76. package/dist/ui/hooks/useTerminalSize.js +56 -0
  77. package/dist/ui/hooks/useTimeout.d.ts +9 -0
  78. package/dist/ui/hooks/useTimeout.js +31 -0
  79. package/dist/ui/index.d.ts +25 -0
  80. package/dist/ui/index.js +80 -0
  81. package/dist/ui/output.d.ts +20 -0
  82. package/dist/ui/output.js +56 -0
  83. package/dist/ui/spinner.d.ts +13 -0
  84. package/dist/ui/spinner.js +58 -0
  85. package/dist/ui/types.d.ts +105 -0
  86. package/dist/ui/types.js +60 -0
  87. package/dist/ui/utils/formatCommand.d.ts +50 -0
  88. package/dist/ui/utils/formatCommand.js +113 -0
  89. package/package.json +68 -0
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ export declare const ProviderConfigSchema: z.ZodObject<{
3
+ apiKey: z.ZodOptional<z.ZodString>;
4
+ apiKeyEnv: z.ZodOptional<z.ZodString>;
5
+ model: z.ZodOptional<z.ZodString>;
6
+ endpoint: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
9
+ export declare const FileConfigSchema: z.ZodObject<{
10
+ provider: z.ZodOptional<z.ZodObject<{
11
+ default: z.ZodDefault<z.ZodString>;
12
+ fallback: z.ZodDefault<z.ZodArray<z.ZodString>>;
13
+ }, z.core.$strip>>;
14
+ context: z.ZodOptional<z.ZodObject<{
15
+ maxFiles: z.ZodDefault<z.ZodNumber>;
16
+ maxHistory: z.ZodDefault<z.ZodNumber>;
17
+ redactPaths: z.ZodDefault<z.ZodBoolean>;
18
+ redactUsername: z.ZodDefault<z.ZodBoolean>;
19
+ }, z.core.$strip>>;
20
+ safety: z.ZodOptional<z.ZodObject<{
21
+ confirmDangerous: z.ZodDefault<z.ZodBoolean>;
22
+ dangerousPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
23
+ }, z.core.$strip>>;
24
+ ui: z.ZodOptional<z.ZodObject<{
25
+ color: z.ZodDefault<z.ZodEnum<{
26
+ auto: "auto";
27
+ always: "always";
28
+ never: "never";
29
+ }>>;
30
+ debugLogFile: z.ZodOptional<z.ZodString>;
31
+ interactive: z.ZodDefault<z.ZodBoolean>;
32
+ promptTimeout: z.ZodDefault<z.ZodNumber>;
33
+ }, z.core.$strip>>;
34
+ providers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
35
+ apiKey: z.ZodOptional<z.ZodString>;
36
+ apiKeyEnv: z.ZodOptional<z.ZodString>;
37
+ model: z.ZodOptional<z.ZodString>;
38
+ endpoint: z.ZodOptional<z.ZodString>;
39
+ }, z.core.$strip>>>;
40
+ }, z.core.$strip>;
41
+ export type FileConfig = z.infer<typeof FileConfigSchema>;
42
+ export interface Config {
43
+ provider: {
44
+ default: string;
45
+ fallback: string[];
46
+ };
47
+ context: {
48
+ maxFiles: number;
49
+ maxHistory: number;
50
+ redactPaths: boolean;
51
+ redactUsername: boolean;
52
+ };
53
+ safety: {
54
+ confirmDangerous: boolean;
55
+ dangerousPatterns: string[];
56
+ };
57
+ ui: {
58
+ color: 'auto' | 'always' | 'never';
59
+ debugLogFile?: string;
60
+ interactive: boolean;
61
+ numOptions: number;
62
+ promptTimeout: number;
63
+ };
64
+ providers: Record<string, ProviderConfig>;
65
+ model?: string;
66
+ providerName?: string;
67
+ quiet: boolean;
68
+ verbose: number;
69
+ force: boolean;
70
+ dryRun: boolean;
71
+ contextFile?: string;
72
+ offline: boolean;
73
+ debug: boolean;
74
+ debugFile?: string;
75
+ instruction: string;
76
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+ // Provider configuration
3
+ export const ProviderConfigSchema = z.object({
4
+ apiKey: z.string().optional(),
5
+ apiKeyEnv: z.string().optional(),
6
+ model: z.string().optional(),
7
+ endpoint: z.string().optional(),
8
+ });
9
+ // File config schema (from TOML files) - all nested objects are optional for partial configs
10
+ export const FileConfigSchema = z.object({
11
+ provider: z
12
+ .object({
13
+ default: z.string().default('openrouter'),
14
+ fallback: z.array(z.string()).default([]),
15
+ })
16
+ .optional(),
17
+ context: z
18
+ .object({
19
+ maxFiles: z.number().int().min(1).max(100).default(10),
20
+ maxHistory: z.number().int().min(0).max(50).default(3),
21
+ redactPaths: z.boolean().default(false),
22
+ redactUsername: z.boolean().default(false),
23
+ })
24
+ .optional(),
25
+ safety: z
26
+ .object({
27
+ confirmDangerous: z.boolean().default(true),
28
+ dangerousPatterns: z.array(z.string()).default([]),
29
+ })
30
+ .optional(),
31
+ ui: z
32
+ .object({
33
+ color: z.enum(['auto', 'always', 'never']).default('auto'),
34
+ debugLogFile: z.string().optional(),
35
+ interactive: z.boolean().default(false),
36
+ promptTimeout: z.number().int().min(0).max(300000).default(30000),
37
+ })
38
+ .optional(),
39
+ providers: z.record(z.string(), ProviderConfigSchema).default({}),
40
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Get current working directory
3
+ * Throws ContextError if cwd cannot be determined (fatal)
4
+ */
5
+ export declare function getCwd(): string;
6
+ /**
7
+ * Scan current directory and return list of files
8
+ * - Limited to maxFiles (sorted alphabetically)
9
+ * - Paths truncated to 80 chars (using basename if too long)
10
+ * - Optionally redacted for privacy
11
+ *
12
+ * On read error, returns empty array (non-fatal)
13
+ */
14
+ export declare function scanDirectory(maxFiles: number, redactPaths: boolean): string[];
15
+ /**
16
+ * Get redacted current working directory
17
+ */
18
+ export declare function getRedactedCwd(redactPaths: boolean): string;
@@ -0,0 +1,71 @@
1
+ // Directory context gathering
2
+ import { readdirSync } from 'fs';
3
+ import { basename } from 'path';
4
+ import { ContextError } from './types.js';
5
+ import { redactPath } from './redaction.js';
6
+ const PATH_TRUNCATE_LENGTH = 80;
7
+ /**
8
+ * Get current working directory
9
+ * Throws ContextError if cwd cannot be determined (fatal)
10
+ */
11
+ export function getCwd() {
12
+ try {
13
+ return process.cwd();
14
+ }
15
+ catch (err) {
16
+ throw new ContextError(`Failed to get current working directory: ${err instanceof Error ? err.message : String(err)}`, 1);
17
+ }
18
+ }
19
+ /**
20
+ * Truncate a path to maximum length
21
+ * If path exceeds limit, return basename only
22
+ */
23
+ function truncatePath(path, maxLength = PATH_TRUNCATE_LENGTH) {
24
+ if (path.length <= maxLength) {
25
+ return path;
26
+ }
27
+ return basename(path);
28
+ }
29
+ /**
30
+ * Scan current directory and return list of files
31
+ * - Limited to maxFiles (sorted alphabetically)
32
+ * - Paths truncated to 80 chars (using basename if too long)
33
+ * - Optionally redacted for privacy
34
+ *
35
+ * On read error, returns empty array (non-fatal)
36
+ */
37
+ export function scanDirectory(maxFiles, redactPaths) {
38
+ const cwd = getCwd();
39
+ try {
40
+ // Read directory entries
41
+ const entries = readdirSync(cwd, { withFileTypes: true });
42
+ // Sort by name and take first maxFiles
43
+ const sorted = entries
44
+ .map((entry) => entry.name)
45
+ .sort((a, b) => a.localeCompare(b))
46
+ .slice(0, maxFiles);
47
+ // Apply truncation and redaction
48
+ return sorted.map((fileName) => {
49
+ const fullPath = `${cwd}/${fileName}`;
50
+ let result = truncatePath(fullPath);
51
+ if (redactPaths) {
52
+ result = redactPath(result);
53
+ }
54
+ return result;
55
+ });
56
+ }
57
+ catch {
58
+ // Non-fatal: return empty list on error
59
+ return [];
60
+ }
61
+ }
62
+ /**
63
+ * Get redacted current working directory
64
+ */
65
+ export function getRedactedCwd(redactPaths) {
66
+ const cwd = getCwd();
67
+ if (redactPaths) {
68
+ return redactPath(cwd);
69
+ }
70
+ return cwd;
71
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Get shell history
3
+ * Returns last N commands based on detected shell
4
+ *
5
+ * - bash: ~/.bash_history
6
+ * - zsh: ~/.zsh_history
7
+ * - fish: ~/.local/share/fish/fish_history (read raw lines)
8
+ * - other: empty array
9
+ *
10
+ * On error or unsupported shell, returns empty array (non-fatal)
11
+ */
12
+ export declare function getShellHistory(maxHistory: number): string[];
13
+ /**
14
+ * Get detected shell name (for testing/debugging)
15
+ */
16
+ export declare function getDetectedShell(): string | null;
@@ -0,0 +1,89 @@
1
+ // Shell history gathering
2
+ import { readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ // History file paths by shell
6
+ const HISTORY_PATHS = {
7
+ bash: ['.bash_history'],
8
+ zsh: ['.zsh_history'],
9
+ fish: ['.local/share/fish/fish_history'],
10
+ sh: ['.sh_history'],
11
+ };
12
+ /**
13
+ * Detect shell from SHELL environment variable
14
+ * Returns shell name (e.g., 'bash', 'zsh') or null if unknown
15
+ */
16
+ function detectShell() {
17
+ const shell = process.env.SHELL || '';
18
+ if (!shell)
19
+ return null;
20
+ const shellName = shell.split('/').pop() || '';
21
+ // Check if we support this shell
22
+ if (HISTORY_PATHS[shellName]) {
23
+ return shellName;
24
+ }
25
+ return null;
26
+ }
27
+ /**
28
+ * Get history file path for a given shell
29
+ */
30
+ function getHistoryPath(shell) {
31
+ const home = homedir();
32
+ if (!home)
33
+ return null;
34
+ const paths = HISTORY_PATHS[shell];
35
+ if (!paths)
36
+ return null;
37
+ return join(home, paths[0]);
38
+ }
39
+ /**
40
+ * Read last N lines from a file efficiently
41
+ * Uses tail-like approach: seek to end minus ~4KB, read, take last N lines
42
+ */
43
+ function readLastLines(filePath, numLines) {
44
+ try {
45
+ // Read entire file - for most history files this is fine
46
+ // For very large files, we'd use a streaming approach
47
+ const content = readFileSync(filePath, 'utf-8');
48
+ const lines = content.split('\n');
49
+ // Filter out empty lines and get last N
50
+ const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
51
+ return nonEmptyLines.slice(-numLines);
52
+ }
53
+ catch {
54
+ // File doesn't exist or can't be read
55
+ return [];
56
+ }
57
+ }
58
+ /**
59
+ * Get shell history
60
+ * Returns last N commands based on detected shell
61
+ *
62
+ * - bash: ~/.bash_history
63
+ * - zsh: ~/.zsh_history
64
+ * - fish: ~/.local/share/fish/fish_history (read raw lines)
65
+ * - other: empty array
66
+ *
67
+ * On error or unsupported shell, returns empty array (non-fatal)
68
+ */
69
+ export function getShellHistory(maxHistory) {
70
+ const shell = detectShell();
71
+ if (!shell) {
72
+ return [];
73
+ }
74
+ const historyPath = getHistoryPath(shell);
75
+ if (!historyPath) {
76
+ return [];
77
+ }
78
+ const lines = readLastLines(historyPath, maxHistory);
79
+ // For fish shell, we read raw lines (matching Rust behavior)
80
+ // Fish uses format: "- cmd: <command>" but we return raw lines
81
+ // This matches the PRD: "The Rust code does NOT parse fish's - cmd: ... format"
82
+ return lines;
83
+ }
84
+ /**
85
+ * Get detected shell name (for testing/debugging)
86
+ */
87
+ export function getDetectedShell() {
88
+ return detectShell();
89
+ }
@@ -0,0 +1,24 @@
1
+ import { Config } from '../config/types.js';
2
+ import { ContextData } from './types.js';
3
+ export { ContextError } from './types.js';
4
+ export type { ContextData, SystemInfo } from './types.js';
5
+ export { getSystemInfo, clearSystemCache } from './system.js';
6
+ export { getCwd, scanDirectory, getRedactedCwd } from './directory.js';
7
+ export { getShellHistory, getDetectedShell } from './history.js';
8
+ export { readStdin, hasPipedStdin } from './stdin.js';
9
+ export { redactPath, redactUsername, redactEnvVars } from './redaction.js';
10
+ /**
11
+ * Gather all context information for the AI prompt
12
+ *
13
+ * Components:
14
+ * - System info (cached): OS, shell, user, memory
15
+ * - Current working directory (fatal if fails)
16
+ * - Directory files (sorted, truncated, redacted)
17
+ * - Shell history (last N commands)
18
+ * - Stdin content (only if piped, max 10KB)
19
+ *
20
+ * @param config - Runtime configuration with context settings
21
+ * @returns ContextData with all gathered information
22
+ * @throws ContextError if CWD cannot be determined (fatal)
23
+ */
24
+ export declare function gatherContext(config: Config): Promise<ContextData>;
@@ -0,0 +1,61 @@
1
+ // Context gathering module - main entry point
2
+ // Gathers system info, directory context, shell history, and stdin
3
+ import { ContextError } from './types.js';
4
+ import { getSystemInfo } from './system.js';
5
+ import { getCwd, scanDirectory } from './directory.js';
6
+ import { getShellHistory } from './history.js';
7
+ import { readStdin } from './stdin.js';
8
+ import { redactPath } from './redaction.js';
9
+ // Re-export types and functions
10
+ export { ContextError } from './types.js';
11
+ export { getSystemInfo, clearSystemCache } from './system.js';
12
+ export { getCwd, scanDirectory, getRedactedCwd } from './directory.js';
13
+ export { getShellHistory, getDetectedShell } from './history.js';
14
+ export { readStdin, hasPipedStdin } from './stdin.js';
15
+ export { redactPath, redactUsername, redactEnvVars } from './redaction.js';
16
+ /**
17
+ * Gather all context information for the AI prompt
18
+ *
19
+ * Components:
20
+ * - System info (cached): OS, shell, user, memory
21
+ * - Current working directory (fatal if fails)
22
+ * - Directory files (sorted, truncated, redacted)
23
+ * - Shell history (last N commands)
24
+ * - Stdin content (only if piped, max 10KB)
25
+ *
26
+ * @param config - Runtime configuration with context settings
27
+ * @returns ContextData with all gathered information
28
+ * @throws ContextError if CWD cannot be determined (fatal)
29
+ */
30
+ export async function gatherContext(config) {
31
+ const { maxFiles, maxHistory, redactPaths, redactUsername } = config.context;
32
+ // 1. System info (cached, non-fatal)
33
+ const system = getSystemInfo(redactUsername);
34
+ // 2. CWD (fatal if fails)
35
+ let cwd;
36
+ try {
37
+ cwd = getCwd();
38
+ if (redactPaths) {
39
+ cwd = redactPath(cwd);
40
+ }
41
+ }
42
+ catch (err) {
43
+ if (err instanceof ContextError) {
44
+ throw err;
45
+ }
46
+ throw new ContextError(`Failed to gather context: ${err instanceof Error ? err.message : String(err)}`, 1);
47
+ }
48
+ // 3. Directory files (non-fatal, empty on error)
49
+ const files = scanDirectory(maxFiles, redactPaths);
50
+ // 4. Shell history (non-fatal, empty on error)
51
+ const history = getShellHistory(maxHistory);
52
+ // 5. Stdin (only if piped, non-fatal)
53
+ const stdin = await readStdin();
54
+ return {
55
+ system,
56
+ cwd,
57
+ files,
58
+ history,
59
+ stdin,
60
+ };
61
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Redact sensitive path information from a string
3
+ * Replaces home directory patterns with [REDACTED]
4
+ */
5
+ export declare function redactPath(path: string): string;
6
+ /**
7
+ * Redact username
8
+ */
9
+ export declare function redactUsername(_username: string): string;
10
+ /**
11
+ * Redact environment variables in a string
12
+ * Replaces $VAR and ${VAR} patterns
13
+ */
14
+ export declare function redactEnvVars(text: string, envVars: string[]): string;
@@ -0,0 +1,57 @@
1
+ // Path and username redaction helpers for privacy
2
+ import { homedir } from 'os';
3
+ const REDACTED = '[REDACTED]';
4
+ /**
5
+ * Redact sensitive path information from a string
6
+ * Replaces home directory patterns with [REDACTED]
7
+ */
8
+ export function redactPath(path) {
9
+ const home = homedir();
10
+ if (!home)
11
+ return path;
12
+ // Normalize path separators for Windows
13
+ const normalizedPath = path.replace(/\\/g, '/');
14
+ const normalizedHome = home.replace(/\\/g, '/');
15
+ // Extract username from home path (e.g., /home/username or /Users/username)
16
+ const homeParts = normalizedHome.split('/');
17
+ const username = homeParts[homeParts.length - 1] || '';
18
+ let redacted = normalizedPath;
19
+ // Replace full home directory path
20
+ if (normalizedHome) {
21
+ const escapedHome = normalizedHome.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
+ redacted = redacted.replace(new RegExp(escapedHome, 'g'), REDACTED);
23
+ }
24
+ // Replace ~ (home shorthand)
25
+ redacted = redacted.replace(/^~\//, `${REDACTED}/`);
26
+ redacted = redacted.replace(/^~$/, REDACTED);
27
+ // Replace /home/username/ pattern
28
+ if (username) {
29
+ redacted = redacted.replace(new RegExp(`/home/${username}/`, 'g'), `${REDACTED}/`);
30
+ redacted = redacted.replace(new RegExp(`/Users/${username}/`, 'g'), `${REDACTED}/`);
31
+ }
32
+ // Restore original path separators for Windows
33
+ if (process.platform === 'win32') {
34
+ redacted = redacted.replace(/\//g, '\\');
35
+ }
36
+ return redacted;
37
+ }
38
+ /**
39
+ * Redact username
40
+ */
41
+ export function redactUsername(_username) {
42
+ return REDACTED;
43
+ }
44
+ /**
45
+ * Redact environment variables in a string
46
+ * Replaces $VAR and ${VAR} patterns
47
+ */
48
+ export function redactEnvVars(text, envVars) {
49
+ let redacted = text;
50
+ for (const envVar of envVars) {
51
+ // Replace ${VAR} pattern
52
+ redacted = redacted.replace(new RegExp(`\\$\\{${envVar}\\}`, 'g'), REDACTED);
53
+ // Replace $VAR pattern (word boundary)
54
+ redacted = redacted.replace(new RegExp(`\\$${envVar}\\b`, 'g'), REDACTED);
55
+ }
56
+ return redacted;
57
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Read stdin if it's not a TTY (i.e., data is being piped)
3
+ * Returns up to 10KB of content as UTF-8 string
4
+ * Returns undefined if stdin is a TTY or empty
5
+ *
6
+ * UTF-8 invalid sequences are replaced with replacement character (lossy decode)
7
+ */
8
+ export declare function readStdin(): Promise<string | undefined>;
9
+ /**
10
+ * Check if stdin has piped data available
11
+ * This is synchronous - useful for checking without reading
12
+ */
13
+ export declare function hasPipedStdin(): boolean;
@@ -0,0 +1,86 @@
1
+ // Stdin input gathering for piped content
2
+ const MAX_STDIN_BYTES = 10 * 1024; // 10 KB
3
+ /**
4
+ * Read stdin if it's not a TTY (i.e., data is being piped)
5
+ * Returns up to 10KB of content as UTF-8 string
6
+ * Returns undefined if stdin is a TTY or empty
7
+ *
8
+ * UTF-8 invalid sequences are replaced with replacement character (lossy decode)
9
+ */
10
+ export async function readStdin() {
11
+ // Check if stdin is a TTY
12
+ if (process.stdin.isTTY) {
13
+ return undefined;
14
+ }
15
+ // Check if stdin is readable (has data or will have data)
16
+ // In test environments, stdin might not be a TTY but also not have any data
17
+ if (!process.stdin.readable) {
18
+ return undefined;
19
+ }
20
+ // Check if stdin is already at EOF
21
+ if (process.stdin.readableEnded) {
22
+ return undefined;
23
+ }
24
+ const chunks = [];
25
+ let totalBytes = 0;
26
+ let hasReceivedData = false;
27
+ let endReceived = false;
28
+ return new Promise((resolve) => {
29
+ // Set a timeout to prevent hanging if no data comes
30
+ const timeout = setTimeout(() => {
31
+ if (!hasReceivedData) {
32
+ // No data received within timeout, assume empty stdin
33
+ cleanup();
34
+ resolve(undefined);
35
+ }
36
+ }, 100);
37
+ function cleanup() {
38
+ clearTimeout(timeout);
39
+ process.stdin.removeAllListeners('data');
40
+ process.stdin.removeAllListeners('end');
41
+ process.stdin.removeAllListeners('error');
42
+ }
43
+ process.stdin.on('data', (chunk) => {
44
+ hasReceivedData = true;
45
+ const remainingBytes = MAX_STDIN_BYTES - totalBytes;
46
+ if (remainingBytes <= 0) {
47
+ // We've already reached the limit, ignore further data
48
+ return;
49
+ }
50
+ // Only take what we need up to the limit
51
+ const bytesToTake = Math.min(chunk.length, remainingBytes);
52
+ chunks.push(chunk.subarray(0, bytesToTake));
53
+ totalBytes += bytesToTake;
54
+ });
55
+ process.stdin.on('end', () => {
56
+ if (endReceived)
57
+ return;
58
+ endReceived = true;
59
+ cleanup();
60
+ if (chunks.length === 0) {
61
+ resolve(undefined);
62
+ return;
63
+ }
64
+ const buffer = Buffer.concat(chunks);
65
+ // Convert to UTF-8 with lossy replacement for invalid sequences
66
+ const content = buffer.toString('utf8');
67
+ resolve(content.length > 0 ? content : undefined);
68
+ });
69
+ process.stdin.on('error', () => {
70
+ cleanup();
71
+ // Non-fatal: return undefined on error
72
+ resolve(undefined);
73
+ });
74
+ // Resume stdin if paused
75
+ if (process.stdin.isPaused()) {
76
+ process.stdin.resume();
77
+ }
78
+ });
79
+ }
80
+ /**
81
+ * Check if stdin has piped data available
82
+ * This is synchronous - useful for checking without reading
83
+ */
84
+ export function hasPipedStdin() {
85
+ return !process.stdin.isTTY && process.stdin.readable;
86
+ }
@@ -0,0 +1,11 @@
1
+ import { SystemInfo } from './types.js';
2
+ /**
3
+ * Get system information
4
+ * Results are cached for the process lifetime
5
+ * @param redactUser - Whether to redact the username
6
+ */
7
+ export declare function getSystemInfo(redactUser?: boolean): SystemInfo;
8
+ /**
9
+ * Clear the system info cache (useful for testing)
10
+ */
11
+ export declare function clearSystemCache(): void;
@@ -0,0 +1,56 @@
1
+ // System information gathering - cached per process
2
+ import { type, release, totalmem } from 'os';
3
+ import { redactUsername } from './redaction.js';
4
+ // Cached system info to avoid repeated OS calls
5
+ let cachedSystemInfo = null;
6
+ let cachedWithRedaction = false;
7
+ /**
8
+ * Get shell name from SHELL environment variable
9
+ * Returns basename (e.g., '/bin/bash' -> 'bash')
10
+ */
11
+ function getShell() {
12
+ const shell = process.env.SHELL || '';
13
+ if (!shell)
14
+ return 'unknown';
15
+ // Extract basename from path
16
+ const parts = shell.split('/');
17
+ return parts[parts.length - 1] || 'unknown';
18
+ }
19
+ /**
20
+ * Get username from environment variables
21
+ */
22
+ function getUsername() {
23
+ return process.env.USER || process.env.USERNAME || 'unknown';
24
+ }
25
+ /**
26
+ * Get system information
27
+ * Results are cached for the process lifetime
28
+ * @param redactUser - Whether to redact the username
29
+ */
30
+ export function getSystemInfo(redactUser = false) {
31
+ // Return cached value if available and redaction matches
32
+ if (cachedSystemInfo && cachedWithRedaction === redactUser) {
33
+ return cachedSystemInfo;
34
+ }
35
+ const rawUsername = getUsername();
36
+ const user = redactUser ? redactUsername(rawUsername) : rawUsername;
37
+ const info = {
38
+ osName: type(),
39
+ osVersion: release(),
40
+ architecture: process.arch,
41
+ shell: getShell(),
42
+ user,
43
+ totalMemoryMb: Math.floor(totalmem() / (1024 * 1024)),
44
+ };
45
+ // Cache the result
46
+ cachedSystemInfo = info;
47
+ cachedWithRedaction = redactUser;
48
+ return info;
49
+ }
50
+ /**
51
+ * Clear the system info cache (useful for testing)
52
+ */
53
+ export function clearSystemCache() {
54
+ cachedSystemInfo = null;
55
+ cachedWithRedaction = false;
56
+ }
@@ -0,0 +1,31 @@
1
+ import { ClaiError } from '../error/index.js';
2
+ export interface SystemInfo {
3
+ /** Operating system name (e.g., 'Linux', 'Darwin', 'Windows_NT') */
4
+ osName: string;
5
+ /** OS version string */
6
+ osVersion: string;
7
+ /** CPU architecture (e.g., 'x64', 'arm64') */
8
+ architecture: string;
9
+ /** Shell name (e.g., 'bash', 'zsh', 'fish') */
10
+ shell: string;
11
+ /** Username (may be redacted) */
12
+ user: string;
13
+ /** Total system memory in MB */
14
+ totalMemoryMb: number;
15
+ }
16
+ export interface ContextData {
17
+ /** System information */
18
+ system: SystemInfo;
19
+ /** Current working directory (may be redacted) */
20
+ cwd: string;
21
+ /** List of files in current directory (sorted, truncated, redacted) */
22
+ files: string[];
23
+ /** Recent shell history entries */
24
+ history: string[];
25
+ /** Piped stdin content (only when stdin is not a TTY) */
26
+ stdin?: string;
27
+ }
28
+ /** Error class for context gathering failures */
29
+ export declare class ContextError extends ClaiError {
30
+ constructor(message: string, code?: number);
31
+ }