Skill: brucezhu888/dingtalk-workspace (version 1.0.1)

--- name: dingtalk-workspace description: Interact with DingTalk enterprise workspace using the `dws` CLI. Use for: (1) searching contacts and departments, (2) sending chat messages, (3) managing calendar events and meeting rooms, (4) creating and managing todos, (5) approval workflows, (6) attendance records, (7) AITable CRUD operations, (8) report management. Requires dws CLI installed and authenticated via OAuth. --- # DingTalk Workspace Skill Use the `dws` CLI to interact with DingTalk enterprise workspace. This skill covers all 12 products: contact, chat, bot, calendar, todo, oa (approval), attendance, ding, report, aitable, workbench, and devdoc. ## ⚠️ Security & Safety Notes **Read before installing:** 1. **Credentials Required**: This skill requires OAuth credentials (DWS_CLIENT_ID, DWS_CLIENT_SECRET) from a DingTalk Open Platform app. Enterprise admin approval may be needed. 2. **Install Safely**: The `dws` CLI installer fetches from GitHub. **Review the installer script before running**: - Installer: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/blob/main/scripts/install.sh - Releases: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases 3. **Autonomous Execution Risk**: This skill can perform destructive actions (approve workflows, send messages, delete records). **Always use `--dry-run` first** and restrict autonomous invocation unless you trust the agent. 4. **Least Privilege**: Use scoped OAuth credentials with minimum permissions. Test in a sandbox enterprise first. ## Prerequisites ### Installation **Option 1: Install from release (recommended)** Download pre-built binary from https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases **Option 2: Build from source (safer)** ```bash git clone https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git cd dingtalk-workspace-cli go build -o dws ./cmd cp dws ~/.local/bin/ ``` **Option 3: Install script (review first!)** ```bash # macOS / Linux - REVIEW SCRIPT BEFORE RUNNING curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh | sh # Windows (PowerShell) - REVIEW SCRIPT BEFORE RUNNING irm https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.ps1 | iex ``` ### Authentication ```bash # First-time login (credentials saved to system Keychain) dws auth login --client-id <your-app-key> --client-secret <your-app-secret> # Or via environment variables export DWS_CLIENT_ID=<your-app-key> export DWS_CLIENT_SECRET=<your-app-secret> dws auth login ``` ## Safe Execution Guidelines ### For Agents - **`--dry-run`**: **ALWAYS use first** for mutations to preview API calls - **`--yes`**: Skip confirmation prompts (use only after verifying with --dry-run) - **`--jq`**: Extract specific fields to reduce token consumption - **`--fields`**: Return only needed fields ### Recommended Workflow ```bash # 1. Preview the operation dws todo task create --title "Test" --executors "user123" --dry-run # 2. Verify the output looks correct # 3. Execute (only if preview was correct) dws todo task create --title "Test" --executors "user123" --yes ``` ### Auto-Correction `dws` automatically corrects common AI mistakes: - `--baseId` → `--base-id` (camelCase to kebab-case) - `--timeout30` → `--timeout 30` (sticky argument splitting) - `--tabel-id` → `--table-id` (fuzzy matching) - `"yes"` → `true`, `"2024/03/29"` → `"2024-03-29"` (value normalization) ## Discovery & Introspection Before making calls, discover available capabilities: ```bash # List all products and tool counts dws schema --jq '.products[] | {id, tool_count: (.tools | length)}' # Inspect a specific tool's parameter schema dws schema aitable.query_records --jq '.tool.parameters' # View required fields dws schema aitable.query_records --jq '.tool.required' # List all product IDs dws schema --jq '.products[].id' ``` ## Quick Reference by Product ### Contact ```bash # Search users by keyword dws contact user search --keyword "engineering" # Get current user profile dws contact user get-self --jq '.result[0].orgEmployeeModel | {name: .orgUserName, dept: .depts[0].deptName}' # Search department by name dws contact dept search --keyword "Engineering" # List department members dws contact dept members --dept-id <dept-id> ``` ### Chat ```bash # Send message as bot dws chat message send-by-bot --robot-code <BOT_CODE> --group <GROUP_ID> --title "Weekly Report" --text @report.md # List groups dws chat group list # Get group info dws chat group get --group-id <GROUP_ID> ``` ### Calendar ```bash # List calendar events dws calendar event list # Create event dws calendar event create --title "Team Meeting" --start "2024-03-29T14:00:00Z" --end "2024-03-29T15:00:00Z" # Find free slots dws calendar participant busy --user-ids <user-id-1>,<user-id-2> --start "2024-03-29" --end "2024-03-30" # Search meeting rooms dws calendar room search --keyword "Meeting Room" ``` ### Todo ```bash # Create todo dws todo task create --title "Review PR" --executors "<your-userId>" --yes # List todos dws todo task list # Mark as done dws todo task done --task-id <task-id> ``` ### Approval (OA) ```bash # List pending approvals dws oa approval list --status pending # Approve instance dws oa approval approve --instance-id <instance-id> --comment "Approved" # Reject instance dws oa approval reject --instance-id <instance-id> --comment "Needs revision" ``` ### Attendance ```bash # View my attendance records dws attendance record list --user-id <your-userId> # View team shift schedule dws attendance shift list --dept-id <dept-id> ``` ### Report ```bash # View today's received reports dws report list --type received --start-date "2024-03-29" --end-date "2024-03-29" # Create report dws report create --template-id <template-id> --content @report.md ``` ### AITable ```bash # Query records dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --limit 10 # Create record dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> --fields '{"name": "Task 1", "status": "open"}' # List bases dws aitable base list # List tables in a base dws aitable table list --base-id <BASE_ID> ``` ## Output Control ### jq Filtering ```bash # Extract specific fields dws contact user search --keyword "engineering" --jq '.result[] | {name: .orgUserName, userId: .userId}' # Count results dws todo task list --jq '.result | length' ``` ### Field Selection ```bash # Return only specific fields dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --fields invocation,response ``` ### File Input ```bash # Read from file dws chat message send-by-bot --robot-code <BOT_CODE> --group <GROUP_ID> --text @message.md # Pipe from stdin cat message.md | dws chat message send-by-bot --robot-code <BOT_CODE> --group <GROUP_ID> ``` ## Common Workflows See bundled scripts in `scripts/` for batch operations: | Script | Description | |--------|-------------| | `calendar_schedule_meeting.py` | Create event + add participants + book meeting room | | `calendar_free_slot_finder.py` | Find common free slots across multiple people | | `todo_batch_create.py` | Batch create todos from JSON | | `contact_dept_members.py` | Search department and list all members | | `report_inbox_today.py` | View today's received reports | ## Error Handling ### Common Error Codes - `INVALID_TOKEN`: Re-authenticate with `dws auth login` - `PERMISSION_DENIED`: Check app permissions in DingTalk Open Platform - `RESOURCE_NOT_FOUND`: Verify IDs with `dws schema` introspection ### Recovery When encountering `RECOVERY_EVENT_ID`, use: ```bash dws --recovery <RECOVERY_EVENT_ID> ``` ## Security Notes - Credentials are stored encrypted in system Keychain (never in config files) - All requests use HTTPS to `*.dingtalk.com` only - Use `--dry-run` before any mutation to preview the API call - Token refresh is automatic; no manual intervention needed ## Reference Files - **Product commands**: See `references/products/*.md` for detailed command reference per product - **Intent guide**: See `references/intent-guide.md` for disambiguation (e.g., report vs todo) - **Error codes**: See `references/error-codes.md` for debugging workflows - **Global reference**: See `references/global-reference.md` for auth, output formats, global flags

path: clawhub.yml
content: # ClawHub Skill Metadata
# See: https://clawhub.ai/docs/publish

name: dingtalk-workspace
version: 1.0.1
description: Interact with DingTalk enterprise workspace using the dws CLI

author:
  name: Bruce Zhu
  email: [REDACTED_SECRET]

