@webdevtoday/grok-cli
Version:
A sophisticated CLI tool for interacting with xAI Grok 4, featuring conversation history, file reference, custom commands, memory system, and genetic development workflows
727 lines • 29.1 kB
JavaScript
"use strict";
/**
* Enhanced Permission System for Grok CLI
* Provides interactive prompts, persistence, and fine-grained control
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnhancedPermissionManager = void 0;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const chalk_1 = __importDefault(require("chalk"));
/**
* Enhanced Permission Manager with interactive prompts and persistence
*/
class EnhancedPermissionManager {
constructor(config, cwd = process.cwd()) {
this.sessionDecisions = new Map();
this.persistentRules = [];
this.auditLog = [];
this.config = config;
this.cwd = cwd;
this.loadPersistentRules();
}
/**
* Check if a tool has permission to execute
*/
checkToolPermission(toolName, params) {
const permissions = this.config.permissions;
// Check disallowed tools first
if (permissions.disallowedTools?.includes(toolName)) {
this.auditDecision(toolName, params, 'deny', 'Tool in disallowed list');
return false;
}
// Check persistent rules
const rule = this.findMatchingRule(toolName, params);
if (rule) {
const allowed = rule.decision === 'allow';
this.auditDecision(toolName, params, rule.decision, rule.reason || 'Persistent rule');
return allowed;
}
// Check session decisions
const sessionKey = this.generateSessionKey(toolName, params);
const sessionDecision = this.sessionDecisions.get(sessionKey);
if (sessionDecision) {
const allowed = sessionDecision.decision === 'allow' || sessionDecision.decision === 'always';
this.auditDecision(toolName, params, allowed ? 'allow' : 'deny', 'Session decision');
return allowed;
}
// Check allowed tools (if specified)
if (permissions.allowedTools && permissions.allowedTools.length > 0) {
const allowed = permissions.allowedTools.includes(toolName);
this.auditDecision(toolName, params, allowed ? 'allow' : 'deny', 'Tool allowlist');
return allowed;
}
// Default to allowed if no restrictions
return true;
}
/**
* Request permission with interactive prompt
*/
async requestPermission(toolName, params) {
const permissions = this.config.permissions;
switch (permissions.mode) {
case 'auto':
this.auditDecision(toolName, params, 'allow', 'Auto-approve mode');
return true;
case 'full':
this.auditDecision(toolName, params, 'allow', 'Full access mode');
return true;
case 'plan':
await this.showPlannedExecution(toolName, params);
this.auditDecision(toolName, params, 'deny', 'Plan mode - execution blocked');
return false;
case 'ask':
default:
return await this.askUserPermission(toolName, params);
}
}
/**
* Show interactive permission prompt
*/
async askUserPermission(toolName, params) {
const readline = await Promise.resolve().then(() => __importStar(require('readline')));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log();
console.log(chalk_1.default.yellow('🔐 Permission Required'));
console.log(chalk_1.default.cyan('═══════════════════════'));
console.log();
console.log(chalk_1.default.blue('Tool:'), chalk_1.default.bold(toolName));
// Show tool description if available
const toolDescription = this.getToolDescription(toolName);
if (toolDescription) {
console.log(chalk_1.default.blue('Description:'), toolDescription);
}
// Show parameters with security assessment
console.log(chalk_1.default.blue('Parameters:'));
this.displayParameters(params);
// Show security assessment
const riskLevel = this.assessRisk(toolName, params);
console.log();
console.log(chalk_1.default.blue('Risk Level:'), this.formatRiskLevel(riskLevel));
// Show similar previous decisions
const similarDecisions = this.findSimilarDecisions(toolName, params);
if (similarDecisions.length > 0) {
console.log();
console.log(chalk_1.default.gray('Previous similar decisions:'));
similarDecisions.slice(0, 3).forEach(decision => {
const timeAgo = this.getTimeAgo(decision.timestamp);
console.log(` ${decision.decision === 'allow' ? chalk_1.default.green('✓') : chalk_1.default.red('✗')} ${timeAgo} - ${decision.reason || 'No reason'}`);
});
}
console.log();
console.log(chalk_1.default.gray('Options:'));
console.log(chalk_1.default.green(' [a] Allow once'));
console.log(chalk_1.default.green(' [A] Allow always for this tool'));
console.log(chalk_1.default.red(' [d] Deny once'));
console.log(chalk_1.default.red(' [D] Deny always for this tool'));
console.log(chalk_1.default.blue(' [i] Show more information'));
console.log(chalk_1.default.gray(' [q] Quit'));
console.log();
return new Promise((resolve) => {
const askChoice = () => {
rl.question(chalk_1.default.cyan('Choose [a/A/d/D/i/q]: '), async (answer) => {
const choice = answer.toLowerCase().trim();
switch (choice) {
case 'a':
this.recordSessionDecision(toolName, params, 'allow', 'User allowed once');
this.auditDecision(toolName, params, 'allow', 'User allowed once');
rl.close();
resolve(true);
break;
case 'allow':
this.recordSessionDecision(toolName, params, 'allow', 'User allowed once');
this.auditDecision(toolName, params, 'allow', 'User allowed once');
rl.close();
resolve(true);
break;
case 'A':
await this.createPersistentRule(toolName, params, 'allow', 'User allowed always');
this.auditDecision(toolName, params, 'allow', 'User allowed always');
rl.close();
resolve(true);
break;
case 'd':
this.recordSessionDecision(toolName, params, 'deny', 'User denied once');
this.auditDecision(toolName, params, 'deny', 'User denied once');
rl.close();
resolve(false);
break;
case 'deny':
this.recordSessionDecision(toolName, params, 'deny', 'User denied once');
this.auditDecision(toolName, params, 'deny', 'User denied once');
rl.close();
resolve(false);
break;
case 'D':
await this.createPersistentRule(toolName, params, 'deny', 'User denied always');
this.auditDecision(toolName, params, 'deny', 'User denied always');
rl.close();
resolve(false);
break;
case 'i':
await this.showDetailedInformation(toolName, params);
askChoice(); // Ask again after showing info
break;
case 'q':
case 'quit':
console.log(chalk_1.default.gray('Operation cancelled'));
this.auditDecision(toolName, params, 'deny', 'User cancelled');
rl.close();
resolve(false);
break;
default:
console.log(chalk_1.default.red('Invalid choice. Please enter a, A, d, D, i, or q'));
askChoice();
break;
}
});
};
askChoice();
});
}
/**
* Show planned execution without executing
*/
async showPlannedExecution(toolName, params) {
console.log();
console.log(chalk_1.default.blue('🔍 Planning Mode - Would Execute:'));
console.log(chalk_1.default.cyan('════════════════════════════════'));
console.log();
console.log(chalk_1.default.blue('Tool:'), chalk_1.default.bold(toolName));
const toolDescription = this.getToolDescription(toolName);
if (toolDescription) {
console.log(chalk_1.default.blue('Description:'), toolDescription);
}
console.log(chalk_1.default.blue('Parameters:'));
this.displayParameters(params);
const riskLevel = this.assessRisk(toolName, params);
console.log();
console.log(chalk_1.default.blue('Risk Level:'), this.formatRiskLevel(riskLevel));
// Show what the tool would likely do
const prediction = this.predictToolBehavior(toolName, params);
if (prediction) {
console.log();
console.log(chalk_1.default.blue('Predicted Actions:'));
console.log(chalk_1.default.gray(` ${prediction}`));
}
console.log();
console.log(chalk_1.default.yellow('⚠️ Execution blocked in planning mode'));
}
/**
* Display parameters with formatting and security assessment
*/
displayParameters(params) {
if (Object.keys(params).length === 0) {
console.log(chalk_1.default.gray(' (no parameters)'));
return;
}
for (const [key, value] of Object.entries(params)) {
const sensitivity = this.assessParameterSensitivity(key, value);
const formattedValue = this.formatParameterValue(value, sensitivity);
console.log(` ${chalk_1.default.cyan(key)}: ${formattedValue}`);
if (sensitivity === 'high') {
console.log(` ${chalk_1.default.red('⚠️ Sensitive parameter')}`);
}
}
}
/**
* Show detailed information about the tool and operation
*/
async showDetailedInformation(toolName, params) {
console.log();
console.log(chalk_1.default.blue('📋 Detailed Information'));
console.log(chalk_1.default.cyan('═══════════════════════'));
console.log();
// Tool information
console.log(chalk_1.default.blue('Tool Details:'));
console.log(` Name: ${chalk_1.default.bold(toolName)}`);
const description = this.getToolDescription(toolName);
if (description) {
console.log(` Description: ${description}`);
}
// Parameter analysis
console.log();
console.log(chalk_1.default.blue('Parameter Analysis:'));
for (const [key, value] of Object.entries(params)) {
const sensitivity = this.assessParameterSensitivity(key, value);
const analysis = this.analyzeParameter(key, value);
console.log(` ${chalk_1.default.cyan(key)}:`);
console.log(` Value: ${this.formatParameterValue(value, sensitivity)}`);
console.log(` Sensitivity: ${this.formatSensitivity(sensitivity)}`);
if (analysis) {
console.log(` Analysis: ${chalk_1.default.gray(analysis)}`);
}
}
// Security assessment
console.log();
console.log(chalk_1.default.blue('Security Assessment:'));
const riskLevel = this.assessRisk(toolName, params);
const riskReasons = this.getRiskReasons(toolName, params);
console.log(` Risk Level: ${this.formatRiskLevel(riskLevel)}`);
if (riskReasons.length > 0) {
console.log(` Risk Factors:`);
riskReasons.forEach(reason => {
console.log(` • ${chalk_1.default.gray(reason)}`);
});
}
// Tool capabilities
const capabilities = this.getToolCapabilities(toolName);
if (capabilities.length > 0) {
console.log();
console.log(chalk_1.default.blue('Tool Capabilities:'));
capabilities.forEach(capability => {
console.log(` • ${chalk_1.default.gray(capability)}`);
});
}
console.log();
console.log(chalk_1.default.gray('Press Enter to continue...'));
const readline = await Promise.resolve().then(() => __importStar(require('readline')));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question('', () => {
rl.close();
resolve();
});
});
}
/**
* Record session-level permission decision
*/
recordSessionDecision(toolName, params, decision, reason) {
const sessionKey = this.generateSessionKey(toolName, params);
this.sessionDecisions.set(sessionKey, {
toolName,
decision,
timestamp: new Date(),
params,
reason
});
}
/**
* Create persistent permission rule
*/
async createPersistentRule(toolName, _params, decision, reason) {
const rule = {
toolPattern: toolName,
decision,
scope: 'project', // Default to project scope
reason,
// No expiration by default
};
this.persistentRules.push(rule);
await this.savePersistentRules();
console.log(chalk_1.default.green(`✅ Created persistent rule: ${decision} ${toolName}`));
}
/**
* Load persistent rules from disk
*/
async loadPersistentRules() {
try {
const rulesPath = path_1.default.join(this.cwd, '.grok', 'permissions.json');
const rulesContent = await fs_1.promises.readFile(rulesPath, 'utf-8');
const rulesData = JSON.parse(rulesContent);
this.persistentRules = rulesData.rules || [];
// Clean up expired rules
const now = new Date();
this.persistentRules = this.persistentRules.filter(rule => !rule.expires || rule.expires > now);
}
catch {
// No persistent rules file exists yet, that's fine
}
}
/**
* Save persistent rules to disk
*/
async savePersistentRules() {
try {
const rulesDir = path_1.default.join(this.cwd, '.grok');
await fs_1.promises.mkdir(rulesDir, { recursive: true });
const rulesPath = path_1.default.join(rulesDir, 'permissions.json');
const rulesData = {
version: '1.0',
rules: this.persistentRules,
lastUpdated: new Date().toISOString()
};
await fs_1.promises.writeFile(rulesPath, JSON.stringify(rulesData, null, 2), 'utf-8');
}
catch (error) {
console.warn(chalk_1.default.yellow('⚠️ Failed to save permission rules:'), error instanceof Error ? error.message : String(error));
}
}
/**
* Find matching persistent rule
*/
findMatchingRule(toolName, params) {
return this.persistentRules.find(rule => {
// Check tool pattern match
if (rule.toolPattern !== toolName && !this.matchesPattern(toolName, rule.toolPattern)) {
return false;
}
// Check parameter patterns if specified
if (rule.paramPatterns) {
for (const [paramKey, pattern] of Object.entries(rule.paramPatterns)) {
const paramValue = params[paramKey];
if (!paramValue || !this.matchesPattern(String(paramValue), pattern)) {
return false;
}
}
}
// Check if rule is expired
if (rule.expires && rule.expires <= new Date()) {
return false;
}
return true;
}) || null;
}
/**
* Check if string matches a pattern (supports basic wildcards)
*/
matchesPattern(text, pattern) {
const regexPattern = pattern
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(text);
}
/**
* Generate session key for caching decisions
*/
generateSessionKey(toolName, params) {
const paramStr = JSON.stringify(params, Object.keys(params).sort());
return `${toolName}:${paramStr}`;
}
/**
* Assess risk level of tool execution
*/
assessRisk(toolName, params) {
let riskScore = 0;
// Tool-based risk assessment
const highRiskTools = ['bash', 'tmux', 'custom'];
const mediumRiskTools = ['write', 'read'];
if (highRiskTools.includes(toolName.toLowerCase())) {
riskScore += 3;
}
else if (mediumRiskTools.includes(toolName.toLowerCase())) {
riskScore += 1;
}
// Parameter-based risk assessment
for (const [key, value] of Object.entries(params)) {
const sensitivity = this.assessParameterSensitivity(key, value);
if (sensitivity === 'high') {
riskScore += 2;
}
else if (sensitivity === 'medium') {
riskScore += 1;
}
}
if (riskScore >= 4)
return 'high';
if (riskScore >= 2)
return 'medium';
return 'low';
}
/**
* Get reasons for risk assessment
*/
getRiskReasons(toolName, params) {
const reasons = [];
// Tool-specific risks
switch (toolName.toLowerCase()) {
case 'bash':
reasons.push('Can execute arbitrary system commands');
break;
case 'write':
reasons.push('Can modify files on disk');
break;
case 'tmux':
reasons.push('Can create and manage terminal sessions');
break;
case 'custom':
reasons.push('Executes user-defined commands');
break;
}
// Parameter-specific risks
for (const [key, value] of Object.entries(params)) {
if (key.includes('path') && typeof value === 'string') {
if (value.includes('..')) {
reasons.push('Path traversal detected');
}
if (value.startsWith('/')) {
reasons.push('Absolute path usage');
}
}
if (key.includes('command') && typeof value === 'string') {
if (value.includes('rm ') || value.includes('del ')) {
reasons.push('Potentially destructive command');
}
if (value.includes('sudo') || value.includes('su ')) {
reasons.push('Privilege escalation attempt');
}
}
}
return reasons;
}
/**
* Assess parameter sensitivity
*/
assessParameterSensitivity(key, value) {
const keyLower = key.toLowerCase();
// High sensitivity keywords
if (keyLower.includes('password') || keyLower.includes('secret') ||
keyLower.includes('token') || keyLower.includes('key') ||
keyLower.includes('credential')) {
return 'high';
}
// Medium sensitivity
if (keyLower.includes('path') || keyLower.includes('command') ||
keyLower.includes('script') || keyLower.includes('exec')) {
return 'medium';
}
// Check value patterns
if (typeof value === 'string') {
if (value.includes('/') && value.length > 10)
return 'medium';
if (value.includes('..'))
return 'high';
if (value.match(/^[A-Za-z0-9+/]{20,}={0,2}$/))
return 'high'; // Base64-like
}
return 'low';
}
/**
* Format parameter value for display
*/
formatParameterValue(value, sensitivity) {
if (sensitivity === 'high') {
return chalk_1.default.red('[REDACTED]');
}
const valueStr = typeof value === 'string' ? value : JSON.stringify(value);
if (valueStr.length > 100) {
return chalk_1.default.gray(valueStr.substring(0, 100) + '...');
}
return chalk_1.default.white(valueStr);
}
/**
* Format sensitivity level
*/
formatSensitivity(sensitivity) {
switch (sensitivity) {
case 'high': return chalk_1.default.red('High');
case 'medium': return chalk_1.default.yellow('Medium');
case 'low': return chalk_1.default.green('Low');
}
}
/**
* Format risk level with colors
*/
formatRiskLevel(risk) {
switch (risk) {
case 'high': return chalk_1.default.red.bold('🔴 HIGH');
case 'medium': return chalk_1.default.yellow.bold('🟡 MEDIUM');
case 'low': return chalk_1.default.green.bold('🟢 LOW');
}
}
/**
* Get tool description
*/
getToolDescription(toolName) {
const descriptions = {
'bash': 'Execute shell commands on the system',
'read': 'Read files from the filesystem',
'write': 'Write or modify files on the filesystem',
'tmux': 'Create and manage terminal sessions',
'custom': 'Execute user-defined custom commands',
};
return descriptions[toolName.toLowerCase()] || null;
}
/**
* Get tool capabilities
*/
getToolCapabilities(toolName) {
const capabilities = {
'bash': [
'Execute any shell command',
'Access system utilities',
'Modify system state',
'Access environment variables'
],
'read': [
'Read file contents',
'Access file metadata',
'Traverse directory structure'
],
'write': [
'Create new files',
'Modify existing files',
'Create directories',
'Change file permissions'
],
'tmux': [
'Create terminal sessions',
'Run background processes',
'Split terminal windows',
'Manage session state'
]
};
return capabilities[toolName.toLowerCase()] || [];
}
/**
* Predict tool behavior
*/
predictToolBehavior(toolName, params) {
switch (toolName.toLowerCase()) {
case 'bash':
const command = params['command'];
return command ? `Execute command: ${command}` : 'Execute shell command';
case 'read':
const readPath = params['file_path'];
return readPath ? `Read file: ${readPath}` : 'Read file';
case 'write':
const writePath = params['file_path'];
return writePath ? `Write to file: ${writePath}` : 'Write file';
default:
return null;
}
}
/**
* Analyze parameter
*/
analyzeParameter(key, value) {
if (key.includes('path') && typeof value === 'string') {
if (path_1.default.isAbsolute(value)) {
return 'Absolute path - has system-wide access';
}
if (value.includes('..')) {
return 'Contains parent directory references';
}
}
if (key.includes('command') && typeof value === 'string') {
const dangerousPatterns = ['rm ', 'del ', 'sudo ', 'su ', '>', '>>', '|'];
const found = dangerousPatterns.find(pattern => value.includes(pattern));
if (found) {
return `Contains potentially dangerous pattern: ${found}`;
}
}
return null;
}
/**
* Find similar previous decisions
*/
findSimilarDecisions(toolName, _params) {
const similar = [];
// Check session decisions
for (const decision of this.sessionDecisions.values()) {
if (decision.toolName === toolName) {
similar.push(decision);
}
}
return similar.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
/**
* Record audit entry
*/
auditDecision(toolName, params, decision, reason) {
const entry = {
timestamp: new Date(),
toolName,
params,
decision,
mode: this.config.permissions.mode,
reason,
user: process.env['USER'] || process.env['USERNAME'] || 'unknown',
sessionId: 'current' // TODO: Get actual session ID
};
this.auditLog.push(entry);
// Keep only last 1000 entries
if (this.auditLog.length > 1000) {
this.auditLog = this.auditLog.slice(-1000);
}
}
/**
* Get audit log
*/
getAuditLog() {
return [...this.auditLog];
}
/**
* Get permission statistics
*/
getPermissionStats() {
const allowed = this.auditLog.filter(entry => entry.decision === 'allow').length;
const denied = this.auditLog.filter(entry => entry.decision === 'deny').length;
return {
totalDecisions: this.auditLog.length,
allowedCount: allowed,
deniedCount: denied,
ruleCount: this.persistentRules.length,
sessionDecisions: this.sessionDecisions.size
};
}
/**
* Clear session decisions
*/
clearSessionDecisions() {
this.sessionDecisions.clear();
}
/**
* Get time ago string
*/
getTimeAgo(date) {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays > 0)
return `${diffDays}d ago`;
if (diffHours > 0)
return `${diffHours}h ago`;
if (diffMinutes > 0)
return `${diffMinutes}m ago`;
return 'just now';
}
}
exports.EnhancedPermissionManager = EnhancedPermissionManager;
//# sourceMappingURL=permissions.js.map