Ver Fonte

feat: migrate shell script functionality to TypeScript client #24

This commit removes the need for handle-issue-ts.sh by migrating all
its functionality into the TypeScript client.

Changes:
- Added repository-manager.ts for Git operations (clone, pull)
- Added gogs-helper.ts for fetching SSH URLs from Gogs API
- Added environment setup (HOME, PATH, XDG directories) in index.ts
- Added automatic repository management in index.ts
- Updated commands.json to call TypeScript client directly
- Updated documentation (CLAUDE.md, client/README.md)

The shell script is now obsolete. The TypeScript client handles:
- Environment variable setup
- Repository cloning/pulling
- Git URL fetching from Gogs API
- Issue/comment processing
- All existing features from the shell script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude há 9 meses atrás
pai
commit
9b06e0818f
6 ficheiros alterados com 333 adições e 2 exclusões
  1. 6 1
      CLAUDE.md
  2. 3 1
      client/README.md
  3. 70 0
      client/src/gogs-helper.ts
  4. 50 0
      client/src/index.ts
  5. 164 0
      client/src/repository-manager.ts
  6. 40 0
      commands.json

+ 6 - 1
CLAUDE.md

@@ -310,6 +310,8 @@ The `client/` directory contains a TypeScript-based Claude Agent SDK client that
 **Key Files**:
 - `src/claude-client.ts` - Main Claude Agent SDK client
 - `src/config.ts` - Configuration management
+- `src/repository-manager.ts` - Git repository operations (clone, pull, environment setup)
+- `src/gogs-helper.ts` - Gogs API helper functions
 - `src/types.ts` - TypeScript type definitions
 - `src/index.ts` - CLI entry point
 - `dist/` - Compiled JavaScript (auto-generated by `npm run build`)
@@ -321,7 +323,7 @@ npm install
 npm run build
 ```
 
-**Integration**: The TypeScript client is called from `scripts/handle-issue-ts.sh`, which is configured in `commands.json` for `issues` and `issue_comment` events.
+**Integration**: The TypeScript client is called directly from `commands.json` for `issues` and `issue_comment` events. No shell script wrapper is needed.
 
 **Features**:
 - Integrates with gogs-mcp MCP server for Gogs API access
@@ -330,6 +332,9 @@ npm run build
 - Environment-specific configuration via `.env`
 - **Per-repository MCP configuration support** - Different repositories can use different MCP servers
 - **Dynamic tool discovery** - Automatically discovers available tools from all configured MCP servers
+- **Automatic repository management** - Clones repositories on demand and pulls latest changes
+- **Environment setup** - Handles HOME, PATH, and XDG directory configuration
+- **Git URL from Gogs API** - Fetches SSH URL dynamically from Gogs instead of hardcoding
 
 See `client/README.md` for detailed documentation.
 

+ 3 - 1
client/README.md

@@ -25,6 +25,8 @@ client/
 │   ├── config.ts              # Configuration management
 │   ├── mcpConfigManager.ts    # MCP configuration loader
 │   ├── toolDiscovery.ts       # Dynamic tool discovery
+│   ├── repository-manager.ts  # Git repository operations (clone, pull)
+│   ├── gogs-helper.ts         # Gogs API helper functions
 │   ├── types.ts               # TypeScript type definitions
 │   └── index.ts               # CLI entry point
 ├── dist/                      # Compiled JavaScript (generated)
@@ -94,7 +96,7 @@ node dist/index.js <owner> <repo> <pusher> <issue_number> <issue_title> <issue_b
 
 ### Webhook Integration
 
-The client is integrated with the webhook server via `scripts/handle-issue-ts.sh`, which is called from `commands.json` for issue and issue_comment events.
+The client is called directly from `commands.json` for issue and issue_comment events. The TypeScript client handles all environment setup, repository management, and issue processing.
 
 ## Configuration
 

+ 70 - 0
client/src/gogs-helper.ts

@@ -0,0 +1,70 @@
+/**
+ * Gogs Helper - Utilities for interacting with Gogs API
+ *
+ * This module provides functions to interact with Gogs API directly
+ * when MCP tools are not available (e.g., before Agent SDK initialization)
+ */
+
+import { execSync } from 'child_process';
+import { existsSync, readFileSync } from 'fs';
+import { join } from 'path';
+
+/**
+ * Get Gogs API configuration from environment or MCP config
+ */
+function getGogsConfig(): { baseUrl: string; token: string } {
+  // Try to load from environment first
+  const token = process.env.GOGS_ACCESS_TOKEN || process.env.GOGS_TOKEN;
+  const baseUrl = process.env.GOGS_BASE_URL || 'https://git.smartbotics.hu';
+
+  if (!token) {
+    throw new Error('GOGS_ACCESS_TOKEN or GOGS_TOKEN environment variable is required');
+  }
+
+  return { baseUrl, token };
+}
+
+/**
+ * Fetch repository information from Gogs API
+ */
+export async function fetchRepositorySshUrl(owner: string, repo: string): Promise<string> {
+  try {
+    const { baseUrl, token } = getGogsConfig();
+    const url = `${baseUrl}/api/v1/repos/${owner}/${repo}`;
+
+    // Use curl to fetch repository info
+    const command = `curl -s -H "Authorization: token ${token}" "${url}"`;
+    const response = execSync(command, { encoding: 'utf-8' });
+
+    const repoInfo = JSON.parse(response);
+
+    if (repoInfo.ssh_url) {
+      return repoInfo.ssh_url;
+    }
+
+    throw new Error('ssh_url not found in repository information');
+  } catch (error) {
+    console.error('Error fetching repository SSH URL:', error);
+    throw error;
+  }
+}
+
+/**
+ * Alternative method: Try to extract SSH URL from existing git config
+ */
+export function getExistingGitRemoteUrl(repoPath: string): string | null {
+  try {
+    if (!existsSync(join(repoPath, '.git'))) {
+      return null;
+    }
+
+    const url = execSync('git config --get remote.origin.url', {
+      cwd: repoPath,
+      encoding: 'utf-8'
+    }).trim();
+
+    return url || null;
+  } catch (error) {
+    return null;
+  }
+}

+ 50 - 0
client/src/index.ts

@@ -18,6 +18,12 @@ if (import.meta.url === `file://${process.argv[1]}`) {
   const { ClaudeClient } = await import('./claude-client.js');
   const { Config } = await import('./config.js');
   const { ToolDiscovery } = await import('./toolDiscovery.js');