homepage: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli
repository: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli
license: Apache-2.0

# Required credentials
requires:
  env:
    - name: DWS_CLIENT_ID
      description: DingTalk App Key from Open Platform Console
      required: false  # Can also use interactive login
    - name: DWS_CLIENT_SECRET
      description: DingTalk App Secret from Open Platform Console
      required: false  # Can also use interactive login

# Security notes
security:
  - This skill requires OAuth credentials from DingTalk Open Platform
  - Enterprise admin approval may be required for whitelist access
  - Always use --dry-run before mutation operations
  - Test in sandbox enterprise before production use
  - Install dws CLI from verified releases, review installer scripts

# Tags for discovery
tags:
  - dingtalk
  - enterprise
  - productivity
  - chat
  - calendar
  - todo
  - approval
  - aitable
  - attendance
  - cli


path: references/error-codes.md
content: # Error Codes Reference

## ⚠️ Security Reminders

- **Always use `--dry-run`** before mutation operations
- **Never commit credentials** to version control
- **Use least-privilege OAuth scopes** for your app
- **Test in sandbox enterprise** before production

## Authentication Errors

### INVALID_TOKEN

**Code:** `AUTH_001`

**Message:** `Invalid or expired token`

**Cause:**
- Token has expired
- Credentials have been revoked
- First-time use without authentication

**Solution:**
```bash
# Re-authenticate
dws auth login --client-id <app-key> --client-secret <app-secret>
```

### PERMISSION_DENIED

**Code:** `AUTH_002`

**Message:** `Permission denied`

**Cause:**
- App lacks required permissions
- User not in whitelist
- Enterprise admin hasn't approved

**Solution:**
1. Check app permissions in DingTalk Open Platform Console
2. Contact enterprise admin for approval
3. Join DingTalk DWS co-creation group for whitelist setup

## Resource Errors

### RESOURCE_NOT_FOUND

**Code:** `RES_001`

**Message:** `Resource not found`

**Cause:**
- Invalid ID (base-id, table-id, user-id, etc.)
- Resource has been deleted
- Typo in ID

**Solution:**
```bash
# Verify ID with schema introspection
dws schema aitable.query_records --jq '.tool.parameters'

# List available resources
dws aitable base list --jq '.result[] | .baseId'
```

### INVALID_PARAMETER

**Code:** `RES_002`

**Message:** `Invalid parameter value`

**Cause:**
- Wrong date format (should be ISO 8601)
- Invalid enum value
- Missing required field

**Solution:**
```bash
# Check parameter schema
dws schema <product>.<operation> --jq '.tool.parameters'

# Use correct date format
# Wrong: "2024/03/29"
# Right: "2024-03-29T14:00:00Z"
```

## Rate Limiting

### RATE_LIMITED

**Code:** `RATE_001`

**Message:** `Rate limit exceeded`

**Cause:**
- Too many requests in short time
- Enterprise API quota exceeded

**Solution:**
```bash
# Wait and retry with exponential backoff
sleep 5
dws <command>

# Reduce request frequency
# Use --jq to reduce response size
```

## Network Errors

### NETWORK_TIMEOUT

**Code:** `NET_001`

**Message:** `Request timeout`

**Cause:**
- Network connectivity issue
- Server overload
- Large response

**Solution:**
```bash
# Increase timeout
dws <command> --timeout 60

# Reduce response size
dws <command> --jq '.result[] | {id: .id}'

# Retry
dws <command>
```

### NETWORK_ERROR

**Code:** `NET_002`

**Message:** `Network error`

**Cause:**
- No internet connection
- DNS resolution failure
- Firewall blocking

**Solution:**
```bash
# Check connectivity
curl https://oapi.dingtalk.com

# Check DWS_TRUSTED_DOMAINS
echo $DWS_TRUSTED_DOMAINS
```

## Operation Errors

### OPERATION_FAILED

**Code:** `OP_001`

**Message:** `Operation failed`

**Cause:**
- Business logic validation failed
- Conflicting operation
- Resource in use

**Solution:**
```bash
# Check operation details with --dry-run
dws <command> --dry-run

# Review error message for details
# Retry with corrected parameters
```

### CONFLICT

**Code:** `OP_002`

**Message:** `Resource conflict`

**Cause:**
- Duplicate resource
- Concurrent modification
- State mismatch

**Solution:**
```bash
# Check existing resources
dws <resource> list

# Retry with different parameters
```

## Recovery Workflow

When encountering errors, follow this workflow:

### Step 1: Identify Error Type

```bash
# Enable debug mode
export DWS_DEBUG=1
dws <command>
```

### Step 2: Check Error Code

Look for error code in response:
- `AUTH_*`: Authentication issue → Re-authenticate
- `RES_*`: Resource issue → Verify IDs
- `RATE_*`: Rate limit → Wait and retry
- `NET_*`: Network issue → Check connectivity
- `OP_*`: Operation issue → Review parameters

### Step 3: Apply Fix

Based on error type, apply the corresponding solution above.

### Step 4: Recovery Event

If you receive a `RECOVERY_EVENT_ID`:

```bash
dws --recovery <RECOVERY_EVENT_ID>
```

This will replay the operation with corrected parameters.

## Debugging Tips

### Enable Verbose Logging

```bash
export DWS_DEBUG=1
dws <command>
```

### Use Dry-Run

```bash
# Preview before executing
dws <mutation-command> --dry-run
```

### Check Schema

```bash
# Verify parameter names and types
dws schema <product>.<operation>
```

### Test with Minimal Input

```bash
# Start with required fields only
dws <command> --required-field <value>

# Add optional fields one by one
```

### Use jq for Focused Output

```bash
# Extract error details
dws <command> --jq '.error'
```


path: references/global-reference.md
content: # Global Reference

## ⚠️ Security Warning

**Before using this skill:**

- **Credentials are required**: DWS_CLIENT_ID and DWS_CLIENT_SECRET from DingTalk Open Platform
- **Enterprise admin approval** may be needed for whitelist access
- **Test in sandbox first** before production use
- **Use least-privilege credentials** with minimum required permissions

## Authentication

### First-Time Setup

```bash
# 1. Create app in DingTalk Open Platform
# https://open-dev.dingtalk.com/fe/app

# 2. Configure redirect URLs in app settings:
#    http://127.0.0.1 (local login)
#    https://login.dingtalk.com (device-flow for headless)

# 3. Login with credentials
dws auth login --client-id <app-key> --client-secret <app-secret>
```

### Environment Variables

```bash
export DWS_CLIENT_ID=<app-key>
export DWS_CLIENT_SECRET=<app-secret>
dws auth login
```

**Security note**: Environment variables may be exposed in logs. Prefer interactive login which stores credentials encrypted in system Keychain.

### Token Management

- Tokens are stored encrypted in system Keychain (macOS/Windows) or libsecret (Linux)
- Automatic refresh on expiry
- No manual intervention needed
- **Never share your credentials** or commit them to version control

## Output Formats

### JSON (Default)

All commands return JSON by default.

### Table Format

```bash
dws contact user search --keyword "test" -f table
```

### Raw Format

```bash
dws contact user get-self -f raw
```

### jq Filtering

```bash
# Extract specific fields
dws contact user search --keyword "test" --jq '.result[] | {name: .orgUserName, userId: .userId}'

# Count results
dws todo task list --jq '.result | length'

# Complex transformation
dws calendar event list --jq '.result[] | "\(.startTime): \(.title)"'
```

### Field Selection

```bash
# Return only specific fields
dws aitable record query --base-id <baseId> --table-id <tableId> --fields invocation,response
```

## Global Flags

| Flag | Description | Example |
|------|-------------|---------|
| `--dry-run` | Preview without executing | `dws todo task create --dry-run` |
| `--yes` | Skip confirmations | `dws todo task create --yes` |
| `--jq <expr>` | Filter output with jq | `--jq '.result[]'` |
| `--fields <fields>` | Select specific fields | `--fields invocation,response` |
| `-f <format>` | Output format | `-f table`, `-f json`, `-f raw` |
| `--timeout <seconds>` | Request timeout | `--timeout 30` |

