grokcodecli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.claude/settings.local.json +32 -0
  2. package/README.md +1464 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +61 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/loader.d.ts +34 -0
  8. package/dist/commands/loader.d.ts.map +1 -0
  9. package/dist/commands/loader.js +192 -0
  10. package/dist/commands/loader.js.map +1 -0
  11. package/dist/config/manager.d.ts +21 -0
  12. package/dist/config/manager.d.ts.map +1 -0
  13. package/dist/config/manager.js +203 -0
  14. package/dist/config/manager.js.map +1 -0
  15. package/dist/conversation/chat.d.ts +50 -0
  16. package/dist/conversation/chat.d.ts.map +1 -0
  17. package/dist/conversation/chat.js +1145 -0
  18. package/dist/conversation/chat.js.map +1 -0
  19. package/dist/conversation/history.d.ts +24 -0
  20. package/dist/conversation/history.d.ts.map +1 -0
  21. package/dist/conversation/history.js +103 -0
  22. package/dist/conversation/history.js.map +1 -0
  23. package/dist/grok/client.d.ts +86 -0
  24. package/dist/grok/client.d.ts.map +1 -0
  25. package/dist/grok/client.js +106 -0
  26. package/dist/grok/client.js.map +1 -0
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/permissions/manager.d.ts +26 -0
  32. package/dist/permissions/manager.d.ts.map +1 -0
  33. package/dist/permissions/manager.js +170 -0
  34. package/dist/permissions/manager.js.map +1 -0
  35. package/dist/tools/bash.d.ts +8 -0
  36. package/dist/tools/bash.d.ts.map +1 -0
  37. package/dist/tools/bash.js +102 -0
  38. package/dist/tools/bash.js.map +1 -0
  39. package/dist/tools/edit.d.ts +9 -0
  40. package/dist/tools/edit.d.ts.map +1 -0
  41. package/dist/tools/edit.js +61 -0
  42. package/dist/tools/edit.js.map +1 -0
  43. package/dist/tools/glob.d.ts +7 -0
  44. package/dist/tools/glob.d.ts.map +1 -0
  45. package/dist/tools/glob.js +38 -0
  46. package/dist/tools/glob.js.map +1 -0
  47. package/dist/tools/grep.d.ts +8 -0
  48. package/dist/tools/grep.d.ts.map +1 -0
  49. package/dist/tools/grep.js +78 -0
  50. package/dist/tools/grep.js.map +1 -0
  51. package/dist/tools/read.d.ts +8 -0
  52. package/dist/tools/read.d.ts.map +1 -0
  53. package/dist/tools/read.js +96 -0
  54. package/dist/tools/read.js.map +1 -0
  55. package/dist/tools/registry.d.ts +42 -0
  56. package/dist/tools/registry.d.ts.map +1 -0
  57. package/dist/tools/registry.js +230 -0
  58. package/dist/tools/registry.js.map +1 -0
  59. package/dist/tools/webfetch.d.ts +10 -0
  60. package/dist/tools/webfetch.d.ts.map +1 -0
  61. package/dist/tools/webfetch.js +108 -0
  62. package/dist/tools/webfetch.js.map +1 -0
  63. package/dist/tools/websearch.d.ts +7 -0
  64. package/dist/tools/websearch.d.ts.map +1 -0
  65. package/dist/tools/websearch.js +180 -0
  66. package/dist/tools/websearch.js.map +1 -0
  67. package/dist/tools/write.d.ts +7 -0
  68. package/dist/tools/write.d.ts.map +1 -0
  69. package/dist/tools/write.js +80 -0
  70. package/dist/tools/write.js.map +1 -0
  71. package/dist/utils/security.d.ts +36 -0
  72. package/dist/utils/security.d.ts.map +1 -0
  73. package/dist/utils/security.js +227 -0
  74. package/dist/utils/security.js.map +1 -0
  75. package/dist/utils/ui.d.ts +49 -0
  76. package/dist/utils/ui.d.ts.map +1 -0
  77. package/dist/utils/ui.js +302 -0
  78. package/dist/utils/ui.js.map +1 -0
  79. package/package.json +45 -0
  80. package/src/cli.ts +68 -0
  81. package/src/commands/loader.ts +244 -0
  82. package/src/config/manager.ts +239 -0
  83. package/src/conversation/chat.ts +1294 -0
  84. package/src/conversation/history.ts +131 -0
  85. package/src/grok/client.ts +192 -0
  86. package/src/index.ts +8 -0
  87. package/src/permissions/manager.ts +208 -0
  88. package/src/tools/bash.ts +119 -0
  89. package/src/tools/edit.ts +73 -0
  90. package/src/tools/glob.ts +49 -0
  91. package/src/tools/grep.ts +96 -0
  92. package/src/tools/read.ts +116 -0
  93. package/src/tools/registry.ts +248 -0
  94. package/src/tools/webfetch.ts +127 -0
  95. package/src/tools/websearch.ts +219 -0
  96. package/src/tools/write.ts +94 -0
  97. package/src/utils/security.ts +259 -0
  98. package/src/utils/ui.ts +382 -0
  99. package/tsconfig.json +22 -0
