Skill: vnagin/zoom-manager-clawd (version 0.1.0)

--- name: zoom-manager description: Manage Zoom meetings via OAuth API. Create, list, delete, and update events. metadata: { "clawdbot": { "emoji": "📹", "requires": {"bins": ["node"]}, "secrets": ["ZOOM_CLIENT_ID", "ZOOM_CLIENT_SECRET", "ZOOM_ACCOUNT_ID"] } } --- # Zoom Manager Manage your Zoom meetings directly from Clawdbot with this powerful, headless integration. This skill allows you to automate your meeting workflows without ever opening the Zoom dashboard. ### Key Features: - **Full Meeting Lifecycle**: Create, list, update, and delete meetings using simple natural language or CLI commands. - **Enterprise-Ready**: Built for Server-to-Server OAuth, ensuring secure and robust authentication. - **Automated Recording**: Default support for cloud recording to make sure your sessions are always captured. - **Developer Friendly**: Clean Node.js implementation that can be easily extended for custom automation flows (e.g., auto-scheduling after a CRM update). - **Headless & Fast**: No browser required; interacts directly with the Zoom REST API v2. ## Setup 1. Create a **Server-to-Server OAuth** app in the [Zoom App Marketplace](https://marketplace.zoom.us/). 2. **Scopes Required**: In the Zoom App Marketplace (Scopes tab), add the following: - **Meetings**: - `meeting:read:admin` / `meeting:read:master` (View meetings) - `meeting:write:admin` / `meeting:write:master` (Create and update meetings) - `meeting:delete:admin` / `meeting:delete:master` (Delete meetings) - **Users**: `user:read:admin` - **Recordings**: `recording:read:admin` 3. Get your **Client ID**, **Client Secret**, and **Account ID** from the "App Credentials" tab. 4. Set them as environment variables in your Clawdbot config: - `ZOOM_CLIENT_ID` - `ZOOM_CLIENT_SECRET` - `ZOOM_ACCOUNT_ID` ## Commands ### List Meetings ```bash node {baseDir}/scripts/zoom-cli.js list ``` ### Create a Meeting ```bash node {baseDir}/scripts/zoom-cli.js create "Meeting Topic" "2026-01-30T10:00:00Z" 60 ``` ### Update a Meeting ```bash node {baseDir}/scripts/zoom-cli.js update <meeting_id> <new_start_time> <duration> "New Topic" ``` ### Get Meeting Info ```bash node {baseDir}/scripts/zoom-cli.js info <meeting_id> ``` ### Delete a Meeting ```bash node {baseDir}/scripts/zoom-cli.js delete <meeting_id> ```

path: scripts/create_meeting.js
content: #!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');

// Load config
async function loadConfig() {
  const configPath = path.join(__dirname, '..', 'config.json');
  const data = await fs.readFile(configPath, 'utf8');
  return JSON.parse(data);
}

// Get OAuth access token
async function getAccessToken(config) {
  const url = 'https://api.zoom.us/oauth/token';
  const params = new URLSearchParams({
    grant_type: 'account_credentials',
    account_id: config.account_id
  }).toString();

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': `Basic ${Buffer.from(`${config.client_id}:${config.client_secret}`).toString('base64')}`
    },
    body: params
  });

  if (!response.ok) {
    throw new Error(`Failed to get access [REDACTED_SECRET] ${response.statusText}`);
  }

  const data = await response.json();
  return data.access_token;
}