## Input Methods

### File Input

```bash
# Read from file
dws chat message send-by-bot --robot-code <code> --group <id> --text @message.md

# Read from stdin
cat message.md | dws chat message send-by-bot --robot-code <code> --group <id>
```

### JSON Input

```bash
# Inline JSON
dws aitable record create --base-id <base> --table-id <table> --fields '{"name": "Task 1"}'

# From file
dws aitable record create --base-id <base> --table-id <table> --fields @fields.json
```

## Error Handling

### Common Errors

| Error | Cause | Solution |
|-------|-------|----------|
| `INVALID_TOKEN` | Token expired | Run `dws auth login` |
| `PERMISSION_DENIED` | App lacks permission | Check app permissions in Open Platform |
| `RESOURCE_NOT_FOUND` | Invalid ID | Verify ID with schema introspection |
| `RATE_LIMITED` | Too many requests | Wait and retry |

### Recovery Event

When encountering `RECOVERY_EVENT_ID`:

```bash
dws --recovery <RECOVERY_EVENT_ID>
```

### Debug Mode

```bash
# Enable verbose logging
export DWS_DEBUG=1
dws contact user search --keyword "test"
```

## Schema Introspection

### Discover Capabilities

```bash
# List all products
dws schema --jq '.products[] | {id, tool_count: (.tools | length)}'

# Get product tools
dws schema --jq '.products[] | select(.id == "aitable") | .tools[] | .name'
```

### Inspect Tool Parameters

```bash
# Full schema
dws schema aitable.query_records

# Required fields only
dws schema aitable.query_records --jq '.tool.required'

# Parameter details
dws schema aitable.query_records --jq '.tool.parameters.properties'
```

## Auto-Correction Examples

`dws` automatically corrects common AI mistakes:

```bash
# camelCase → kebab-case
dws aitable record query --baseId BASE_ID
# Auto-corrected to: --base-id BASE_ID

# Sticky arguments
dws contact user search --keyword "test"--limit50
# Auto-corrected to: --keyword "test" --limit 50

# Fuzzy matching
dws aitable record query --tabel-id TABLE_ID
# Auto-corrected to: --table-id TABLE_ID

# Value normalization
dws todo task create --priority "1" --due-date "2024/03/29"
# Auto-corrected to: --priority 1 --due-date "2024-03-29"
```

## Best Practices

### For Agents

1. **Always use `--dry-run` first** for mutations
2. **Use `--yes`** to skip interactive prompts
3. **Use `--jq`** to reduce token consumption
4. **Discover with `dws schema`** before making calls

### Safe Execution Pattern

```bash
# 1. Preview
dws todo task create --title "Test" --executors "user123" --dry-run

# 2. Execute
dws todo task create --title "Test" --executors "user123" --yes
```

### Token Efficiency

```bash
# Bad: Returns full response
dws contact user search --keyword "test"

# Good: Extract only needed fields
dws contact user search --keyword "test" --jq '.result[] | {name: .orgUserName, userId: .userId}'
```


path: references/intent-guide.md
content: # Intent Guide - Disambiguation

This guide helps distinguish between similar user requests that map to different DingTalk products.

## Report vs Todo

**User says:** "Create a task to submit weekly report"

**Analysis:**
- If they want to **track** the work → `todo task create`
- If they want to **actually submit** the report → `report create`

**Decision tree:**
```
Is the user asking to:
├─ Track/remember to do something? → todo
│  └─ "remind me to...", "create a task to...", "add to my todo"
└─ Actually perform the action? → specific product
   └─ "submit a report" → report create
   └─ "send a message" → chat message send
   └─ "schedule a meeting" → calendar event create
```

## Calendar Event vs Meeting Room Booking

**User says:** "Book a meeting room for tomorrow"

**Analysis:**
- Room booking alone → `calendar room book`
- Meeting with participants + room → `calendar event create` (includes room)

**Recommended workflow:**
1. Check participant availability: `calendar participant busy`
2. Find available room: `calendar room search`
3. Create event with room: `calendar event create`

See script: `scripts/calendar_schedule_meeting.py`

## Chat Message vs DING Message

**User says:** "Send an urgent message to the team"

**Analysis:**
- Normal message → `chat message send` or `chat message send-by-bot`
- Urgent notification (SMS/push) → `ding message send`

**Decision:**
- Regular communication → chat
- Requires immediate attention → ding (use sparingly)

## Contact Search vs Department Query

**User says:** "Find all engineers"

**Analysis:**
- Search by keyword → `contact user search --keyword "engineer"`
- Get specific department members → `contact dept members --dept-id <id>`

**Recommended:**
1. Try search first: `contact user search --keyword "engineer"`
2. If need structured results, find department: `contact dept search --keyword "Engineering"`
3. Then get members: `contact dept members --dept-id <deptId>`

## AITable Record vs Todo

**User says:** "Add a new task to the project tracker"

**Analysis:**
- Personal task management → `todo task create`
- Team project tracking in a table → `aitable record create`

**Clues:**
- Mentions "table", "tracker", "spreadsheet" → aitable
- Personal reminder → todo
- Team visibility needed → likely aitable

## Approval vs Todo

**User says:** "I need to approve the leave request"

**Analysis:**
- Formal approval workflow → `oa approval approve`
- Personal task to remember → `todo task create`

**Decision:**
- If there's a formal approval instance → oa approval
- If just tracking → todo

## Common Ambiguous Scenarios

### "Send a notification"
- To chat group → `chat message send-by-bot`
- As urgent DING → `ding message send`
- As report → `report create`

**Ask for clarification:** "Do you want to send a chat message, a DING notification, or create a report?"

### "Check who's available"
- Calendar availability → `calendar participant busy`
- Contact info → `contact user search`
- Attendance status → `attendance record list`

**Ask for clarification:** "Do you want to check calendar availability, contact information, or attendance records?"

### "Create a new entry"
- Todo task → `todo task create`
- AITable record → `aitable record create`
- Calendar event → `calendar event create`

**Ask for clarification:** "What type of entry? A task, a table record, or a calendar event?"


path: references/products/aitable.md
content: # AITable Reference

## Base Operations

### List Bases

```bash
dws aitable base list [--limit <N>]
```

**Example:**
```bash
dws aitable base list --jq '.result[] | {name: .name, baseId: .baseId}'
```

### Get Base Detail

```bash
dws aitable base get --base-id <baseId>
```

## Table Operations

### List Tables in Base

```bash
dws aitable table list --base-id <baseId>
```

**Example:**
```bash
dws aitable table list --base-id "base123" --jq '.result[] | {name: .name, tableId: .tableId}'
```

### Get Table Detail

```bash
dws aitable table get --base-id <baseId> --table-id <tableId>
```

### Create Table

```bash
dws aitable table create --base-id <baseId> --name "<table-name>" --fields '<field-definitions>'
```

## Record Operations

### Query Records

```bash
dws aitable record query --base-id <baseId> --table-id <tableId> [--limit <N>] [--filter '<filter-json>'}
```

**Parameters:**
- `--base-id`: Base ID (required)
- `--table-id`: Table ID (required)
- `--limit`: Max records (default: 100)
- `--filter`: Filter expression (optional)

**Example:**
```bash
# Get all records
dws aitable record query --base-id "base123" --table-id "table456" --limit 50

# With filter
dws aitable record query --base-id "base123" --table-id "table456" --filter '{"status": "open"}'

# Extract specific fields
dws aitable record query --base-id "base123" --table-id "table456" --jq '.result.records[] | {name: .fields.name, status: .fields.status}'
```

### Create Record

```bash
dws aitable record create --base-id <baseId> --table-id <tableId> --fields '<json-fields>'
```