@@ -0,0 +1,219 @@
1
+ import { ToolResult } from './registry.js';
2
+ import chalk from 'chalk';
3
+
4
+ export interface WebSearchToolParams {
5
+ query: string;
6
+ num_results?: number;
7
+ }
8
+
9
+ interface SearchResult {
10
+ title: string;
11
+ url: string;
12
+ snippet: string;
13
+ }
14
+
15
+ // DuckDuckGo HTML search (no API key needed)
16
+ async function searchDuckDuckGo(query: string, numResults: number = 10): Promise<SearchResult[]> {
17
+ const results: SearchResult[] = [];
18
+
19
+ try {
20
+ // Use DuckDuckGo HTML version
21
+ const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
22
+
23
+ const response = await fetch(searchUrl, {
24
+ headers: {
25
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
26
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
27
+ 'Accept-Language': 'en-US,en;q=0.5',
28
+ },
29
+ });
30
+
31
+ if (!response.ok) {
32
+ throw new Error(`Search failed: ${response.status} ${response.statusText}`);
33
+ }
34
+
35
+ const html = await response.text();
36
+
37
+ // Parse results from HTML
38
+ // DuckDuckGo HTML results are in <a class="result__a"> tags
39
+ const resultPattern = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi;
40
+ const snippetPattern = /<a[^>]*class="result__snippet"[^>]*>([^<]*(?:<[^>]*>[^<]*)*)<\/a>/gi;
41
+
42
+ // Alternative: Parse result blocks
43
+ const resultBlocks = html.split('<div class="result results_links results_links_deep web-result');
44
+
45
+ for (let i = 1; i < resultBlocks.length && results.length < numResults; i++) {
46
+ const block = resultBlocks[i];
47
+
48
+ // Extract URL
49
+ const urlMatch = block.match(/href="([^"]+)"/);
50
+ // Extract title
51
+ const titleMatch = block.match(/class="result__a"[^>]*>([^<]+)</);
52
+ // Extract snippet
53
+ const snippetMatch = block.match(/class="result__snippet"[^>]*>([^<]+)/);
54
+
55
+ if (urlMatch && titleMatch) {
56
+ let url = urlMatch[1];
57
+ // DuckDuckGo uses redirect URLs, try to extract actual URL
58
+ if (url.includes('uddg=')) {
59
+ const actualUrl = url.match(/uddg=([^&]+)/);
60
+ if (actualUrl) {
61
+ url = decodeURIComponent(actualUrl[1]);
62
+ }
63
+ }
64
+
65
+ results.push({
66
+ title: decodeHtmlEntities(titleMatch[1].trim()),
67
+ url: url,
68
+ snippet: snippetMatch ? decodeHtmlEntities(snippetMatch[1].trim()) : '',
69
+ });
70
+ }
71
+ }
72
+
73
+ // If no results found with block parsing, try simpler approach
74
+ if (results.length === 0) {
75
+ const links = html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi);
76
+ for (const match of links) {
77
+ if (results.length >= numResults) break;
78
+ let url = match[1];
79
+ if (url.includes('uddg=')) {
80
+ const actualUrl = url.match(/uddg=([^&]+)/);
81
+ if (actualUrl) {
82
+ url = decodeURIComponent(actualUrl[1]);
83
+ }
84
+ }
85
+ results.push({
86
+ title: decodeHtmlEntities(match[2].trim()),
87
+ url: url,
88
+ snippet: '',
89
+ });
90
+ }
91
+ }
92
+
93
+ } catch (error) {
94
+ // Fallback: try alternative search
95
+ return await searchWithAlternative(query, numResults);
96
+ }
97
+
98
+ return results;
99
+ }
100
+
101
+ // Fallback using a simple web scraping approach
102
+ async function searchWithAlternative(query: string, numResults: number): Promise<SearchResult[]> {
103
+ const results: SearchResult[] = [];
104
+
105
+ try {
106
+ // Try using DuckDuckGo Lite
107
+ const searchUrl = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
108
+
109
+ const response = await fetch(searchUrl, {
110
+ headers: {
111
+ 'User-Agent': 'Mozilla/5.0 (compatible; GrokCode/1.0)',
112
+ 'Accept': 'text/html',
113
+ },
114
+ });
115
+
116
+ if (!response.ok) {
117
+ return results;
118
+ }
119
+
120
+ const html = await response.text();
121
+
122
+ // Parse lite version results
123
+ const linkMatches = html.matchAll(/<a[^>]*rel="nofollow"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/gi);
124
+
125
+ for (const match of linkMatches) {
126
+ if (results.length >= numResults) break;
127
+ const url = match[1];
128
+ const title = match[2];
129
+
130
+ // Skip DuckDuckGo internal links
131
+ if (url.startsWith('/') || url.includes('duckduckgo.com')) continue;
132
+
133
+ results.push({
134
+ title: decodeHtmlEntities(title.trim()),
135
+ url: url,
136
+ snippet: '',
137
+ });
138
+ }
139
+ } catch {
140
+ // Return empty results if all methods fail
141
+ }
142
+
143
+ return results;
144
+ }
145
+
146
+ function decodeHtmlEntities(text: string): string {
147
+ return text
148
+ .replace(/&amp;/g, '&')
149
+ .replace(/&lt;/g, '<')
150
+ .replace(/&gt;/g, '>')
151
+ .replace(/&quot;/g, '"')
152
+ .replace(/&#39;/g, "'")
153
+ .replace(/&nbsp;/g, ' ')
154
+ .replace(/&#x27;/g, "'")
155
+ .replace(/&#x2F;/g, '/')
156
+ .replace(/&apos;/g, "'");
157
+ }
158
+
159
+ export async function webSearchTool(params: WebSearchToolParams): Promise<ToolResult> {
160
+ try {
161
+ const query = params.query.trim();
162
+ const numResults = Math.min(params.num_results ?? 10, 20);
163
+
164
+ if (!query) {
165
+ return {
166
+ success: false,
167
+ output: '',
168
+ error: 'Search query cannot be empty',
169
+ };
170
+ }
171
+
172
+ if (query.length > 500) {
173
+ return {
174
+ success: false,
175
+ output: '',
176
+ error: 'Search query too long (maximum 500 characters)',
177
+ };
178
+ }
179
+
180
+ const results = await searchDuckDuckGo(query, numResults);
181
+
182
+ if (results.length === 0) {
183
+ return {
184
+ success: true,
185
+ output: `${chalk.yellow('No results found for:')} "${query}"\n\nTry:\n • Using different keywords\n • Checking spelling\n • Using fewer, more general terms`,
186
+ };
187
+ }
188
+
189
+ let output = `${chalk.cyan('🔍 Search Results for:')} "${query}"\n`;
190
+ output += `${chalk.gray(`Found ${results.length} results`)}\n\n`;
191
+
192
+ results.forEach((result, index) => {
193
+ output += `${chalk.bold(`${index + 1}. ${result.title}`)}\n`;
194
+ output += ` ${chalk.blue(result.url)}\n`;
195
+ if (result.snippet) {
196
+ output += ` ${chalk.gray(result.snippet)}\n`;
197
+ }
198
+ output += '\n';
199
+ });
200
+
201
+ // Add sources section like Claude Code
202
+ output += `${chalk.cyan('Sources:')}\n`;
203
+ results.forEach((result) => {
204
+ output += ` • [${result.title}](${result.url})\n`;
205
+ });
206
+
207
+ return {
208
+ success: true,
209
+ output,
210
+ };
211
+ } catch (error) {
212
+ const err = error as Error;
213
+ return {
214
+ success: false,
215
+ output: '',
216
+ error: `Search failed: ${err.message}`,
217
+ };
218
+ }
219
+ }
@@ -0,0 +1,94 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { ToolResult } from './registry.js';
4
+ import { validatePath } from '../utils/security.js';
5
+ import chalk from 'chalk';
6
+
7
+ export interface WriteToolParams {
8
+ file_path: string;
9
+ content: string;
10
+ }
11
+
12
+ // Maximum file size to write (50MB)
13
+ const MAX_CONTENT_SIZE = 50 * 1024 * 1024;
14
+
15
+ export async function writeTool(params: WriteToolParams): Promise<ToolResult> {
16
+ try {
17
+ const filePath = path.resolve(params.file_path);
18
+
19
+ // Security validation
20
+ const security = validatePath(filePath);
21
+ if (!security.allowed) {
22
+ return {
23
+ success: false,
24
+ output: '',
25
+ error: `Security: ${security.reason}`,
26
+ };
27
+ }
28
+
29
+ // Check content size
30
+ if (params.content.length > MAX_CONTENT_SIZE) {
31
+ return {
32
+ success: false,
33
+ output: '',
34
+ error: `Content too large (${(params.content.length / 1024 / 1024).toFixed(1)}MB). Maximum is ${MAX_CONTENT_SIZE / 1024 / 1024}MB.`,
35
+ };
36
+ }
37
+
38
+ // Check if file exists (for backup/warning)
39
+ let wasExisting = false;
40
+ let previousSize = 0;
41
+ try {
42
+ const stats = await fs.stat(filePath);
43
+ wasExisting = true;
44
+ previousSize = stats.size;
45
+ } catch {
46
+ // File doesn't exist, that's fine
47
+ }
48
+
49
+ // Ensure directory exists
50
+ const dir = path.dirname(filePath);
51
+ await fs.mkdir(dir, { recursive: true });
52
+
53
+ // Write the file
54
+ await fs.writeFile(filePath, params.content, 'utf-8');
55
+
56
+ const lines = params.content.split('\n').length;
57
+ const size = params.content.length;
58
+
59
+ let output = `${chalk.green('✓')} File written: ${filePath}\n`;
60
+ output += ` ${chalk.gray('Lines:')} ${lines}\n`;
61
+ output += ` ${chalk.gray('Size:')} ${formatSize(size)}`;
62
+
63
+ if (wasExisting) {
64
+ const diff = size - previousSize;
65
+ const diffStr = diff >= 0 ? `+${formatSize(diff)}` : `-${formatSize(Math.abs(diff))}`;
66
+ output += ` (${diffStr} from previous)`;
67
+ }
68
+
69
+ // Security warning if applicable
70
+ if (security.severity === 'medium') {
71
+ output = chalk.yellow(`⚠️ ${security.suggestion}\n`) + output;
72
+ }
73
+
74
+ return {
75
+ success: true,
76
+ output,
77
+ };
78
+ } catch (error) {
79
+ const err = error as NodeJS.ErrnoException;
80
+ if (err.code === 'EACCES') {
81
+ return { success: false, output: '', error: `Permission denied: ${params.file_path}` };
82
+ }
83
+ if (err.code === 'ENOSPC') {
84
+ return { success: false, output: '', error: 'No space left on device' };
85
+ }
86
+ return { success: false, output: '', error: `Error writing file: ${err.message}` };
87
+ }
88
+ }
89
+
90
+ function formatSize(bytes: number): string {
91
+ if (bytes < 1024) return `${bytes}B`;
92
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
93
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
94
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Security Utilities
3
+ *
4
+ * Provides input validation, path traversal prevention, and security checks.
5
+ */
6
+
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+
10
+ // Dangerous patterns that should be flagged
11
+ const DANGEROUS_COMMANDS = [
12
+ /rm\s+-rf\s+[\/~]/i, // rm -rf with root or home
13
+ /rm\s+-rf\s+\*/, // rm -rf *
14
+ />\s*\/dev\/sd/, // Writing to disk devices
15
+ /mkfs\./, // Formatting filesystems
16
+ /dd\s+if=.*of=\/dev/, // dd to device
17
+ /chmod\s+-R\s+777/, // Recursive 777
18
+ /curl.*\|\s*sh/, // Curl pipe to shell
19
+ /wget.*\|\s*sh/, // Wget pipe to shell
20
+ /curl.*\|\s*bash/,
21
+ /wget.*\|\s*bash/,
22
+ /:(){.*};:/, // Fork bomb pattern
23
+ />\s*\/etc\//, // Writing to /etc
24
+ />\s*\/boot\//, // Writing to /boot
25
+ />\s*\/proc\//, // Writing to /proc
26
+ />\s*\/sys\//, // Writing to /sys
27
+ ];
28
+
29
+ // Sensitive file patterns
30
+ const SENSITIVE_FILES = [
31
+ /\.env$/,
32
+ /\.env\./,
33
+ /credentials/i,
34
+ /secret/i,
35
+ /password/i,
36
+ /private_key/,
37
+ /\.pem$/,
38
+ /\.key$/,
39
+ /id_rsa/,
40
+ /id_ed25519/,
41
+ /\.ssh\/config/,
42
+ /\.netrc/,
43
+ /\.npmrc/,
44
+ /\.pypirc/,
45
+ ];
46
+
47
+ // Blocked paths (never allow access)
48
+ const BLOCKED_PATHS = [
49
+ '/etc/passwd',
50
+ '/etc/shadow',
51
+ '/etc/sudoers',
52
+ '/etc/ssh/',
53
+ '/root/.ssh/',
54
+ '/proc/',
55
+ '/sys/',
56
+ '/dev/',
57
+ ];
58
+
59
+ export interface SecurityCheckResult {
60
+ allowed: boolean;
61
+ reason?: string;
62
+ severity?: 'low' | 'medium' | 'high' | 'critical';
63
+ suggestion?: string;
64
+ }
65
+
66
+ /**
67
+ * Validate a file path for security issues
68
+ */
69
+ export function validatePath(filePath: string, allowedRoots?: string[]): SecurityCheckResult {
70
+ // Resolve to absolute path
71
+ const resolved = path.resolve(filePath);
72
+ const normalized = path.normalize(resolved);
73
+
74
+ // Check for path traversal attempts
75
+ if (filePath.includes('..')) {
76
+ // Allow if the resolved path is still within allowed roots
77
+ if (allowedRoots && allowedRoots.length > 0) {
78
+ const isWithinRoots = allowedRoots.some(root =>
79
+ normalized.startsWith(path.resolve(root))
80
+ );
81
+ if (!isWithinRoots) {
82
+ return {
83
+ allowed: false,
84
+ reason: 'Path traversal detected - path escapes allowed directories',
85
+ severity: 'high',
86
+ };
87
+ }
88
+ }
89
+ }
90
+
91
+ // Check blocked paths
92
+ for (const blocked of BLOCKED_PATHS) {
93
+ if (normalized.startsWith(blocked) || normalized === blocked.slice(0, -1)) {
94
+ return {
95
+ allowed: false,
96
+ reason: `Access to ${blocked} is blocked for security`,
97
+ severity: 'critical',
98
+ };
99
+ }
100
+ }
101
+
102
+ // Check for sensitive files (warning only)
103
+ for (const pattern of SENSITIVE_FILES) {
104
+ if (pattern.test(normalized)) {
105
+ return {
106
+ allowed: true, // Allow but warn
107
+ reason: 'This appears to be a sensitive file',
108
+ severity: 'medium',
109
+ suggestion: 'Be careful with files containing credentials or secrets',
110
+ };
111
+ }
112
+ }
113
+
114
+ return { allowed: true };
115
+ }
116
+
117
+ /**
118
+ * Validate a bash command for dangerous patterns
119
+ */
120
+ export function validateCommand(command: string): SecurityCheckResult {
121
+ // Check for dangerous patterns
122
+ for (const pattern of DANGEROUS_COMMANDS) {
123
+ if (pattern.test(command)) {
124
+ return {
125
+ allowed: false,
126
+ reason: 'Command matches a dangerous pattern',
127
+ severity: 'critical',
128
+ suggestion: 'This command could cause system damage. Please review carefully.',
129
+ };
130
+ }
131
+ }
132
+
133
+ // Check for commands that modify critical paths
134
+ if (/>\s*(\/etc|\/boot|\/root|\/var\/log)/i.test(command)) {
135
+ return {
136
+ allowed: false,
137
+ reason: 'Writing to system directories is blocked',
138
+ severity: 'high',
139
+ };
140
+ }
141
+
142
+ // Check for privilege escalation
143
+ if (/sudo\s+-s|su\s+-|sudo\s+su/.test(command)) {
144
+ return {
145
+ allowed: true,
146
+ reason: 'Command requires elevated privileges',
147
+ severity: 'medium',
148
+ suggestion: 'This command will prompt for sudo password',
149
+ };
150
+ }
151
+
152
+ return { allowed: true };
153
+ }
154
+
155
+ /**
156
+ * Validate a URL for security issues
157
+ */
158
+ export function validateUrl(url: string): SecurityCheckResult {
159
+ try {
160
+ const parsed = new URL(url);
161
+
162
+ // Only allow http/https
163
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
164
+ return {
165
+ allowed: false,
166
+ reason: `Protocol ${parsed.protocol} is not allowed`,
167
+ severity: 'medium',
168
+ };
169
+ }
170
+
171
+ // Block localhost/internal IPs (SSRF prevention)
172
+ const hostname = parsed.hostname.toLowerCase();
173
+ const blockedHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
174
+ const internalPatterns = [
175
+ /^10\./,
176
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
177
+ /^192\.168\./,
178
+ /^169\.254\./,
179
+ ];
180
+
181
+ if (blockedHosts.includes(hostname)) {
182
+ return {
183
+ allowed: false,
184
+ reason: 'Localhost access is blocked',
185
+ severity: 'high',
186
+ };
187
+ }
188
+
189
+ for (const pattern of internalPatterns) {
190
+ if (pattern.test(hostname)) {
191
+ return {
192
+ allowed: false,
193
+ reason: 'Internal network access is blocked',
194
+ severity: 'high',
195
+ };
196
+ }
197
+ }
198
+
199
+ return { allowed: true };
200
+ } catch {
201
+ return {
202
+ allowed: false,
203
+ reason: 'Invalid URL format',
204
+ severity: 'low',
205
+ };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Sanitize output for display (prevent terminal escape sequences)
211
+ */
212
+ export function sanitizeOutput(output: string): string {
213
+ // Remove potentially dangerous escape sequences but keep basic formatting
214
+ return output
215
+ // Remove cursor manipulation
216
+ .replace(/\x1b\[\d*[ABCDJK]/g, '')
217
+ // Remove window title changes
218
+ .replace(/\x1b\]0;[^\x07]*\x07/g, '')
219
+ // Remove scrolling region changes
220
+ .replace(/\x1b\[\d*;\d*r/g, '')
221
+ // Keep basic colors (safe)
222
+ .replace(/\x1b\[(\d+;)*\d*[mK]/g, (match) => {
223
+ // Only allow color codes 0-107
224
+ const nums = match.slice(2, -1).split(';').map(Number);
225
+ if (nums.every(n => n >= 0 && n <= 107)) {
226
+ return match;
227
+ }
228
+ return '';
229
+ });
230
+ }
231
+
232
+ /**
233
+ * Get safe environment variables (filter out sensitive ones)
234
+ */
235
+ export function getSafeEnv(): Record<string, string> {
236
+ const sensitive = [
237
+ 'API_KEY', 'SECRET', 'TOKEN', 'PASSWORD', 'CREDENTIAL',
238
+ 'AWS_', 'GITHUB_TOKEN', 'NPM_TOKEN', 'PRIVATE_KEY',
239
+ ];
240
+
241
+ const result: Record<string, string> = {};
242
+
243
+ for (const [key, value] of Object.entries(process.env)) {
244
+ const isSensitive = sensitive.some(s => key.toUpperCase().includes(s));
245
+ if (!isSensitive && value !== undefined) {
246
+ result[key] = value;
247
+ }
248
+ }
249
+
250
+ return result;
251
+ }
252
+
253
+ /**
254
+ * Generate a safe temporary path
255
+ */
256
+ export function getSafeTempPath(filename: string): string {
257
+ const sanitized = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
258
+ return path.join(os.tmpdir(), 'grokcode', sanitized);
259
+ }