// Create a meeting
async function createMeeting(config, topic, startTime, durationMinutes) {
  const accessToken = await getAccessToken(config);
  const url = `https://api.zoom.us/v2/users/${config.user_id}/meetings`;
  const payload = {
    topic: topic,
    type: 2,
    start_time: startTime,
    duration: durationMinutes,
    timezone: 'UTC',
    settings: {
      host_video: true,
      participant_video: true,
      join_before_host: false,
      mute_upon_entry: false,
      approval_type: 0,
      audio: 'both',
      auto_recording: 'cloud'
    }
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  if (!response.ok) {
    throw new Error(`Failed to create meeting: ${response.status} ${response.statusText}`);
  }

  return await response.json();
}

// Main
async function main() {
  if (process.argv.length !== 5) {
    console.log('Usage: node create_meeting.js <topic> <start_time> <duration_minutes>');
    process.exit(1);
  }

  try {
    const config = await loadConfig();
    const topic = process.argv[2];
    const startTime = process.argv[3];
    const durationMinutes = parseInt(process.argv[4], 10);

    const meeting = await createMeeting(config, topic, startTime, durationMinutes);
    console.log('✅ Meeting created successfully!');
    console.log(`ID: ${meeting.id}`);
    console.log(`Join URL: ${meeting.join_url}`);
    console.log(`Start Time: ${meeting.start_time}`);
  } catch (error) {
    console.error('❌ Error:', error.message);
    process.exit(1);
  }
}

main();

path: scripts/create_meeting.py
content: #!/usr/bin/env python3
import json
import requests
import sys
import os

# Load config
config_path = os.path.join(os.path.dirname(__file__), '..', 'config.json')
with open(config_path, 'r') as f:
    config = json.load(f)

ACCOUNT_ID = config['account_id']
CLIENT_ID = config['client_id']
CLIENT_SECRET = config['client_secret']
USER_ID = config['user_id']

def get_access_token():
    url = "https://api.zoom.us/oauth/token"
    data = {
        "grant_type": "account_credentials",
        "account_id": ACCOUNT_ID
    }
    response = requests.post(url, auth=(CLIENT_ID, CLIENT_SECRET), data=data)
    response.raise_for_status()
    return response.json()['access_token']

def create_meeting(topic, start_time, duration_minutes):
    access_token = get_access_token()
    url = f"https://api.zoom.us/v2/users/{USER_ID}/meetings"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    payload = {
        "topic": topic,
        "type": 2,  # Scheduled meeting
        "start_time": start_time,
        "duration": duration_minutes,
        "timezone": "UTC",
        "settings": {
            "host_video": True,
            "participant_video": True,
            "join_before_host": False,
            "mute_upon_entry": False,
            "approval_type": 0,
            "audio": "both",
            "auto_recording": "cloud"
        }
    }
    response = requests.post(url, headers=headers, json=payload)
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Usage: create_meeting.py <topic> <start_time> <duration_minutes>")
        sys.exit(1)
    
    topic = sys.argv[1]
    start_time = sys.argv[2]
    duration_minutes = int(sys.argv[3])
    
    try:
        meeting = create_meeting(topic, start_time, duration_minutes)
        print(f"Meeting created successfully!")
        print(f"ID: {meeting['id']}")
        print(f"Join URL: {meeting['join_url']}")
        print(f"Start Time: {meeting['start_time']}")
    except Exception as e:
        print(f"Error: {e}")

path: scripts/delete_meeting.js
content: #!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const fetch = require('node-fetch');

// Load config
const configPath = path.join(__dirname, '..', 'config.json');
let config;

async function loadConfig() {
    try {
        const data = await fs.readFile(configPath, 'utf8');
        config = JSON.parse(data);
    } catch (err) {
        console.error('Error loading config:', err.message);
        process.exit(1);
    }
}

async function getAccessToken() {
    const url = 'https://api.zoom.us/oauth/token';
    const params = new URLSearchParams({
        grant_type: 'account_credentials',
        account_id: config.account_id
    });

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: params.toString(),
            auth: `${config.client_id}:${config.client_secret}`
        });
        const data = await response.json();
        if (!response.ok) {
            throw new Error(`Failed to get [REDACTED_SECRET] || response.status}`);
        }
        return data.access_token;
    } catch (err) {
        console.error('Error getting access token:', err.message);
        process.exit(1);
    }
}

async function deleteMeeting(meetingId) {
    const token = await getAccessToken();
    const url = `https://api.zoom.us/v2/meetings/${meetingId}`;

    try {
        const response = await fetch(url, {
            method: 'DELETE',
            headers: {
                'Authorization': `Bearer ${token}`
            }
        });

        if (response.ok) {
            console.log(`Meeting ${meetingId} deleted successfully.`);
        } else {
            const error = await response.json();
            console.error(`Failed to delete meeting ${meetingId}: ${response.status} - ${error.message || 'Unknown error'}`);
        }
    } catch (err) {
        console.error('Error deleting meeting:', err.message);
    }
}

// Main execution
async function main() {
    await loadConfig();
    const meetingId = process.argv[2];
    if (!meetingId) {
        console.error('Usage: node delete_meeting.js <meeting_id>');
        process.exit(1);
    }
    await deleteMeeting(meetingId);
}

main().catch(console.error);

path: scripts/delete_meeting.py
content: #!/usr/bin/env python3
import requests
import json
import os
import sys

# Загрузка конфигурации
try:
    with open('config.json', 'r') as f:
        config = json.load(f)
except FileNotFoundError:
    print("Error: config.json not found. Please create it with your Zoom API credentials.")
    sys.exit(1)

ACCOUNT_ID = config['account_id']
CLIENT_ID = config['client_id']
CLIENT_SECRET = config['client_secret']
USER_ID = config['user_id']

