mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2026-03-10 08:27:45 +00:00
initial implementation
This commit is contained in:
140
src/extension.ts
140
src/extension.ts
@@ -94,6 +94,8 @@ class ClaudeChatProvider {
|
||||
private _backupRepoPath: string | undefined;
|
||||
private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = [];
|
||||
private _conversationsPath: string | undefined;
|
||||
private _permissionRequestsPath: string | undefined;
|
||||
private _permissionWatcher: vscode.FileSystemWatcher | undefined;
|
||||
private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = [];
|
||||
private _conversationStartTime: string | undefined;
|
||||
private _conversationIndex: Array<{
|
||||
@@ -117,6 +119,8 @@ class ClaudeChatProvider {
|
||||
// Initialize backup repository and conversations
|
||||
this._initializeBackupRepo();
|
||||
this._initializeConversations();
|
||||
this._initializeMCPConfig();
|
||||
this._initializePermissions();
|
||||
|
||||
// Load conversation index from workspace state
|
||||
this._conversationIndex = this._context.workspaceState.get('claude.conversationIndex', []);
|
||||
@@ -393,10 +397,19 @@ class ClaudeChatProvider {
|
||||
// Build command arguments with session management
|
||||
const args = [
|
||||
'-p',
|
||||
'--output-format', 'stream-json', '--verbose',
|
||||
'--dangerously-skip-permissions'
|
||||
'--output-format', 'stream-json', '--verbose'
|
||||
];
|
||||
|
||||
// Add MCP configuration for permissions
|
||||
const mcpConfigPath = this.getMCPConfigPath();
|
||||
if (mcpConfigPath) {
|
||||
args.push('--mcp-config', mcpConfigPath);
|
||||
args.push('--allowedTools', 'mcp__permissions__approval_prompt');
|
||||
args.push('--permission-prompt-tool', 'mcp__permissions__approval_prompt');
|
||||
}else{
|
||||
args.push('--dangerously-skip-permissions')
|
||||
}
|
||||
|
||||
// Add model selection if not using default
|
||||
if (this._selectedModel && this._selectedModel !== 'default') {
|
||||
args.push('--model', this._selectedModel);
|
||||
@@ -970,6 +983,129 @@ class ClaudeChatProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async _initializeMCPConfig(): Promise<void> {
|
||||
try {
|
||||
const storagePath = this._context.storageUri?.fsPath;
|
||||
if (!storagePath) {return;}
|
||||
|
||||
// Create MCP config directory
|
||||
const mcpConfigDir = path.join(storagePath, 'mcp');
|
||||
try {
|
||||
await vscode.workspace.fs.stat(vscode.Uri.file(mcpConfigDir));
|
||||
} catch {
|
||||
await vscode.workspace.fs.createDirectory(vscode.Uri.file(mcpConfigDir));
|
||||
console.log(`Created MCP config directory at: ${mcpConfigDir}`);
|
||||
}
|
||||
|
||||
// Create mcp-servers.json with correct path to compiled MCP permissions server
|
||||
const mcpConfigPath = path.join(mcpConfigDir, 'mcp-servers.json');
|
||||
const mcpPermissionsPath = path.join(this._extensionUri.fsPath, 'out', 'permissions', 'mcp-permissions.js');
|
||||
const permissionRequestsPath = path.join(storagePath, 'permission-requests');
|
||||
|
||||
const mcpConfig = {
|
||||
mcpServers: {
|
||||
permissions: {
|
||||
command: 'node',
|
||||
args: [mcpPermissionsPath],
|
||||
env: {
|
||||
CLAUDE_PERMISSIONS_PATH: permissionRequestsPath
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2));
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(mcpConfigPath), configContent);
|
||||
|
||||
console.log(`Created MCP config at: ${mcpConfigPath}`);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to initialize MCP config:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async _initializePermissions(): Promise<void> {
|
||||
try {
|
||||
const storagePath = this._context.storageUri?.fsPath;
|
||||
if (!storagePath) {return;}
|
||||
|
||||
// Create permission requests directory
|
||||
this._permissionRequestsPath = path.join(storagePath, 'permission-requests');
|
||||
try {
|
||||
await vscode.workspace.fs.stat(vscode.Uri.file(this._permissionRequestsPath));
|
||||
} catch {
|
||||
await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._permissionRequestsPath));
|
||||
console.log(`Created permission requests directory at: ${this._permissionRequestsPath}`);
|
||||
}
|
||||
|
||||
// Set up file watcher for *.request files
|
||||
this._permissionWatcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(this._permissionRequestsPath, '*.request')
|
||||
);
|
||||
|
||||
this._permissionWatcher.onDidCreate(async (uri) => {
|
||||
// Only handle file scheme URIs, ignore vscode-userdata scheme
|
||||
if (uri.scheme === 'file') {
|
||||
await this._handlePermissionRequest(uri);
|
||||
}
|
||||
});
|
||||
|
||||
this._disposables.push(this._permissionWatcher);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Failed to initialize permissions:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handlePermissionRequest(requestUri: vscode.Uri): Promise<void> {
|
||||
try {
|
||||
// Read the request file
|
||||
const content = await vscode.workspace.fs.readFile(requestUri);
|
||||
const request = JSON.parse(new TextDecoder().decode(content));
|
||||
|
||||
// Show permission dialog
|
||||
const approved = await this._showPermissionDialog(request);
|
||||
|
||||
// Write response file
|
||||
const responseFile = requestUri.fsPath.replace('.request', '.response');
|
||||
const response = {
|
||||
id: request.id,
|
||||
approved: approved,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const responseContent = new TextEncoder().encode(JSON.stringify(response));
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(responseFile), responseContent);
|
||||
|
||||
// Clean up request file
|
||||
await vscode.workspace.fs.delete(requestUri);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Failed to handle permission request:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async _showPermissionDialog(request: any): Promise<boolean> {
|
||||
const toolName = request.tool || 'Unknown Tool';
|
||||
const toolInput = JSON.stringify(request.input, null, 2);
|
||||
|
||||
const message = `Tool "${toolName}" is requesting permission to execute:\n\n${toolInput}\n\nDo you want to allow this?`;
|
||||
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
message,
|
||||
{ modal: true },
|
||||
'Allow',
|
||||
'Deny'
|
||||
);
|
||||
|
||||
return result === 'Allow';
|
||||
}
|
||||
|
||||
public getMCPConfigPath(): string | undefined {
|
||||
const storagePath = this._context.storageUri?.fsPath;
|
||||
if (!storagePath) {return undefined;}
|
||||
return path.join(storagePath, 'mcp', 'mcp-servers.json');
|
||||
}
|
||||
|
||||
private _sendAndSaveMessage(message: { type: string, data: any }): void {
|
||||
// Initialize conversation if this is the first message
|
||||
if (this._currentConversation.length === 0) {
|
||||
|
||||
118
src/permissions/mcp-permissions.ts
Normal file
118
src/permissions/mcp-permissions.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const server = new McpServer({
|
||||
name: "Claude Code Permissions MCP Server",
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
// Get permissions directory from environment
|
||||
const PERMISSIONS_PATH = process.env.CLAUDE_PERMISSIONS_PATH;
|
||||
if (!PERMISSIONS_PATH) {
|
||||
console.error("CLAUDE_PERMISSIONS_PATH environment variable not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function generateRequestId(): string {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
async function requestPermission(tool_name: string, input: any): Promise<boolean> {
|
||||
if (!PERMISSIONS_PATH) {
|
||||
console.error("Permissions path not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestId = generateRequestId();
|
||||
const requestFile = path.join(PERMISSIONS_PATH, `${requestId}.request`);
|
||||
const responseFile = path.join(PERMISSIONS_PATH, `${requestId}.response`);
|
||||
|
||||
// Write request file
|
||||
const request = {
|
||||
id: requestId,
|
||||
tool: tool_name,
|
||||
input: input,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
try {
|
||||
fs.writeFileSync(requestFile, JSON.stringify(request, null, 2));
|
||||
|
||||
// Poll for response file
|
||||
const maxWaitTime = 30000; // 30 seconds timeout
|
||||
const pollInterval = 100; // Check every 100ms
|
||||
let waitTime = 0;
|
||||
|
||||
while (waitTime < maxWaitTime) {
|
||||
if (fs.existsSync(responseFile)) {
|
||||
const responseContent = fs.readFileSync(responseFile, 'utf8');
|
||||
const response = JSON.parse(responseContent);
|
||||
|
||||
// Clean up response file
|
||||
fs.unlinkSync(responseFile);
|
||||
|
||||
return response.approved;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
waitTime += pollInterval;
|
||||
}
|
||||
|
||||
// Timeout - clean up request file and deny
|
||||
if (fs.existsSync(requestFile)) {
|
||||
fs.unlinkSync(requestFile);
|
||||
}
|
||||
|
||||
console.error(`Permission request ${requestId} timed out`);
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error requesting permission: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
server.tool(
|
||||
"approval_prompt",
|
||||
'Request user permission to execute a tool via VS Code dialog',
|
||||
{
|
||||
tool_name: z.string().describe("The name of the tool requesting permission"),
|
||||
input: z.object({}).passthrough().describe("The input for the tool"),
|
||||
tool_use_id: z.string().optional().describe("The unique tool use request ID"),
|
||||
},
|
||||
async ({ tool_name, input }) => {
|
||||
console.error(`Requesting permission for tool: ${tool_name}`);
|
||||
|
||||
const approved = await requestPermission(tool_name, input);
|
||||
|
||||
const behavior = approved ? "allow" : "deny";
|
||||
console.error(`Permission ${behavior}ed for tool: ${tool_name}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
behavior: behavior,
|
||||
updatedInput: input,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`Permissions MCP Server running on stdio`);
|
||||
console.error(`Using permissions directory: ${PERMISSIONS_PATH}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
10
src/permissions/mcp-servers.json
Normal file
10
src/permissions/mcp-servers.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"permissions": {
|
||||
"command": "node",
|
||||
"args": [
|
||||
"./out/permissions/mcp-permissions.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user