**Example:**
```bash
dws aitable record create \
  --base-id "base123" \
  --table-id "table456" \
  --fields '{"name": "Task 1", "status": "open", "priority": "high"}'
```

### Update Record

```bash
dws aitable record update --base-id <baseId> --table-id <tableId> --record-id <recordId> --fields '<json-fields>'
```

**Example:**
```bash
dws aitable record update \
  --base-id "base123" \
  --table-id "table456" \
  --record-id "rec789" \
  --fields '{"status": "done"}'
```

### Delete Record

```bash
dws aitable record delete --base-id <baseId> --table-id <tableId> --record-ids <rec1,rec2,rec3>
```

### Batch Create Records

```bash
dws aitable record batch-create --base-id <baseId> --table-id <tableId> --records '<json-array>'
```

**Example:**
```bash
dws aitable record batch-create \
  --base-id "base123" \
  --table-id "table456" \
  --records '[{"name": "Task 1"}, {"name": "Task 2"}, {"name": "Task 3"}]'
```

## Field Operations

### List Fields

```bash
dws aitable field list --base-id <baseId> --table-id <tableId>
```

**Example:**
```bash
dws aitable field list --base-id "base123" --table-id "table456" --jq '.result[] | {name: .name, type: .type, fieldId: .fieldId}'
```

### Create Field

```bash
dws aitable field create --base-id <baseId> --table-id <tableId> --name "<field-name>" --type "<field-type>"
```

**Field Types:** `text`, `number`, `singleSelect`, `multiSelect`, `date`, `checkbox`, `attachment`, `member`, `link`

### Update Field

```bash
dws aitable field update --base-id <baseId> --table-id <tableId> --field-id <fieldId> --name "<new-name>"
```

### Delete Field

```bash
dws aitable field delete --base-id <baseId> --table-id <tableId> --field-id <fieldId>
```

## Attachment Operations

### Upload Attachment

```bash
dws aitable attachment upload --base-id <baseId> --table-id <tableId> --record-id <recordId> --field-id <fieldId> --file <path>
```

See bundled script: `scripts/upload_attachment.py`

## Common Patterns

### Import Records from CSV

See bundled script: `scripts/import_records.py`

```bash
python scripts/import_records.py --base-id <baseId> --table-id <tableId> data.csv
```

### Batch Add Fields

See bundled script: `scripts/bulk_add_fields.py`

```bash
python scripts/bulk_add_fields.py --base-id <baseId> --table-id <tableId> fields.json
```

### Query with Complex Filter

```bash
# Filter by multiple conditions
dws aitable record query \
  --base-id "base123" \
  --table-id "table456" \
  --filter '{"and": [{"status": "open"}, {"priority": "high"}]}'
```

### Get Record Count

```bash
dws aitable record query --base-id "base123" --table-id "table456" --limit 1 --jq '.result.total'
```


path: references/products/attendance.md
content: # Attendance Reference

## Record Operations

### List Attendance Records

```bash
dws attendance record list --user-id <userId> [--start <date>] [--end <date>]
```

**Parameters:**
- `--user-id`: User ID (required)
- `--start`: Start date (YYYY-MM-DD)
- `--end`: End date (YYYY-MM-DD)

**Example:**
```bash
# Today's records
dws attendance record list --user-id "user123" --start "2024-03-29" --end "2024-03-29"

# This week
dws attendance record list --user-id "user123" --start "2024-03-25" --end "2024-03-31" --jq '.result[] | {date: .workDate, checkIn: .checkInTime, checkOut: .checkOutTime}'
```

### Get My Records

```bash
# First get your userId
dws contact user get-self --jq '.result[0].userId'

# Then get records
dws attendance record list --user-id "<your-userId>" --start "2024-03-29" --end "2024-03-29"
```

See bundled script: `scripts/attendance_my_record.py`

## Shift Operations

### List Shift Schedules

```bash
dws attendance shift list --dept-id <deptId> [--start <date>] [--end <date>]
```

**Example:**
```bash
dws attendance shift list --dept-id "dept123" --start "2024-03-29" --end "2024-04-05"
```

See bundled script: `scripts/attendance_team_shift.py`

### Get Shift Detail

```bash
dws attendance shift get --shift-id <shiftId>
```

## Summary Operations

### Get Attendance Summary

```bash
dws attendance summary get --user-id <userId> --start <date> --end <date>
```

**Example:**
```bash
dws attendance summary get --user-id "user123" --start "2024-03-01" --end "2024-03-31" --jq '.result | {workDays: .workDays, attendanceDays: .attendanceDays, lateDays: .lateDays}'
```

### Get Team Summary

```bash
dws attendance summary team --dept-id <deptId> --start <date> --end <date>
```

## Group Rules

### List Attendance Rules

```bash
dws attendance rules list --dept-id <deptId>
```

### Get Rule Detail

```bash
dws attendance rules get --rule-id <ruleId>
```

## Common Patterns

### Check If Someone Clock In Today

```bash
dws attendance record list --user-id "user123" --start "$(date -I)" --end "$(date -I)" --jq '.result | length > 0'
```

### Get Team Attendance Rate

```bash
# Get department members
dws contact dept members --dept-id "dept123" --jq '.result[] | .userId'

# Get summary for each
for user in $USERS; do
  dws attendance summary get --user-id "$user" --start "2024-03-01" --end "2024-03-31" --jq '.result.attendanceRate'
done
```

### Find Late Arrivals

```bash
dws attendance summary get --user-id "user123" --start "2024-03-01" --end "2024-03-31" --jq '.result.lateDays'
```


path: references/products/calendar.md
content: # Calendar Reference

## Event Operations

### List Events

```bash
dws calendar event list [--start <date>] [--end <date>] [--limit <N>]
```

**Parameters:**
- `--start`: Start date (ISO 8601, e.g., "2024-03-29T00:00:00Z")
- `--end`: End date (ISO 8601)
- `--limit`: Max results

**Example:**
```bash
# Today's events
dws calendar event list --start "2024-03-29T00:00:00Z" --end "2024-03-29T23:59:59Z"

# This week
dws calendar event list --start "2024-03-29" --end "2024-04-05" --jq '.result[] | {title: .title, start: .startTime}'
```

### Create Event

```bash
dws calendar event create --title "<title>" --start "<startTime>" --end "<endTime>" [--participants <userIds>] [--location "<location>"]
```

**Example:**
```bash
dws calendar event create \
  --title "Team Meeting" \
  --start "2024-03-29T14:00:00Z" \
  --end "2024-03-29T15:00:00Z" \
  --participants "user1,user2,user3" \
  --location "Conference Room A"
```

### Update Event

```bash
dws calendar event update --event-id <eventId> --title "<new-title>" [--start <new-start>] [--end <new-end>]
```

### Delete Event

```bash
dws calendar event delete --event-id <eventId>
```

### Get Event Detail

```bash
dws calendar event get --event-id <eventId>
```

## Participant Operations

### Add Participants

```bash
dws calendar participant add --event-id <eventId> --user-ids <userId1,userId2>
```

### Remove Participants

```bash
dws calendar participant remove --event-id <eventId> --user-ids <userId1,userId2>
```

### Check Participant Availability

```bash
dws calendar participant busy --user-ids <userId1,userId2> --start "<start>" --end "<end>"
```

**Example:**
```bash
# Find when everyone is free
dws calendar participant busy \
  --user-ids "user1,user2,user3" \
  --start "2024-03-29T09:00:00Z" \
  --end "2024-03-29T18:00:00Z" \
  --jq '.result.busySlots[] | {start: .startTime, end: .endTime}'
```

## Meeting Room Operations

### Search Meeting Rooms

```bash
dws calendar room search --keyword "<room-name>" [--city "<city>"]
```

**Example:**
```bash
dws calendar room search --keyword "Conference Room" --jq '.result[] | {name: .name, roomId: .roomId, capacity: .capacity}'
```

### Book Meeting Room