def get_access_token():
    url = "https://api.zoom.us/oauth/token"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "grant_type": "account_credentials",
        "account_id": ACCOUNT_ID
    }
    response = requests.post(url, headers=headers, data=data, auth=(CLIENT_ID, CLIENT_SECRET))
    if response.status_code == 200:
        return response.json().get('access_token')
    else:
        print(f"Failed to get access [REDACTED_SECRET] - {response.text}")
        return None

def delete_meeting(meeting_id):
    access_token = get_access_token()
    if not access_token:
        return False

    url = f"https://api.zoom.us/v2/meetings/{meeting_id}"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }

    response = requests.delete(url, headers=headers)
    if response.status_code == 204:
        print(f"Meeting {meeting_id} deleted successfully.")
        return True
    else:
        print(f"Failed to delete meeting {meeting_id}: {response.status_code} - {response.text}")
        return False

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python delete_meeting.py <meeting_id>")
        sys.exit(1)

    meeting_id = sys.argv[1]
    delete_meeting(meeting_id)

path: scripts/get_meeting_info.js
content: #!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const fetch = require('node-fetch');

// Load config
async function loadConfig() {
  const configPath = path.join(__dirname, '..', 'config.json');
  const data = await fs.readFile(configPath, 'utf8');
  return JSON.parse(data);
}

// Get OAuth access token
async function getAccessToken(config) {
  const url = 'https://api.zoom.us/oauth/token';
  const params = new URLSearchParams({
    grant_type: 'account_credentials',
    account_id: config.account_id
  }).toString();

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': `Basic ${Buffer.from(`${config.client_id}:${config.client_secret}`).toString('base64')}`
    },
    body: params
  });

  if (!response.ok) {
    throw new Error(`Failed to get access [REDACTED_SECRET] ${response.statusText}`);
  }

  const data = await response.json();
  return data.access_token;
}

// Get meeting info
async function getMeetingInfo(meetingId, accessToken) {
  const url = `https://api-us.zoom.us/v2/meetings/${meetingId}`;
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${accessToken}`
    }
  });

  if (!response.ok) {
    throw new Error(`Failed to get meeting info: ${response.status} ${response.statusText}`);
  }

  return await response.json();
}

// Main execution
async function main() {
  if (process.argv.length !== 3) {
    console.error('Usage: node get_meeting_info.js <meeting_id>');
    process.exit(1);
  }

  const meetingId = process.argv[2];
  try {
    const config = await loadConfig();
    const token = await getAccessToken(config);
    const info = await getMeetingInfo(meetingId, token);
    console.log(JSON.stringify(info, null, 2));
  } catch (error) {
    console.error('Error:', error.message);
    process.exit(1);
  }
}

main();

path: scripts/get_meeting_info.py
content: #!/usr/bin/env python3
"""
Get detailed information about a specific Zoom meeting.
Usage: python get_meeting_info.py <meeting_id>
"""

import os
import sys
import json
import requests

# Load config
CONFIG_PATH = os.path.join(os.path.dirname(__file__), '..', 'config.json')
with open(CONFIG_PATH, 'r') as f:
    config = json.load(f)

ACCOUNT_ID = config['account_id']
CLIENT_ID = config['client_id']
CLIENT_SECRET = config['client_secret']
USER_ID = config['user_id']

def get_access_token():
    """Get OAuth access token."""
    url = "https://api.zoom.us/oauth/token"
    data = {
        "grant_type": "account_credentials",
        "account_id": ACCOUNT_ID
    }
    response = requests.post(url, auth=(CLIENT_ID, CLIENT_SECRET), data=data)
    response.raise_for_status()
    return response.json()['access_token']

def get_meeting_info(meeting_id):
    """Get details of a specific meeting."""
    [REDACTED_SECRET])
    url = f"https://api-us.zoom.us/v2/meetings/{meeting_id}"
    headers = {
        "Authorization": f"Bearer {token}"
    }
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python get_meeting_info.py <meeting_id>")
        sys.exit(1)

    meeting_id = sys.argv[1]
    try:
        info = get_meeting_info(meeting_id)
        print(json.dumps(info, indent=2))
    except requests.exceptions.HTTPError as e:
        print(f"Error: {e}")
        print(f"Response: {e.response.text}")
    except Exception as e:
        print(f"Unexpected error: {e}")

path: scripts/list_meetings.js
content: #!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const fetch = require('node-fetch');

// Load config
async function loadConfig() {
  const configPath = path.join(__dirname, '..', 'config.json');
  const data = await fs.readFile(configPath, 'utf8');
  return JSON.parse(data);
}

async function getAccessToken(config) {
  const url = 'https://api.zoom.us/oauth/token';
  const params = new URLSearchParams({
    grant_type: 'account_credentials',
    account_id: config.account_id
  });

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': `Basic ${Buffer.from(`${config.client_id}:${config.client_secret}`).toString('base64')}`
    },
    body: params.toString()
  });

  if (!response.ok) {
    throw new Error(`Failed to get access [REDACTED_SECRET] ${response.statusText}`);
  }

  const data = await response.json();
  return data.access_token;
}

async function listMeetings(config, accessToken) {
  const url = `https://api-us.zoom.us/v2/users/${config.user_id}/meetings?type=upcoming`;
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`Failed to list meetings: ${response.status} ${response.statusText}`);
  }

  return await response.json();
}