+  const { setupEnvironment, ensureRepository, detectAgentManagerPath } = await import('./repository-manager.js');
+  const { fetchRepositorySshUrl, getExistingGitRemoteUrl } = await import('./gogs-helper.js');
+
+  // Setup environment variables (HOME, PATH, XDG directories)
+  console.log('Setting up environment...');
+  setupEnvironment();
 
   // Parse command line arguments
   const args = process.argv.slice(2);
@@ -29,6 +35,12 @@ if (import.meta.url === `file://${process.argv[1]}`) {
 
   const [owner, repo, pusher, issueNumber, issueTitle, issueBody, issueAction, commentBody, assignee] = args;
 
+  // Exit if issue action is 'closed' (but allow 'reopened')
+  if (issueAction === 'closed') {
+    console.log("Issue action is 'closed', skipping automation");
+    process.exit(0);
+  }
+
   // Validate assignee
   if (assignee && assignee !== 'claude') {
     console.log(`Issue not assigned to claude (assignee: ${assignee}), skipping`);
@@ -42,10 +54,48 @@ if (import.meta.url === `file://${process.argv[1]}`) {
     process.exit(0);
   }
 
+  // Detect Agent Manager path
+  const agentManagerPath = detectAgentManagerPath();
+  console.log(`Agent Manager Path: ${agentManagerPath}`);
+
   // Determine repo path
   const home = process.env.HOME || '/root';
   const repoPath = `${home}/${repo}`;
 
+  // Fetch SSH URL from Gogs API or use existing git remote
+  let sshUrl: string;
+  try {
+    // First, try to get from existing repository
+    const existingUrl = getExistingGitRemoteUrl(repoPath);
+    if (existingUrl) {
+      console.log(`Using existing git remote URL: ${existingUrl}`);
+      sshUrl = existingUrl;
+    } else {
+      console.log('Fetching repository SSH URL from Gogs API...');
+      sshUrl = await fetchRepositorySshUrl(owner, repo);
+      console.log(`Fetched SSH URL: ${sshUrl}`);
+    }
+  } catch (error) {
+    console.error('Failed to fetch SSH URL from Gogs API:', error);
+    console.log('Falling back to existing repository or default URL format');
+    const existingUrl = getExistingGitRemoteUrl(repoPath);
+    if (existingUrl) {
+      sshUrl = existingUrl;
+    } else {
+      // Fallback to default format
+      sshUrl = `ssh://git@git.smartbotics.hu:10022/${owner}/${repo}.git`;
+      console.log(`Using fallback SSH URL: ${sshUrl}`);
+    }
+  }
+
+  // Ensure repository exists and is up to date
+  console.log('Ensuring repository exists and is up to date...');
+  ensureRepository(repoPath, sshUrl);
+
+  // Change to repository directory
+  process.chdir(repoPath);
+  console.log(`Working directory: ${process.cwd()}`);
+
   // Load configuration
   Config.load(repoPath);
 

+ 164 - 0
client/src/repository-manager.ts

@@ -0,0 +1,164 @@
+/**
+ * Repository Manager - Handles Git repository cloning and updates
+ */
+
+import { execSync } from 'child_process';
+import { existsSync, mkdirSync } from 'fs';
+import { dirname } from 'path';
+
+export interface RepositoryInfo {
+  owner: string;
+  repo: string;
+  sshUrl: string;
+}
+
+/**
+ * Fetch repository information from Gogs MCP
+ */
+async function fetchRepositoryInfo(owner: string, repo: string): Promise<RepositoryInfo> {
+  // This will be called from the context where Gogs MCP is already connected
+  // For now, we'll use a simple approach: try to fetch the repo info using the Gogs API
+  // In the actual implementation, this will be provided by the MCP server
+
+  // Since we're running in a context where we can use the mcp__gogs__get_repository tool,
+  // we'll need to make this work within the Agent SDK context
+  // For now, return a placeholder that will be populated by the caller
+  return {
+    owner,
+    repo,
+    sshUrl: '' // Will be populated by the caller
+  };
+}
+
+/**
+ * Clone a Git repository
+ */
+export function cloneRepository(gitUrl: string, targetPath: string): void {
+  console.log(`Repository not found at ${targetPath}, cloning...`);
+
+  // Create parent directory if it doesn't exist
+  const parentDir = dirname(targetPath);
+  if (!existsSync(parentDir)) {
+    mkdirSync(parentDir, { recursive: true });
+  }
+
+  try {
+    execSync(`git clone "${gitUrl}" "${targetPath}"`, {
+      stdio: 'inherit',
+      encoding: 'utf-8'
+    });
+    console.log('Repository cloned successfully');
+  } catch (error) {
+    console.error('Failed to clone repository:', error);
+    throw new Error('Failed to clone repository');
+  }
+}
+
+/**
+ * Pull latest changes from a Git repository
+ */
+export function pullRepository(repoPath: string): void {
+  console.log('Repository found, pulling latest changes...');
+
+  try {
+    execSync('git pull', {
+      cwd: repoPath,
+      stdio: 'inherit',
+      encoding: 'utf-8'
+    });
+    console.log('Repository updated successfully');
+  } catch (error) {
+    console.warn('Warning: git pull failed, continuing with existing repository state');
+    console.warn(error);
+  }
+}
+
+/**
+ * Ensure repository exists and is up to date
+ * Returns the ssh_url from Gogs MCP that should be used
+ */
+export function ensureRepository(repoPath: string, sshUrl: string): void {
+  if (!existsSync(repoPath)) {
+    cloneRepository(sshUrl, repoPath);
+  } else {
+    pullRepository(repoPath);
+  }
+}
+
+/**
+ * Setup environment variables for proper execution
+ */
+export function setupEnvironment(): void {
+  // Ensure HOME is set (systemd might not set it)
+  if (!process.env.HOME) {
+    // Try to get HOME from system
+    try {
+      const whoami = execSync('whoami', { encoding: 'utf-8' }).trim();
+      const homeDir = execSync(`getent passwd "${whoami}" | cut -d: -f6`, { encoding: 'utf-8' }).trim();
+      if (homeDir) {
+        process.env.HOME = homeDir;
+      }
+    } catch (error) {
+      console.warn('Could not determine HOME directory, using /root');
+      process.env.HOME = '/root';
+    }
+  }
+
+  // Add NODE to PATH
+  const localBin = `${process.env.HOME}/.local/bin`;
+  if (!process.env.PATH?.includes(localBin)) {
+    process.env.PATH = `${localBin}:${process.env.PATH || ''}`;
+  }
+
+  // Ensure XDG directories are set
+  process.env.XDG_CACHE_HOME = process.env.XDG_CACHE_HOME || `${process.env.HOME}/.cache`;
+  process.env.XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME || `${process.env.HOME}/.config`;
+  process.env.XDG_DATA_HOME = process.env.XDG_DATA_HOME || `${process.env.HOME}/.local/share`;
+  process.env.XDG_STATE_HOME = process.env.XDG_STATE_HOME || `${process.env.HOME}/.local/state`;
+
+  // Create XDG directories if they don't exist
+  const xdgDirs = [
+    process.env.XDG_CACHE_HOME,
+    process.env.XDG_CONFIG_HOME,
+    process.env.XDG_DATA_HOME,
+    process.env.XDG_STATE_HOME
+  ];
+
+  for (const dir of xdgDirs) {
+    if (dir && !existsSync(dir)) {
+      try {
+        mkdirSync(dir, { recursive: true });
+      } catch (error) {
+        console.warn(`Warning: Could not create directory ${dir}:`, error);
+      }
+    }
+  }
+}
+
+/**
+ * Detect Agent Manager path
+ */
+export function detectAgentManagerPath(): string {
+  // Try multiple possible locations for the agent-manager .env file
+  const possiblePaths = [
+    '/home/claude/agent-manager',
+    '/data/agent-manager',
+    `${process.env.HOME}/agent-manager`
+  ];
+
+  for (const path of possiblePaths) {
+    const envPath = `${path}/.env`;
+    if (existsSync(envPath)) {
+      console.log(`Found agent-manager at: ${path}`);
+      return path;
+    }
+  }
+
+  // Fallback: try to detect from current file location
+  // This file is in client/src/, so agent-manager root is ../../
+  const scriptDir = dirname(new URL(import.meta.url).pathname);
+  const agentManagerPath = dirname(dirname(scriptDir));
+  console.log(`Using detected path from script location: ${agentManagerPath}`);
+
+  return agentManagerPath;
+}

+ 40 - 0
commands.json

@@ -0,0 +1,40 @@
+{
+  "$schema": "https://json-schema.org/draft-07/schema#",
+  "description": "Command configuration for webhook events. Available variables: {{repo}}, {{full_repo}}, {{repo_owner}}, {{branch}}, {{pusher}}, {{event}}, {{commit}}, {{commit_msg}}, {{pr_number}}, {{pr_action}}, {{tag}}, {{ref_type}}, {{issue_number}}, {{issue_title}}, {{issue_action}}, {{issue_body}}, {{issue_assignee}}, {{comment_body}}",
+  "commands": {
+    "push": [],
+    "pull_request": [],
+    "create": [],
+    "delete": [],
+    "release": [],
+    "issues": [
+      {
+        "name": "claude-issue-handler",
+        "description": "Automatically handle issues assigned to Claude with Claude Agent SDK TypeScript client",
+        "type": "node",
+        "command": "${AGENT_MANAGER_PATH}/client/dist/index.js",
+        "args": ["{{repo_owner}}", "{{repo}}", "{{pusher}}", "{{issue_number}}", "{{issue_title}}", "{{issue_body}}", "{{issue_action}}", "", "{{issue_assignee}}"],
+        "cwd": "/home/claude",
+        "timeout": 600000,
+        "filterBranch": null,
+        "filterActions": ["assigned", "opened", "reopened"],
+        "filterAssignee": "claude"
+      }
+    ],
+    "issue_comment": [
+      {
+        "name": "claude-comment-handler",
+        "description": "Automatically handle issue comments for issues assigned to Claude with Claude Agent SDK TypeScript client",
+        "type": "node",
+        "command": "${AGENT_MANAGER_PATH}/client/dist/index.js",
+        "args": ["{{repo_owner}}", "{{repo}}", "{{pusher}}", "{{issue_number}}", "{{issue_title}}", "{{issue_body}}", "{{issue_action}}", "{{comment_body}}", "{{issue_assignee}}"],
+        "cwd": "/home/claude",
+        "timeout": 600000,
+        "filterBranch": null,
+        "filterActions": ["created"],
+        "filterAssignee": "claude"
+      }
+    ],
+    "global": []
+  }
+}