```bash
dws calendar room book --room-id <roomId> --start "<start>" --end "<end>" --title "<title>"
```

### Get Room Availability

```bash
dws calendar room get --room-id <roomId> --start "<start>" --end "<end>"
```

## Common Patterns

### Schedule Meeting with Room

```bash
# 1. Find free slot
dws calendar participant busy --user-ids "user1,user2" --start "2024-03-29T09:00:00Z" --end "2024-03-29T18:00:00Z"

# 2. Find available room
dws calendar room search --keyword "Conference Room" --jq '.result[0].roomId'

# 3. Create event with room
dws calendar event create --title "Meeting" --start "2024-03-29T14:00:00Z" --end "2024-03-29T15:00:00Z" --participants "user1,user2"
```

### Find Common Free Slots

See bundled script: `scripts/calendar_free_slot_finder.py`

### View Today's Agenda

```bash
dws calendar event list --start "$(date -I)T00:00:00Z" --end "$(date -I)T23:59:59Z" --jq '.result[] | "\(.startTime) - \(.title)"'
```


path: references/products/chat.md
content: # Chat Reference

## Message Operations

### Send Message as Bot

```bash
dws chat message send-by-bot --robot-code <BOT_CODE> --group <GROUP_ID> --title "<title>" --text "<content>"
```

**Parameters:**
- `--robot-code`: Bot robot code from DingTalk app settings
- `--group`: Group conversation ID
- `--title`: Message title (for markdown messages)
- `--text`: Message content (supports `@filename` for file input)

**Example:**
```bash
# Send from file
dws chat message send-by-bot --robot-code "robot123" --group "conv123" --title "Weekly Report" --text @report.md

# Send from stdin
cat message.md | dws chat message send-by-bot --robot-code "robot123" --group "conv123" --title "Update"
```

### Send Message as User

```bash
dws chat message send --conversation-id <convId> --text "<content>"
```

### Recall Message

```bash
dws chat message recall --conversation-id <convId> --message-id <msgId>
```

## Group Operations

### List Groups

```bash
dws chat group list [--limit <N>]
```

### Get Group Info

```bash
dws chat group get --group-id <groupId>
```

**Example:**
```bash
dws chat group get --group-id "conv123" --jq '.result | {name: .name, memberCount: .memberCount}'
```

### Create Group

```bash
dws chat group create --name "<group-name>" --user-ids <userId1,userId2>
```

### Add Members

```bash
dws chat group add-members --group-id <groupId> --user-ids <userId1,userId2>
```

### Remove Members

```bash
dws chat group remove-members --group-id <groupId> --user-ids <userId1,userId2>
```

## Topic/Thread Operations

### List Topics

```bash
dws chat topic list --group-id <groupId>
```

### Reply to Topic

```bash
dws chat topic reply --group-id <groupId> --topic-id <topicId> --text "<content>"
```

## Bot Operations

### Get Bot Info

```bash
dws chat bot get --robot-code <BOT_CODE>
```

### Send Single Chat Message

```bash
dws chat bot send-single --robot-code <BOT_CODE> --user-id <userId> --text "<content>"
```

## Common Patterns

### Send Message to Multiple Groups

```bash
# Get group IDs
dws chat group list --jq '.result[] | select(.name | contains("Project")) | .conversationId'

# Send to each (use script for batch)
for group in $GROUPS; do
  dws chat message send-by-bot --robot-code "robot123" --group "$group" --title "Notice" --text "Meeting at 3pm"
done
```

### Search Groups by Name

```bash
dws chat group list --jq '.result[] | select(.name | contains("Engineering")) | {id: .conversationId, name: .name}'
```

### Send Rich Text Message

```bash
# Markdown format
dws chat message send-by-bot --robot-code "robot123" --group "conv123" \
  --title "## Weekly Summary" \
  --text "### Completed\n- Task 1\n- Task 2\n\n### Next Week\n- Task 3"
```


path: references/products/contact.md
content: # Contact Reference

## User Operations

### Search Users

```bash
dws contact user search --keyword "<name|mobile|email>" [--limit <N>]
```

**Parameters:**
- `--keyword`: Search term (name, mobile, or email)
- `--limit`: Max results (default: 50)

**Example:**
```bash
dws contact user search --keyword "John" --jq '.result[] | {name: .orgUserName, userId: .userId, mobile: .mobile}'
```

### Get User by ID

```bash
dws contact user get --user-id <userId>
```

### Get Current User

```bash
dws contact user get-self
```

**Example:**
```bash
dws contact user get-self --jq '.result[0].orgEmployeeModel | {name: .orgUserName, dept: .depts[0].deptName, userId: .userId}'
```

### Batch Query Users

```bash
dws contact user batch-get --user-ids <id1,id2,id3>
```

## Department Operations

### Search Departments

```bash
dws contact dept search --keyword "<dept-name>"
```

### Get Department Info

```bash
dws contact dept get --dept-id <deptId>
```

### List Department Members

```bash
dws contact dept members --dept-id <deptId> [--limit <N>]
```

**Example:**
```bash
dws contact dept members --dept-id "12345" --jq '.result[] | {name: .name, userId: .userId}'
```

### List Sub-departments

```bash
dws contact dept sub-depts --dept-id <deptId>
```

## Common Patterns

### Find User's Department

```bash
# Get user info with department
dws contact user get --user-id <userId> --jq '.result.depts[0].deptName'
```

### List All Members in Multiple Departments

```bash
# First get dept IDs
dws contact dept search --keyword "Engineering" --jq '.result[] | .deptId'

# Then get members for each
dws contact dept members --dept-id <dept-id>
```

### Search by Mobile

```bash
dws contact user search --keyword "13800138000" --jq '.result[0] | {name: .orgUserName, userId: .userId}'
```


path: references/products/oa.md
content: # Approval (OA) Reference

## Approval Operations

### List Pending Approvals

```bash
dws oa approval list --status pending [--limit <N>]
```

**Example:**
```bash
dws oa approval list --status pending --jq '.result[] | {instanceId: .instanceId, title: .title, applicant: .applicantName}'
```

### List All Approvals

```bash
dws oa approval list [--status <pending|approved|rejected|all>] [--start <date>] [--end <date>]
```

### Get Approval Detail

```bash
dws oa approval get --instance-id <instanceId>
```

**Example:**
```bash
dws oa approval get --instance-id "inst123" --jq '.result | {title: .title, status: .status, applicant: .applicantName}'
```

### Approve Instance

```bash
dws oa approval approve --instance-id <instanceId> [--comment "<comment>"]
```

**Example:**
```bash
dws oa approval approve --instance-id "inst123" --comment "Approved" --yes
```

### Reject Instance

```bash
dws oa approval reject --instance-id <instanceId> --comment "<reason>"
```

**Example:**
```bash
dws oa approval reject --instance-id "inst123" --comment "Needs more details" --yes
```

### Revoke Instance

```bash
dws oa approval revoke --instance-id <instanceId>
```

## Process Operations

### List Approval Processes

```bash
dws oa process list
```

**Example:**
```bash
dws oa process list --jq '.result[] | {name: .name, processId: .processId}'
```

### Get Process Detail

```bash
dws oa process get --process-id <processId>
```

## Common Patterns

### Auto-Approve Low-Value Requests

```bash
# Get pending approvals
dws oa approval list --status pending --jq '.result[] | select(.title | contains("Expense")) | .instanceId'

# Approve each
for inst in $INSTANCES; do
  dws oa approval approve --instance-id "$inst" --comment "Auto-approved" --yes
done
```

### Batch Process Pending Approvals

```bash
# List with details
dws oa approval list --status pending --jq '.result[] | "\(.instanceId): \(.title) - \(.applicantName)"'

# Process one by one based on business logic
```

### Get My Pending Approvals

```bash
dws oa approval list --status pending --jq '.result[] | select(.handlerName == "Your Name") | {title: .title, instanceId: .instanceId}'
```


path: references/products/report.md
content: # Report Reference

