@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,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
|
+
}
|