async function main() {
  try {
    const config = await loadConfig();
    const token = await getAccessToken(config);
    const meetings = await listMeetings(config, token);

    if (!meetings.meetings || meetings.meetings.length === 0) {
      console.log('No upcoming meetings found.');
      return;
    }

    console.log(`Found ${meetings.meetings.length} upcoming meeting(s):`);
    for (const meeting of meetings.meetings) {
      console.log(`- ID: ${meeting.id}`);
      console.log(`  Topic: ${meeting.topic}`);
      console.log(`  Start: ${meeting.start_time}`);
      console.log(`  Join URL: ${meeting.join_url}\n`);
    }
  } catch (error) {
    console.error('Error:', error.message);
  }
}

main();

path: scripts/list_meetings.py
content: #!/usr/bin/env python3
"""
List all upcoming meetings for the authenticated user.
Usage: python list_meetings.py
"""

import json
import requests
import os

# Load config
with open('config.json', 'r') as f:
    config = json.load(f)

ACCOUNT_ID = config['account_id']
CLIENT_ID = config['client_id']
CLIENT_SECRET = config['client_secret']
USER_ID = config['user_id']

def get_access_token():
    url = "https://api.zoom.us/oauth/token"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "grant_type": "account_credentials",
        "account_id": ACCOUNT_ID
    }
    response = requests.post(url, headers=headers, data=data, auth=(CLIENT_ID, CLIENT_SECRET))
    response.raise_for_status()
    return response.json()['access_token']

def list_meetings(access_token):
    url = f"https://api-us.zoom.us/v2/users/{USER_ID}/meetings"
    headers = {
        "Authorization": f"Bearer {access_token}"
    }
    params = {
        "type": "upcoming"
    }
    response = requests.get(url, headers=headers, params=params)
    response.raise_for_status()
    return response.json()

if __name__ == "__main__":
    try:
        [REDACTED_SECRET])
        meetings = list_meetings(token)
        
        if 'meetings' not in meetings or not meetings['meetings']:
            print("No upcoming meetings found.")
        else:
            print(f"Found {len(meetings['meetings'])} upcoming meeting(s):")
            for meeting in meetings['meetings']:
                print(f"- ID: {meeting['id']}")
                print(f"  Topic: {meeting['topic']}")
                print(f"  Start: {meeting['start_time']}")
                print(f"  Join URL: {meeting['join_url']}\n")
                
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")

path: scripts/zoom-cli.js
content: #!/usr/bin/env node
/**
 * Zoom CLI - Unified interface for Zoom API operations.
 * Ported for ClawdHub - Uses Environment Variables for security.
 */
const path = require('path');

// Configuration from Environment Variables
const config = {
  client_id: process.env.ZOOM_CLIENT_ID,
  client_secret: process.env.ZOOM_CLIENT_SECRET,
  account_id: process.env.ZOOM_ACCOUNT_ID,
  user_id: process.env.ZOOM_USER_ID || 'me'
};

function checkConfig() {
  const missing = [];
  if (!config.client_id) missing.push('ZOOM_CLIENT_ID');
  if (!config.client_secret) missing.push('ZOOM_CLIENT_SECRET');
  if (!config.account_id) missing.push('ZOOM_ACCOUNT_ID');
  
  if (missing.length > 0) {
    console.error('❌ Missing required environment variables:', missing.join(', '));
    process.exit(1);
  }
}