## Report Operations

### List Received Reports

```bash
dws report list --type received [--start-date <date>] [--end-date <date>] [--limit <N>]
```

**Parameters:**
- `--type`: `received` | `sent` | `all`
- `--start-date`: Start date (YYYY-MM-DD)
- `--end-date`: End date (YYYY-MM-DD)

**Example:**
```bash
# Today's received reports
dws report list --type received --start-date "2024-03-29" --end-date "2024-03-29"

# With details
dws report list --type received --start-date "2024-03-29" --end-date "2024-03-29" --jq '.result[] | {title: .title, sender: .senderName, createTime: .createTime}'
```

See bundled script: `scripts/report_inbox_today.py`

### List Sent Reports

```bash
dws report list --type sent [--start-date <date>] [--end-date <date>]
```

### Get Report Detail

```bash
dws report get --report-id <reportId>
```

**Example:**
```bash
dws report get --report-id "report123" --jq '.result | {title: .title, content: .content, sender: .senderName}'
```

### Create Report

```bash
dws report create --template-id <templateId> --content "<content>" [--receivers <userIds>]
```

**Example:**
```bash
# From file
dws report create --template-id "tpl123" --content @daily_report.md --receivers "user1,user2"

# Direct content
dws report create --template-id "tpl123" --content "### Today's Work\n- Task 1\n- Task 2"
```

## Template Operations

### List Templates

```bash
dws report template list
```

**Example:**
```bash
dws report template list --jq '.result[] | {name: .templateName, templateId: .templateId}'
```

### Get Template Detail

```bash
dws report template get --template-id <templateId>
```

## Statistics Operations

### Get Report Statistics

```bash
dws report stats --start-date <date> --end-date <date> [--dept-id <deptId>]
```

**Example:**
```bash
dws report stats --start-date "2024-03-01" --end-date "2024-03-31" --jq '.result | {sentCount: .sentCount, receivedCount: .receivedCount}'
```

## Common Patterns

### Daily Report Workflow

```bash
# 1. Get template
dws report template list --jq '.result[] | select(.templateName | contains("Daily")) | .templateId'

# 2. Create report
dws report create --template-id "tpl123" --content @daily_report.md --receivers "manager_userId"
```

### View All Reports from Specific Person

```bash
# Get user's sent reports
dws report list --type sent --start-date "2024-03-01" --end-date "2024-03-31" --jq '.result[] | select(.senderName == "John") | {title: .title, date: .createTime}'
```

### Check Who Hasn't Submitted Report

```bash
# Get department members
dws contact dept members --dept-id "dept123" --jq '.result[] | .name'

# Get today's senders
dws report list --type sent --start-date "2024-03-29" --end-date "2024-03-29" --jq '.result[] | .senderName'

# Compare to find missing
```


path: references/products/todo.md
content: # Todo Reference

## Task Operations

### Create Task

```bash
dws todo task create --title "<title>" --executors "<userId1,userId2>" [--priority <1|2|3|4>] [--due-date "<date>"] [--description "<desc>"]
```

**Parameters:**
- `--title`: Task title (required)
- `--executors`: Comma-separated user IDs
- `--priority`: 1=Urgent, 2=High, 3=Normal, 4=Low
- `--due-date`: Due date (ISO 8601)
- `--description`: Task description

**Example:**
```bash
dws todo task create \
  --title "Review PR #123" \
  --executors "user123" \
  --priority 2 \
  --due-date "2024-04-01T18:00:00Z" \
  --yes
```

### List Tasks

```bash
dws todo task list [--status <pending|done|all>] [--limit <N>]
```

**Example:**
```bash
# Pending tasks
dws todo task list --status pending --jq '.result[] | {title: .title, due: .dueDate}'

# Count pending
dws todo task list --status pending --jq '.result | length'
```

### Get Task Detail

```bash
dws todo task get --task-id <taskId>
```

### Update Task

```bash
dws todo task update --task-id <taskId> [--title "<new-title>"] [--priority <new-priority>] [--due-date "<new-date>"]
```

### Mark as Done

```bash
dws todo task done --task-id <taskId>
```

### Delete Task

```bash
dws todo task delete --task-id <taskId>
```

## Common Patterns

### Batch Create Todos

See bundled script: `scripts/todo_batch_create.py`

```bash
# From JSON file
python scripts/todo_batch_create.py tasks.json
```

### Daily Todo Summary

See bundled script: `scripts/todo_daily_summary.py`

```bash
python scripts/todo_daily_summary.py
```

### Check Overdue Tasks

See bundled script: `scripts/todo_overdue_check.py`

```bash
python scripts/todo_overdue_check.py
```

### Create Todo for Self

```bash
# First get your userId
dws contact user get-self --jq '.result[0].userId'

# Then create todo
dws todo task create --title "Finish report" --executors "<your-userId>" --yes
```

### Assign Todo to Team

```bash
# Get department members
dws contact dept members --dept-id "dept123" --jq '.result[] | .userId' | tr '\n' ','

# Create with all members
dws todo task create --title "Team task" --executors "user1,user2,user3" --yes
```


path: scripts/calendar_free_slot_finder.py
content: #!/usr/bin/env python3
"""
Calendar Free Slot Finder

Find common free time slots across multiple participants.
Usage: python calendar_free_slot_finder.py --user-ids user1,user2,user3 --date 2024-03-29
"""

import argparse
import subprocess
import json
import sys
from datetime import datetime, timedelta


def run_dws(args):
    """Run dws command and return JSON output."""
    cmd = ["dws"] + args + ["--jq", ".result"]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error: {result.stderr}", file=sys.stderr)
        return None
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return None


def get_busy_slots(user_ids, start_time, end_time):
    """Get busy slots for all users."""
    result = run_dws([
        "calendar", "participant", "busy",
        "--user-ids", user_ids,
        "--start", start_time,
        "--end", end_time
    ])
    
    if not result:
        return []
    
    busy_slots = []
    for slot in result.get("busySlots", []):
        busy_slots.append({
            "start": slot.get("startTime"),
            "end": slot.get("endTime")
        })
    
    return busy_slots


def find_free_slots(busy_slots, work_start="09:00", work_end="18:00", slot_duration=30):
    """Find free slots given busy periods."""
    if not busy_slots:
        return []
    
    # Parse work hours
    work_start_h, work_start_m = map(int, work_start.split(":"))
    work_end_h, work_end_m = map(int, work_end.split(":"))
    
    # Sort busy slots by start time
    busy_slots.sort(key=lambda x: x["start"])
    
    free_slots = []
    current_time = None
    
    for busy in busy_slots:
        busy_start = datetime.fromisoformat(busy["start"].replace("Z", "+00:00"))
        busy_end = datetime.fromisoformat(busy["end"].replace("Z", "+00:00"))
        
        if current_time is None:
            current_time = busy_start.replace(hour=work_start_h, minute=work_start_m)
        
        if current_time < busy_start:
            # Found a free slot
            duration = (busy_start - current_time).total_seconds() / 60
            if duration >= slot_duration:
                free_slots.append({
                    "start": current_time.isoformat(),
                    "end": busy_start.isoformat(),
                    "duration_minutes": duration
                })
        
        current_time = max(current_time, busy_end)
    
    # Check end of day
    work_end_time = current_time.replace(hour=work_end_h, minute=work_end_m)
    if current_time < work_end_time:
        duration = (work_end_time - current_time).total_seconds() / 60
        if duration >= slot_duration:
            free_slots.append({
                "start": current_time.isoformat(),
                "end": work_end_time.isoformat(),
                "duration_minutes": duration
            })
    
    return free_slots


def main():
    parser = argparse.ArgumentParser(description="Find common free slots across participants")
    parser.add_argument("--user-ids", required=True, help="Comma-separated user IDs")
    parser.add_argument("--date", required=True, help="Date (YYYY-MM-DD)")
    parser.add_argument("--work-start", default="09:00", help="Work start time (HH:MM)")
    parser.add_argument("--work-end", default="18:00", help="Work end time (HH:MM)")
    parser.add_argument("--slot-duration", type=int, default=30, help="Minimum slot duration in minutes")
    
    args = parser.parse_args()
    
    # Convert date to ISO format
    start_time = f"{args.date}T00:00:00Z"
    end_time = f"{args.date}T23:59:59Z"
    
    print(f"Finding free slots for {args.user_ids} on {args.date}...")
    
    # Get busy slots
    busy_slots = get_busy_slots(args.user_ids, start_time, end_time)
    
    if busy_slots is None:
        print("Failed to get busy slots", file=sys.stderr)
        sys.exit(1)
    
    print(f"Found {len(busy_slots)} busy periods")
    
    # Find free slots
    free_slots = find_free_slots(
        busy_slots,
        args.work_start,
        args.work_end,
        args.slot_duration
    )
    
    if not free_slots:
        print("No common free slots found")
        sys.exit(0)
    
    print(f"\nAvailable time slots:")
    for slot in free_slots:
        start = datetime.fromisoformat(slot["start"].replace("Z", "+00:00"))
        end = datetime.fromisoformat(slot["end"].replace("Z", "+00:00"))
        print(f"  {start.strftime('%H:%M')} - {end.strftime('%H:%M')} ({slot['duration_minutes']} min)")


if __name__ == "__main__":
    main()


path: scripts/calendar_schedule_meeting.py
content: #!/usr/bin/env python3
"""
Calendar Meeting Scheduler

Create event + add participants + find & book meeting room.
Usage: python calendar_schedule_meeting.py --title "Team Meeting" --date 2024-03-29 --time 14:00 --duration 60 --users user1,user2
"""

import argparse
import subprocess
import json
import sys
from datetime import datetime, timedelta


def run_dws(args):
    """Run dws command and return JSON output."""
    cmd = ["dws"] + args
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error: {result.stderr}", file=sys.stderr)
        return None
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return None


def run_dws_action(args, dry_run=False):
    """Run dws action command."""
    cmd = ["dws"] + args
    if dry_run:
        cmd.append("--dry-run")
    else:
        cmd.append("--yes")
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error: {result.stderr}", file=sys.stderr)
        return False
    print(result.stdout)
    return True


def search_meeting_room(keyword="Meeting Room"):
    """Search for available meeting rooms."""
    result = run_dws(["calendar", "room", "search", "--keyword", keyword])
    if not result:
        return []
    return result.get("result", [])


def create_event(title, start_time, end_time, participants=None, location=None, dry_run=False):
    """Create calendar event."""
    args = [
        "calendar", "event", "create",
        "--title", title,
        "--start", start_time,
        "--end", end_time
    ]
    
    if participants:
        args.extend(["--participants", participants])
    if location:
        args.extend(["--location", location])
    
    return run_dws_action(args, dry_run)


def main():
    parser = argparse.ArgumentParser(description="Schedule meeting with room booking")
    parser.add_argument("--title", required=True, help="Meeting title")
    parser.add_argument("--date", required=True, help="Meeting date (YYYY-MM-DD)")
    parser.add_argument("--time", default="14:00", help="Meeting time (HH:MM)")
    parser.add_argument("--duration", type=int, default=60, help="Duration in minutes")
    parser.add_argument("--users", help="Participant user IDs (comma-separated)")
    parser.add_argument("--room-keyword", default="Meeting Room", help="Meeting room search keyword")
    parser.add_argument("--dry-run", action="store_true", help="Preview without booking")
    
    args = parser.parse_args()
    
    # Calculate start and end times
    start_dt = datetime.strptime(f"{args.date} {args.time}", "%Y-%m-%d %H:%M")
    end_dt = start_dt + timedelta(minutes=args.duration)
    
    start_iso = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
    end_iso = end_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
    
    print(f"Scheduling meeting:")
    print(f"  Title: {args.title}")
    print(f"  Date: {args.date}")
    print(f"  Time: {args.time} ({args.duration} min)")
    print(f"  Participants: {args.users or 'None'}")
    
    if args.dry_run:
        print("\nDry run - no booking will be made")
    
    # Search for meeting room
    print(f"\nSearching for meeting room: {args.room_keyword}")
    rooms = search_meeting_room(args.room_keyword)
    
    if not rooms:
        print("No meeting rooms found")
        sys.exit(1)
    
    print(f"Found {len(rooms)} room(s):")
    for room in rooms[:3]:  # Show first 3
        print(f"  - {room.get('name', 'Unknown')} (Capacity: {room.get('capacity', 'N/A')})")
    
    # Use first available room
    room = rooms[0]
    room_name = room.get("name", "Unknown")
    print(f"\nSelected room: {room_name}")
    
    # Create event
    print(f"\nCreating calendar event...")
    if create_event(args.title, start_iso, end_iso, args.users, room_name, args.dry_run):
        print("Meeting scheduled successfully!")
    else:
        print("Failed to schedule meeting")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/contact_dept_members.py
content: #!/usr/bin/env python3
"""
Contact Department Members

Search for a department by name and list all members.
Usage: python contact_dept_members.py --keyword "Engineering"
"""

import argparse
import subprocess
import json
import sys


def run_dws(args):
    """Run dws command and return JSON output."""
    cmd = ["dws"] + args
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error: {result.stderr}", file=sys.stderr)
        return None
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return None


def search_department(keyword):
    """Search for department by keyword."""
    result = run_dws(["contact", "dept", "search", "--keyword", keyword])
    if not result:
        return []
    return result.get("result", [])


def get_department_members(dept_id):
    """Get all members of a department."""
    result = run_dws(["contact", "dept", "members", "--dept-id", dept_id])
    if not result:
        return []
    return result.get("result", [])


def main():
    parser = argparse.ArgumentParser(description="Search department and list members")
    parser.add_argument("--keyword", required=True, help="Department name keyword")
    parser.add_argument("--format", choices=["table", "json", "csv"], default="table")
    
    args = parser.parse_args()
    
    # Search for department
    print(f"Searching for department: {args.keyword}")
    depts = search_department(args.keyword)
    
    if not depts:
        print("No departments found")
        sys.exit(0)
    
    print(f"Found {len(depts)} department(s):")
    for dept in depts:
        print(f"  - {dept.get('deptName', 'Unknown')} (ID: {dept.get('deptId', 'N/A')})")
    
    # Get members for first result
    dept = depts[0]
    dept_id = dept.get("deptId")
    dept_name = dept.get("deptName", "Unknown")
    
    print(f"\nGetting members of '{dept_name}'...")
    members = get_department_members(dept_id)
    
    if not members:
        print("No members found")
        sys.exit(0)
    
    print(f"\nFound {len(members)} member(s):\n")
    
    if args.format == "json":
        print(json.dumps(members, indent=2, ensure_ascii=False))
    elif args.format == "csv":
        print("name,userId,mobile")
        for member in members:
            name = member.get("name", member.get("orgUserName", ""))
            user_id = member.get("userId", "")
            mobile = member.get("mobile", "")
            print(f"{name},{user_id},{mobile}")
    else:  # table
        # Calculate column widths
        names = [m.get("name", m.get("orgUserName", "")) for m in members]
        ids = [m.get("userId", "") for m in members]
        
        name_width = max(len("Name"), max(len(n) for n in names) if names else 0)
        id_width = max(len("User ID"), max(len(i) for i in ids) if ids else 0)
        
        print(f"{'Name':<{name_width}} | {'User ID':<{id_width}} | Mobile")
        print("-" * (name_width + id_width + 15))
        
        for member in members:
            name = member.get("name", member.get("orgUserName", ""))
            user_id = member.get("userId", "")
            mobile = member.get("mobile", "")
            print(f"{name:<{name_width}} | {user_id:<{id_width}} | {mobile or 'N/A'}")
    
    print(f"\nTotal: {len(members)} members")