async function getAccessToken() {
  const url = 'https://api.zoom.us/oauth/token';
  const params = new URLSearchParams({
    grant_type: 'account_credentials',
    account_id: config.account_id
  });

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': `Basic ${Buffer.from(`${config.client_id}:${config.client_secret}`).toString('base64')}`
    },
    body: params.toString()
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Failed to get access [REDACTED_SECRET] ${response.statusText}\n${errorText}`);
  }

  const data = await response.json();
  return data.access_token;
}

async function createMeeting(topic, startTime, durationMinutes) {
  const token = await getAccessToken();
  const url = `https://api.zoom.us/v2/users/${config.user_id}/meetings`;
  const payload = {
    topic: topic,
    type: 2,
    start_time: startTime,
    duration: durationMinutes,
    timezone: 'UTC',
    settings: {
      host_video: true,
      participant_video: true,
      join_before_host: false,
      mute_upon_entry: false,
      approval_type: 0,
      audio: 'both',
      auto_recording: 'cloud'
    }
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Failed to create meeting: ${response.status} ${response.statusText}\n${errorText}`);
  }

  return await response.json();
}

async function listMeetings() {
  const token = await getAccessToken();
  const url = `https://api.zoom.us/v2/users/${config.user_id}/meetings?type=upcoming`;

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Failed to list meetings: ${response.status} ${response.statusText}\n${errorText}`);
  }

  return await response.json();
}

async function getMeetingInfo(meetingId) {
  const token = await getAccessToken();
  const url = `https://api.zoom.us/v2/meetings/${meetingId}`;

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Failed to get meeting info: ${response.status} ${response.statusText}\n${errorText}`);
  }

  return await response.json();
}

async function deleteMeeting(meetingId) {
  const token = await getAccessToken();
  const url = `https://api.zoom.us/v2/meetings/${meetingId}`;

  const response = await fetch(url, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  if (response.status === 204) {
    return true;
  }

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Failed to delete meeting: ${response.status} ${response.statusText}\n${errorText}`);
  }

  return true;
}

// Main logic
async function main() {
  checkConfig();

  const command = process.argv[2];
  switch (command) {
    case 'create':
      if (process.argv.length !== 6) {
        console.log('Usage: zoom-cli.js create <topic> <start_time> <duration_minutes>');
        process.exit(1);
      }
      const [topic, startTime, duration] = [process.argv[3], process.argv[4], parseInt(process.argv[5], 10)];
      try {
        const meeting = await createMeeting(topic, startTime, duration);
        console.log('✅ Meeting created successfully!');
        console.log(`ID: ${meeting.id}`);
        console.log(`Join URL: ${meeting.join_url}`);
        console.log(`Start Time: ${meeting.start_time}`);
      } catch (e) {
        console.error('❌ Error:', e.message);
      }
      break;

    case 'list':
      try {
        const data = await listMeetings();
        if (!data.meetings || data.meetings.length === 0) {
          console.log('No upcoming meetings found.');
        } else {
          console.log(`Found ${data.meetings.length} upcoming meeting(s):`);
          for (const m of data.meetings) {
            console.log(`- ${m.topic} | ID: ${m.id} | ${m.start_time} | ${m.join_url}`);
          }
        }
      } catch (e) {
        console.error('❌ Error:', e.message);
      }
      break;

    case 'info':
      if (process.argv.length !== 4) {
        console.log('Usage: zoom-cli.js info <meeting_id>');
        process.exit(1);
      }
      try {
        const info = await getMeetingInfo(process.argv[3]);
        console.log(JSON.stringify(info, null, 2));
      } catch (e) {
        console.error('❌ Error:', e.message);
      }
      break;

    case 'delete':
      if (process.argv.length !== 4) {
        console.log('Usage: zoom-cli.js delete <meeting_id>');
        process.exit(1);
      }
      try {
        await deleteMeeting(process.argv[3]);
        console.log(`✅ Meeting ${process.argv[3]} deleted successfully.`);
      } catch (e) {
        console.error('❌ Error:', e.message);
      }
      break;

    case 'update':
      if (process.argv.length < 5) {
        console.log('Usage: zoom-cli.js update <meeting_id> <start_time> <duration_minutes> [topic]');
        process.exit(1);
      }
      const [mId, newStart, newDuration, newTopic] = [process.argv[3], process.argv[4], parseInt(process.argv[5], 10), process.argv[6]];
      try {
        const token = await getAccessToken();
        const url = `https://api.zoom.us/v2/meetings/${mId}`;
        const payload = {
          start_time: newStart,
          duration: newDuration
        };
        if (newTopic) payload.topic = newTopic;

        const response = await fetch(url, {
          method: 'PATCH',
          headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(payload)
        });

        if (!response.ok) {
          const errorText = await response.text();
          throw new Error(`Failed to update meeting: ${response.status} ${response.statusText}\n${errorText}`);
        }

        console.log(`✅ Meeting ${mId} updated successfully to ${newStart}.`);
      } catch (e) {
        console.error('❌ Error:', e.message);
      }
      break;

    default:
      console.log('Available commands: create, list, info, delete, update');
      process.exit(1);
  }
}

main().catch(console.error);


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)