if __name__ == "__main__":
    main()


path: scripts/import_records.py
content: #!/usr/bin/env python3
"""
AITable Record Importer

Import records from CSV file into AITable.
Usage: python import_records.py --base-id BASE_ID --table-id TABLE_ID data.csv
"""

import argparse
import subprocess
import json
import sys
import csv


def run_dws(args):
    """Run dws command."""
    cmd = ["dws"] + args + ["--yes"]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error: {result.stderr}", file=sys.stderr)
        return False
    return True


def import_record(base_id, table_id, fields):
    """Import a single record."""
    args = [
        "aitable", "record", "create",
        "--base-id", base_id,
        "--table-id", table_id,
        "--fields", json.dumps(fields, ensure_ascii=False)
    ]
    return run_dws(args)


def main():
    parser = argparse.ArgumentParser(description="Import records from CSV into AITable")
    parser.add_argument("csv_file", help="CSV file to import")
    parser.add_argument("--base-id", required=True, help="AITable Base ID")
    parser.add_argument("--table-id", required=True, help="AITable Table ID")
    parser.add_argument("--dry-run", action="store_true", help="Preview without importing")
    parser.add_argument("--batch-size", type=int, default=10, help="Batch size for imports")
    
    args = parser.parse_args()
    
    # Read CSV file
    try:
        with open(args.csv_file, "r", encoding="utf-8") as f:
            reader = csv.DictReader(f)
            records = list(reader)
    except FileNotFoundError:
        print(f"Error: File not found: {args.csv_file}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error reading CSV: {e}", file=sys.stderr)
        sys.exit(1)
    
    if not records:
        print("No records found in CSV file")
        sys.exit(0)
    
    print(f"Found {len(records)} records to import")
    print(f"Base ID: {args.base_id}")
    print(f"Table ID: {args.table_id}")
    print(f"Fields: {', '.join(records[0].keys())}")
    
    if args.dry_run:
        print("\nDry run - no records will be imported")
        print(f"\nFirst record preview:")
        print(json.dumps(records[0], indent=2, ensure_ascii=False))
        sys.exit(0)
    
    # Import records
    success_count = 0
    error_count = 0
    
    print(f"\nImporting records...")
    for i, record in enumerate(records, 1):
        # Clean up field names (remove whitespace, handle special chars)
        fields = {k.strip(): v.strip() if isinstance(v, str) else v for k, v in record.items()}
        
        print(f"[{i}/{len(records)}] Importing: {fields.get('name', fields.get('title', 'N/A'))}")
        
        if import_record(args.base_id, args.table_id, fields):
            success_count += 1
        else:
            error_count += 1
            print(f"  Failed to import record {i}")
        
        # Progress indicator
        if i % args.batch_size == 0:
            print(f"  Progress: {i}/{len(records)} records processed")
    
    print(f"\nImport completed:")
    print(f"  Success: {success_count}")
    print(f"  Errors: {error_count}")
    print(f"  Total: {len(records)}")


if __name__ == "__main__":
    main()


path: scripts/report_inbox_today.py
content: #!/usr/bin/env python3
"""
Report Inbox Today

View today's received reports with details.
Usage: python report_inbox_today.py [--date 2024-03-29]
"""

import argparse
import subprocess
import json
import sys
from datetime import datetime


def run_dws(args):
    """Run dws command and return JSON output."""
    cmd = ["dws"] + args
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error: {result.stderr}", file=sys.stderr)
        return None
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return None


def get_received_reports(date):
    """Get received reports for a specific date."""
    result = run_dws([
        "report", "list",
        "--type", "received",
        "--start-date", date,
        "--end-date", date
    ])
    if not result:
        return []
    return result.get("result", [])


def main():
    parser = argparse.ArgumentParser(description="View today's received reports")
    parser.add_argument("--date", help="Date (YYYY-MM-DD), defaults to today")
    parser.add_argument("--format", choices=["table", "json"], default="table")
    
    args = parser.parse_args()
    
    # Use today's date if not specified
    if args.date:
        date = args.date
    else:
        date = datetime.now().strftime("%Y-%m-%d")
    
    print(f"Received reports for {date}:\n")
    
    reports = get_received_reports(date)
    
    if not reports:
        print("No reports received today")
        sys.exit(0)
    
    if args.format == "json":
        print(json.dumps(reports, indent=2, ensure_ascii=False))
    else:  # table
        # Calculate column widths
        titles = [r.get("title", "") for r in reports]
        senders = [r.get("senderName", "") for r in reports]
        times = [r.get("createTime", "")[:16] for r in reports]
        
        title_width = max(len("Title"), max(len(t) for t in titles) if titles else 0)
        sender_width = max(len("Sender"), max(len(s) for s in senders) if senders else 0)
        time_width = max(len("Time"), max(len(t) for t in times) if times else 0)
        
        print(f"{'Title':<{title_width}} | {'Sender':<{sender_width}} | {'Time':<{time_width}} | Status")
        print("-" * (title_width + sender_width + time_width + 15))
        
        for report in reports:
            title = report.get("title", "")[:50]  # Truncate long titles
            sender = report.get("senderName", "Unknown")
            time = report.get("createTime", "")[:16]
            status = report.get("status", "")
            print(f"{title:<{title_width}} | {sender:<{sender_width}} | {time:<{time_width}} | {status}")
    
    print(f"\nTotal: {len(reports)} report(s)")


if __name__ == "__main__":
    main()


path: scripts/todo_batch_create.py
content: #!/usr/bin/env python3
"""
Todo Batch Creator

Create multiple todos from a JSON file.
Usage: python todo_batch_create.py tasks.json [--executors user1,user2]
"""

import argparse
import subprocess
import json
import sys


def run_dws(args, dry_run=False):
    """Run dws command."""
    cmd = ["dws"] + args
    if dry_run:
        cmd.append("--dry-run")
    else:
        cmd.append("--yes")
    
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error: {result.stderr}", file=sys.stderr)
        return False
    print(result.stdout)
    return True


def create_todo(title, executors, priority=None, due_date=None, description=None, dry_run=False):
    """Create a single todo."""
    args = [
        "todo", "task", "create",
        "--title", title,
        "--executors", executors
    ]
    
    if priority:
        args.extend(["--priority", str(priority)])
    if due_date:
        args.extend(["--due-date", due_date])
    if description:
        args.extend(["--description", description])
    
    return run_dws(args, dry_run)


def main():
    parser = argparse.ArgumentParser(description="Batch create todos from JSON")
    parser.add_argument("input_file", help="JSON file with tasks")
    parser.add_argument("--executors", help="Default executors (comma-separated)")
    parser.add_argument("--dry-run", action="store_true", help="Preview without creating")
    
    args = parser.parse_args()
    
    # Load tasks
    try:
        with open(args.input_file, "r") as f:
            tasks = json.load(f)
    except FileNotFoundError:
        print(f"Error: File not found: {args.input_file}", file=sys.stderr)
        sys.exit(1)
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON: {e}", file=sys.stderr)
        sys.exit(1)
    
    # Ensure tasks is a list
    if isinstance(tasks, dict):
        tasks = tasks.get("tasks", [tasks])
    
    print(f"Processing {len(tasks)} tasks...")
    
    success_count = 0
    for i, task in enumerate(tasks, 1):
        title = task.get("title")
        if not title:
            print(f"Task {i}: Missing title, skipping")
            continue
        
        executors = task.get("executors", args.executors)
        if not executors:
            print(f"Task {i}: Missing executors, skipping")
            continue
        
        priority = task.get("priority")
        due_date = task.get("due_date") or task.get("due-date")
        description = task.get("description")
        
        print(f"\n[{i}/{len(tasks)}] Creating: {title}")
        if create_todo(title, executors, priority, due_date, description, args.dry_run):
            success_count += 1
    
    print(f"\nCompleted: {success_count}/{len(tasks)} tasks created")


if __name__ == "__main__":
    main()


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