Skill: ohano/ai-course-agent (version 0.2.1)

--- name: ai-course-agent description: Auto-generates AI education courses from natural language requests in Chinese. Detects patterns like "帮我生成6年级数学分数乘除法的课程" and calls Edustem API to create and return a course link. Uses SkillPay for usage-based billing (1 token per course). metadata: { "openclaw": { "requires": { "bins": [] }, "env": [ { "key": "EDUSTEM_USERNAME", "description": "Edustem API username (email)", "required": true, "secret": true }, { "key": "EDUSTEM_PASSWORD", "description": "Edustem API password", "required": true, "secret": true } ] } } --- # AI Course Agent OpenClaw Skill for auto-generating AI education courses. Detects natural language course generation requests and calls the Edustem API to create ready-to-use course content. ## 💳 Billing & Pricing This skill uses **SkillPay** for usage-based billing: - **Rate:** 1 token per course generation - **Pricing:** 1 USDT = 1000 tokens - **Minimum deposit:** 8 USDT (8000 tokens = 8000 courses) - **Payment:** USDT cryptocurrency via SkillPay When your balance runs out, the skill will return a payment link for top-up. ## Quick Start ```typescript import { isCourseLessonRequest, processUserMessage } from 'ai-course-agent'; // When user sends a message: const userId = req.user.sub; // Get user ID from your auth system if (isCourseLessonRequest(userInput)) { const response = await processUserMessage(userInput, userId); // Success: // "✅ 成功为6年级数学《分数乘除法》生成课程！\n\n📚 课程链接: https://..." // Insufficient balance: // "❌ 余额不足 (当前: 0 tokens)\n\n💳 请充值后继续使用: https://skillpay.me/..." } ``` ## Configuration Set environment variables before use: ### Edustem API (Required) ```bash export EDUSTEM_USERNAME="[REDACTED_SECRET]" export EDUSTEM_PASSWORD="your-password" ``` ### SkillPay Billing **No configuration needed.** SkillPay credentials are hardcoded in the skill and belong to the skill author. Payments are automatically deducted from your SkillPay balance. ## Supported Input Patterns ``` 帮我生成6年级数学分数乘除法的课程 帮我创建一个七年级语文从百草园到三味书屋的课程 帮我制作9年级英语日常会话的课程 生成8年级科学地球和宇宙的课程 ``` Supports both Arabic (6年级) and Chinese (六年级) numerals for grade levels. ## Supported Subjects 数学 · 语文 · 英语 · 科学 · 历史 · 地理 · 物理 · 化学 · 生物 ## Output Format ``` ✅ 成功为6年级数学《分数乘除法》生成课程！ 📚 课程链接: https://your-api-host/ai-lesson/{lesson_ref} ``` ## API Flow 0. `handleBilling(userId)` — Charge user via SkillPay (1 token) 1. `login()` — Authenticate and get JWT token 2. `createLessonPlan()` — Create lesson plan with metadata 3. `acceptLessonPlan()` — Confirm and trigger course generation 4. Return course URL (or payment link if balance insufficient) ## Exports ```typescript // Main integration functions isCourseLessonRequest(message: string): boolean processUserMessage(userInput: string, userId: string): Promise<string> // Core functions generateCourse(request: CourseRequest, userId: string): Promise<GeneratedCourseResponse> parseCourseRequest(userInput: string): CourseRequest | null // SkillPay Billing handleBilling(userId: string): Promise<BillingResult> chargeUser(userId: string, amount?: number): Promise<ChargeResult> getBalance(userId: string): Promise<number> getPaymentLink(userId: string, amount?: number): Promise<string> // Edustem API (lower level) login(username, password): Promise<string> createLessonPlan(token, payload): Promise<CreateLessonPlanResponse> acceptLessonPlan(token, lessonRef): Promise<AcceptLessonPlanResponse> generateLessonUrl(lessonRef): string ``` ## Tech Stack TypeScript · Node.js · axios · form-data

path: API_REQUIREMENTS.md
content: # API Requirements Document

## Overview

This document will be filled with API specifications once provided.

### 1. Authentication

- **Method**: (to be filled)
- **Credentials**: (to be filled)
- **Token Format**: (to be filled)
- **Token Expiration**: (to be filled)

### 2. Course Generation Endpoint

- **URL**: (to be filled)
- **Method**: (to be filled)
- **Headers**: (to be filled)

#### Request Parameters

| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| grade | | | |
| subject | | | |
| content | | | |

#### Response Format

```json
{
  "comment": "Expected response structure to be filled"
}
```

### 3. Error Handling

- **Error Codes**: (to be filled)
- **Retry Logic**: (to be filled)

### 4. Rate Limiting

(to be filled)



path: README.md
content: # AI Course Agent

An OpenClaw agent for generating AI education course content using the Edustem API.

## Overview

This agent handles course generation requests by:
1. **Billing** - Charges user via SkillPay (1 token per course generation)
2. **Parsing** user input to extract grade level, subject, and content
3. **Gathering** curriculum information via placeholder content
4. **Creating** a lesson plan via Edustem API using the `api-edustem-skill` Skill
5. **Accepting** the lesson plan to trigger course generation
6. **Returning** the generated course link

## 💳 Billing & Pricing

This skill uses **SkillPay** for usage-based billing:

- **Rate:** 1 token per course generation
- **Pricing:** 1 USDT = 1000 tokens
- **Minimum deposit:** 8 USDT
- **Payment:** USDT cryptocurrency

When balance is insufficient, the agent will return a payment link for top-up.

## Request Example

```
User: "帮我生成6年级数学分数乘除法的课程"

Agent:
1. Billing → chargeUser(userId) → success (1 token deducted)
2. Parses → grade: 6, subject: 数学, content: 分数乘除法
3. Gathers content (curriculum_text, elaborations, teacher_notes)
4. Calls Skill:
   - login(username, password) → token
   - createLessonPlan(token, metadata) → lesson_ref
   - acceptLessonPlan(token, lesson_ref) → confirmed
5. Returns → https://6bb95bf119bf.ngrok-free.app/ai-lesson/{lesson_ref}
```

### Insufficient Balance Example

```
User: "帮我生成6年级数学分数乘除法的课程"

Agent:
1. Billing → chargeUser(userId) → failed (balance: 0)
2. Returns → ❌ 余额不足 (当前: 0 tokens)
             💳 请充值后继续使用: https://skillpay.me/payment/...
             充值说明: 1 USDT = 1000 tokens, 最低充值 8 USDT
```

## Architecture

```
User Request
  ↓
Agent (src/agent.ts)
  ├─ Parse input
  ├─ Gather curriculum
  ├─ Call Skill functions
  └─ Return course link
  ↓
Skill (api-edustem-skill)
  ├─ login()
  ├─ createLessonPlan()
  └─ acceptLessonPlan()
  ↓
Edustem API
```

## Installation

```bash
# Install dependencies
npm install

# Build TypeScript
npm run build

# Test locally
npm run test

# Run dev mode
npm run dev
```

## Usage

### Local Testing

```bash
npm run test
```

This runs local tests with sample course requests.

### In OpenClaw (Production)

```typescript
import { generateCourse, parseCourseRequest } from './agent';

// In your agent code:
const userId = req.user.sub; // Or any user identifier
const request = parseCourseRequest(userInput);
const result = await generateCourse(request, userId);

if (result.success) {
  // Send result.courseLink to user
} else {
  // Handle error (may include payment link if insufficient balance)
  // result.message contains the error/payment info
}
```

### Quick Integration (Simplified)

```typescript
import { processUserMessage } from './integration';

// When user sends a message:
const userId = req.user.sub; // Get from your auth system
const response = await processUserMessage(userInput, userId);

// Send response back to user
// Response will include course link or payment link if balance insufficient
```

## Key Files

- **src/agent.ts** - Main orchestration logic
  - `generateCourse()` - Full flow from request to course link
  - `parseCourseRequest()` - Extract grade/subject/content from user input

- **src/utils.ts** - Helper functions
  - `gatherCurriculumContent()` - Fetch course metadata
  - `generateTeacherNotes()` - Format teacher instructions (with 12 trailing dots!)
  - `validateMetadata()` - Validate metadata before API call

- **src/test.ts** - Local test cases

## Important Notes

### Teacher Notes Format

The `teacher_notes` parameter has a **strict format requirement**:

```
"我需要你帮我生成这个主题的课程内容，特别是需要讲解{{topic}}。...让学生参与度高。。。。。。。。。。。。"
                                                                         ^12 dots required^
```

The **12 trailing dots (。)** are MANDATORY. The Edustem API uses them as an end-of-instruction marker. Missing them will cause the API to reject the request.

### Supported Subjects

- 数学 (Math)
- 语文 (Chinese)
- 英语 (English)
- 科学 (Science)
- 历史 (History)
- 地理 (Geography)
- 物理 (Physics)
- 化学 (Chemistry)
- 生物 (Biology)

### Input Format

User input should follow this pattern:
```
帮我生成{{grade}}年级{{subject}}{{content}}的课程
```

Example:
```
帮我生成6年级数学分数乘除法的课程
帮我创建一个七年级语文从百草园到三味书屋的课程
```

## Environment Variables

Configure credentials via environment variables (never hardcode!):

### Edustem API Credentials (Required)

```bash
export EDUSTEM_USERNAME="[REDACTED_SECRET]"
export EDUSTEM_PASSWORD="your-password"
```

Or use a `.env` file (not committed to git):
```
EDUSTEM_USERNAME=[REDACTED_SECRET]
EDUSTEM_PASSWORD=your-password
```

### SkillPay Billing

**Note:** SkillPay credentials (API key and Skill ID) are **hardcoded in the skill** and belong to the skill author. End users do not need to configure these. Payments are automatically deducted from the user's SkillPay balance and sent to the skill author's account.

## Error Handling

The agent handles these error cases:
- Failed to parse user input → Returns null
- Login failure → Returns error message
- Lesson plan creation failure → Returns API error
- Network errors → Returns error message with details

## Future Improvements

- [ ] Add web search for real curriculum content
- [ ] Implement LLM-based content generation
- [ ] Add caching for frequently requested courses
- [ ] Implement retry logic with exponential backoff
- [ ] Support more subjects and locales
- [ ] Add structured logging
- [ ] Move credentials to .env file
- [ ] Add input validation and sanitization

## Tech Stack

- **Language:** TypeScript
- **Runtime:** Node.js
- **HTTP:** axios
- **Skill:** api-edustem-skill

## License

MIT


path: package-lock.json
content: {
  "name": "ai-course-agent",
  "version": "0.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "ai-course-agent",
      "version": "0.1.0",
      "license": "MIT",
      "dependencies": {
        "axios": "^1.6.0",
        "form-data": "^4.0.0"
      },
      "devDependencies": {
        "@types/node": "^20.0.0",
        "eslint": "^8.0.0",
        "prettier": "^3.0.0",
        "ts-node": "^10.9.0",
        "typescript": "^5.0.0"
      }
    },
    "node_modules/@cspotcode/source-map-support": {
      "version": "0.8.1",
      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "@jridgewell/trace-mapping": "0.3.9"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/@eslint-community/eslint-utils": {
      "version": "4.9.1",
      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "eslint-visitor-keys": "^3.4.3"
      },
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      },
      "peerDependencies": {
        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
      }
    },
    "node_modules/@eslint-community/regexpp": {
      "version": "4.12.2",
      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
      }
    },
    "node_modules/@eslint/eslintrc": {
      "version": "2.1.4",
      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "ajv": "^6.12.4",
        "debug": "^4.3.2",
        "espree": "^9.6.0",
        "globals": "^13.19.0",
        "ignore": "^5.2.0",
        "import-fresh": "^3.2.1",
        "js-yaml": "^4.1.0",
        "minimatch": "^3.1.2",
        "strip-json-comments": "^3.1.1"
      },
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/@eslint/js": {
      "version": "8.57.1",
      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      }
    },
    "node_modules/@humanwhocodes/config-array": {
      "version": "0.13.0",
      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Use @eslint/config-array instead",
      "dev": true,
      "dependencies": {
        "@humanwhocodes/object-schema": "^2.0.3",
        "debug": "^4.3.1",
        "minimatch": "^3.0.5"
      },
      "engines": {
        "node": ">=10.10.0"
      }
    },
    "node_modules/@humanwhocodes/module-importer": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=12.22"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/nzakas"
      }
    },
    "node_modules/@humanwhocodes/object-schema": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Use @eslint/object-schema instead",
      "dev": true
    },
    "node_modules/@jridgewell/resolve-uri": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/@jridgewell/sourcemap-codec": {
      "version": "1.5.5",
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/@jridgewell/trace-mapping": {
      "version": "0.3.9",
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "@jridgewell/resolve-uri": "^3.0.3",
        "@jridgewell/sourcemap-codec": "^1.4.10"
      }
    },
    "node_modules/@nodelib/fs.scandir": {
      "version": "2.1.5",
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "@nodelib/fs.stat": "2.0.5",
        "run-parallel": "^1.1.9"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/@nodelib/fs.stat": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/@nodelib/fs.walk": {
      "version": "1.2.8",
      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "@nodelib/fs.scandir": "2.1.5",
        "fastq": "^1.6.0"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/@tsconfig/node10": {
      "version": "1.0.12",
      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/@tsconfig/node12": {
      "version": "1.0.11",
      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/@tsconfig/node14": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/@tsconfig/node16": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/@types/node": {
      "version": "20.19.33",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/@ungap/structured-clone": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/acorn": {
      "version": "8.15.0",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "bin": {
        "acorn": "bin/acorn"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/acorn-jsx": {
      "version": "5.3.2",
      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "peerDependencies": {
        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
      }
    },
    "node_modules/acorn-walk": {
      "version": "8.3.4",
      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "acorn": "^8.11.0"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/ajv": {
      "version": "6.12.6",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "fast-deep-equal": "^3.1.1",
        "fast-json-stable-stringify": "^2.0.0",
        "json-schema-traverse": "^0.4.1",
        "uri-js": "^4.2.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/arg": {
      "version": "4.1.3",
      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/argparse": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/axios": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "follow-redirects": "^1.15.11",
        "form-data": "^4.0.5",
        "proxy-from-env": "^1.1.0"
      }
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/brace-expansion": {
      "version": "1.1.12",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "balanced-match": "^1.0.0",
        "concat-map": "0.0.1"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/callsites": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/create-require": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/deep-is": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/diff": {
      "version": "4.0.4",
      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=0.3.1"
      }
    },
    "node_modules/doctrine": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "esutils": "^2.0.2"
      },
      "engines": {
        "node": ">=6.0.0"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escape-string-regexp": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/eslint": {
      "version": "8.57.1",
      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
      "dev": true,
      "dependencies": {
        "@eslint-community/eslint-utils": "^4.2.0",
        "@eslint-community/regexpp": "^4.6.1",
        "@eslint/eslintrc": "^2.1.4",
        "@eslint/js": "8.57.1",
        "@humanwhocodes/config-array": "^0.13.0",
        "@humanwhocodes/module-importer": "^1.0.1",
        "@nodelib/fs.walk": "^1.2.8",
        "@ungap/structured-clone": "^1.2.0",
        "ajv": "^6.12.4",
        "chalk": "^4.0.0",
        "cross-spawn": "^7.0.2",
        "debug": "^4.3.2",
        "doctrine": "^3.0.0",
        "escape-string-regexp": "^4.0.0",
        "eslint-scope": "^7.2.2",
        "eslint-visitor-keys": "^3.4.3",
        "espree": "^9.6.1",
        "esquery": "^1.4.2",
        "esutils": "^2.0.2",
        "fast-deep-equal": "^3.1.3",
        "file-entry-cache": "^6.0.1",
        "find-up": "^5.0.0",
        "glob-parent": "^6.0.2",
        "globals": "^13.19.0",
        "graphemer": "^1.4.0",
        "ignore": "^5.2.0",
        "imurmurhash": "^0.1.4",
        "is-glob": "^4.0.0",
        "is-path-inside": "^3.0.3",
        "js-yaml": "^4.1.0",
        "json-stable-stringify-without-jsonify": "^1.0.1",
        "levn": "^0.4.1",
        "lodash.merge": "^4.6.2",
        "minimatch": "^3.1.2",
        "natural-compare": "^1.4.0",
        "optionator": "^0.9.3",
        "strip-ansi": "^6.0.1",
        "text-table": "^0.2.0"
      },
      "bin": {
        "eslint": "bin/eslint.js"
      },
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/eslint-scope": {
      "version": "7.2.2",
      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "esrecurse": "^4.3.0",
        "estraverse": "^5.2.0"
      },
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/eslint-visitor-keys": {
      "version": "3.4.3",
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/espree": {
      "version": "9.6.1",
      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "acorn": "^8.9.0",
        "acorn-jsx": "^5.3.2",
        "eslint-visitor-keys": "^3.4.1"
      },
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/esquery": {
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "estraverse": "^5.1.0"
      },
      "engines": {
        "node": ">=0.10"
      }
    },
    "node_modules/esrecurse": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "estraverse": "^5.2.0"
      },
      "engines": {
        "node": ">=4.0"
      }
    },
    "node_modules/estraverse": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=4.0"
      }
    },
    "node_modules/esutils": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/fast-json-stable-stringify": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/fast-levenshtein": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/fastq": {
      "version": "1.20.1",
      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "reusify": "^1.0.4"
      }
    },
    "node_modules/file-entry-cache": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "flat-cache": "^3.0.4"
      },
      "engines": {
        "node": "^10.12.0 || >=12.0.0"
      }
    },
    "node_modules/find-up": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "locate-path": "^6.0.0",
        "path-exists": "^4.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/flat-cache": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "flatted": "^3.2.9",
        "keyv": "^4.5.3",
        "rimraf": "^3.0.2"
      },
      "engines": {
        "node": "^10.12.0 || >=12.0.0"
      }
    },
    "node_modules/flatted": {
      "version": "3.3.3",
      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/follow-redirects": {
      "version": "1.15.11",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/glob": {
      "version": "7.2.3",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [REDACTED_SECRET]",
      "dev": true,
      "dependencies": {
        "fs.realpath": "^1.0.0",
        "inflight": "^1.0.4",
        "inherits": "2",
        "minimatch": "^3.1.1",
        "once": "^1.3.0",
        "path-is-absolute": "^1.0.0"
      },
      "engines": {
        "node": "*"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/glob-parent": {
      "version": "6.0.2",
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "is-glob": "^4.0.3"
      },
      "engines": {
        "node": ">=10.13.0"
      }
    },
    "node_modules/globals": {
      "version": "13.24.0",
      "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "type-fest": "^0.20.2"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/graphemer": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ignore": {
      "version": "5.3.2",
      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">= 4"
      }
    },
    "node_modules/import-fresh": {
      "version": "3.3.1",
      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "parent-module": "^1.0.0",
        "resolve-from": "^4.0.0"
      },
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/imurmurhash": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=0.8.19"
      }
    },
    "node_modules/inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
      "dev": true,
      "dependencies": {
        "once": "^1.3.0",
        "wrappy": "1"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/is-extglob": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-glob": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "is-extglob": "^2.1.1"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-path-inside": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/js-yaml": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "argparse": "^2.0.1"
      },
      "bin": {
        "js-yaml": "bin/js-yaml.js"
      }
    },
    "node_modules/json-buffer": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/json-schema-traverse": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/json-stable-stringify-without-jsonify": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/keyv": {
      "version": "4.5.4",
      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "json-buffer": "3.0.1"
      }
    },
    "node_modules/levn": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "prelude-ls": "^1.2.1",
        "type-check": "~0.4.0"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/locate-path": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "p-locate": "^5.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/lodash.merge": {
      "version": "4.6.2",
      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/make-error": {
      "version": "1.3.6",
      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/minimatch": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "brace-expansion": "^1.1.7"
      },
      "engines": {
        "node": "*"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/natural-compare": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/optionator": {
      "version": "0.9.4",
      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "deep-is": "^0.1.3",
        "fast-levenshtein": "^2.0.6",
        "levn": "^0.4.1",
        "prelude-ls": "^1.2.1",
        "type-check": "^0.4.0",
        "word-wrap": "^1.2.5"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/p-limit": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "yocto-queue": "^0.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-locate": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "p-limit": "^3.0.2"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/parent-module": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "callsites": "^3.0.0"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/path-exists": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-is-absolute": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/prelude-ls": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/prettier": {
      "version": "3.8.1",
      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "bin": {
        "prettier": "bin/prettier.cjs"
      },
      "engines": {
        "node": ">=14"
      },
      "funding": {
        "url": "https://github.com/prettier/prettier?sponsor=1"
      }
    },
    "node_modules/proxy-from-env": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/punycode": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/queue-microtask": {
      "version": "1.2.3",
      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ]
    },
    "node_modules/resolve-from": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/reusify": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "iojs": ">=1.0.0",
        "node": ">=0.10.0"
      }
    },
    "node_modules/rimraf": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Rimraf versions prior to v4 are no longer supported",
      "dev": true,
      "dependencies": {
        "glob": "^7.1.3"
      },
      "bin": {
        "rimraf": "bin.js"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/run-parallel": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "dependencies": {
        "queue-microtask": "^1.2.2"
      }
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-json-comments": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/text-table": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/ts-node": {
      "version": "10.9.2",
      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "@cspotcode/source-map-support": "^0.8.0",
        "@tsconfig/node10": "^1.0.7",
        "@tsconfig/node12": "^1.0.7",
        "@tsconfig/node14": "^1.0.0",
        "@tsconfig/node16": "^1.0.2",
        "acorn": "^8.4.1",
        "acorn-walk": "^8.1.1",
        "arg": "^4.1.0",
        "create-require": "^1.1.0",
        "diff": "^4.0.1",
        "make-error": "^1.1.1",
        "v8-compile-cache-lib": "^3.0.1",
        "yn": "3.1.1"
      },
      "bin": {
        "ts-node": "dist/bin.js",
        "ts-node-cwd": "dist/bin-cwd.js",
        "ts-node-esm": "dist/bin-esm.js",
        "ts-node-script": "dist/bin-script.js",
        "ts-node-transpile-only": "dist/bin-transpile.js",
        "ts-script": "dist/bin-script-deprecated.js"
      },
      "peerDependencies": {
        "@swc/core": ">=1.2.50",
        "@swc/wasm": ">=1.2.50",
        "@types/node": "*",
        "typescript": ">=2.7"
      },
      "peerDependenciesMeta": {
        "@swc/core": {
          "optional": true
        },
        "@swc/wasm": {
          "optional": true
        }
      }
    },
    "node_modules/type-check": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "prelude-ls": "^1.2.1"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/type-fest": {
      "version": "0.20.2",
      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/uri-js": {
      "version": "4.4.1",
      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "punycode": "^2.1.0"
      }
    },
    "node_modules/v8-compile-cache-lib": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/word-wrap": {
      "version": "1.2.5",
      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true
    },
    "node_modules/yn": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/yocto-queue": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    }
  }
}


path: package.json
content: {
  "name": "ai-course-agent",
  "version": "0.2.1",
  "description": "OpenClaw Skill for auto-generating AI education courses via Edustem API with SkillPay billing",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "ts-node src/agent.ts",
    "lint": "eslint src --ext .ts",
    "format": "prettier --write src",
    "test": "ts-node src/test.ts"
  },
  "keywords": ["openclaw", "skill", "course-generation", "education", "ai", "edustem"],
  "author": "omgrich",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "ts-node": "^10.9.0",
    "prettier": "^3.0.0",
    "eslint": "^8.0.0"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "form-data": "^4.0.0"
  }
}


path: src/agent.ts
content: /**
 * AI Course Generator Agent
 *
 * Main orchestration layer that:
 * 1. Parses user request (grade, subject, content)
 * 2. Gathers curriculum information
 * 3. Calls Edustem API via api-edustem-skill
 * 4. Returns course link
 */

import {
  login,
  createLessonPlan,
  acceptLessonPlan,
  generateLessonUrl,
} from "./edustem-api";
import { gatherCurriculumContent, generateTeacherNotes } from "./utils";
import { getEdustemConfig } from "./config";
import { handleBilling } from "./skillpay";

/**
 * Types
 */

export interface CourseRequest {
  grade: string;
  subject: string;
  content: string;
}

export interface GeneratedCourseResponse {
  success: boolean;
  courseLink?: string;
  lessonRef?: string;
  message: string;
}

/**
 * Main course generation function
 *
 * Orchestrates the full flow:
 * 0. Check billing and charge user (SkillPay)
 * 1. Gather curriculum content
 * 2. Login to Edustem API
 * 3. Create lesson plan
 * 4. Accept lesson plan
 * 5. Return course link
 */
export async function generateCourse(
  request: CourseRequest,
  userId: string,
): Promise<GeneratedCourseResponse> {
  try {
    console.log(`[Agent] Processing course request:`, request);

    // Step 0: Billing check - charge user via SkillPay
    console.log(`[Agent] Checking billing for user: ${userId}...`);
    const billing = await handleBilling(userId);

    if (!billing.ok) {
      console.warn(`[Agent] Billing failed: ${billing.message}`);
      
      if (billing.paymentUrl) {
        return {
          success: false,
          message: `${billing.message}\n\n💳 请充值后继续使用:\n${billing.paymentUrl}\n\n充值说明: 1 USDT = 1000 tokens, 最低充值 8 USDT`,
        };
      }

      return {
        success: false,
        message: billing.message,
      };
    }

    console.log(`[Agent] Billing successful: ${billing.message}`);

    // Get credentials from config (environment or gateway)
    let credentials;
    try {
      credentials = getEdustemConfig();
    } catch (error) {
      return {
        success: false,
        message: `配置错误: ${error instanceof Error ? error.message : "Missing credentials"}`,
      };
    }

    // Step 1: Gather curriculum content
    console.log("[Agent] Gathering curriculum content...");
    const curriculumData = await gatherCurriculumContent(
      request.subject,
      request.content,
      request.grade,
    );

    // Step 2: Login to Edustem API
    console.log("[Agent] Logging in to Edustem API...");
    const token = await login(credentials.username, credentials.password);
    console.log("[Agent] Login successful, token received");

    // Step 3: Create lesson plan
    console.log("[Agent] Creating lesson plan...");
    const createResponse = await createLessonPlan(token, {
      subject: curriculumData.subject,
      year_level: request.grade,
      teaching_time_minutes: "45",
      topic: curriculumData.topic,
      curriculum_text: curriculumData.curriculum_text,
      elaborations: curriculumData.elaborations,
      teacher_notes: curriculumData.teacher_notes,
      concepts: "",
      subject_specific_instructions: "",
    });

    console.log("[Agent] Create response:", JSON.stringify(createResponse, null, 2));
    const lessonRef = createResponse.data.lesson_ref;
    console.log(`[Agent] Lesson plan created: ${lessonRef}`);

    // Step 4: Accept lesson plan
    console.log("[Agent] Accepting lesson plan...");
    const acceptResponse = await acceptLessonPlan(token, lessonRef);
    console.log("[Agent] Lesson plan accepted");

    // Step 5: Generate and return course link
    const courseLink = generateLessonUrl(lessonRef);

    return {
      success: true,
      courseLink,
      lessonRef,
      message: `成功为${request.grade}年级${request.subject}《${request.content}》生成课程！`,
    };
  } catch (error) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    console.error("[Agent] Error:", errorMsg);
    return {
      success: false,
      message: `生成课程失败: ${errorMsg}`,
    };
  }
}

/**
 * Convert Chinese numerals to Arabic numerals
 */
function chineseNumberToArabic(chinese: string): string {
  const map: { [key: string]: string } = {
    零: "0",
    一: "1",
    二: "2",
    三: "3",
    四: "4",
    五: "5",
    六: "6",
    七: "7",
    八: "8",
    九: "9",
    十: "10",
  };

  if (map[chinese]) {
    return map[chinese];
  }

  // For compound numbers like 十一, 十二, etc.
  if (chinese.includes("十")) {
    if (chinese === "十") return "10";
    const digit = chinese.charAt(1);
    return "1" + (map[digit] || digit);
  }

  return chinese;
}

/**
 * Parse user input to extract course request
 *
 * Example: "帮我生成6年级数学分数乘除法的课程"
 *          "帮我创建一个七年级语文从百草园到三味书屋的课程"
 * Output: { grade: '6', subject: '数学', content: '分数乘除法' }
 */
export function parseCourseRequest(userInput: string): CourseRequest | null {
  try {
    // Match both Arabic and Chinese numerals for year level
    const gradeMatch = userInput.match(/(\d+|[一二三四五六七八九十零]+)年级/);
    const subjectMatch = userInput.match(
      /(数学|语文|英语|科学|历史|地理|物理|化学|生物)/,
    );

    if (!gradeMatch || !subjectMatch) {
      return null;
    }

    // Convert Chinese numeral to Arabic if needed
    let gradeStr = gradeMatch[1];
    if (!/^\d+$/.test(gradeStr)) {
      gradeStr = chineseNumberToArabic(gradeStr);
    }

    // Extract content - everything between subject and "课程"
    const subject = subjectMatch[1];
    const subjectIndex = userInput.indexOf(subject);
    const contentStart = subjectIndex + subject.length;
    const contentEnd = userInput.indexOf("课程", contentStart);
    const content = userInput
      .substring(contentStart, contentEnd >= 0 ? contentEnd : undefined)
      .trim();

    return {
      grade: gradeStr,
      subject,
      content: content || subject,
    };
  } catch (error) {
    console.error("[Parser] Error parsing request:", error);
    return null;
  }
}

/**
 * Main entry point for testing
 */
async function main() {
  // Test input
  const userInput = "帮我生成6年级数学分数乘除法的课程";
  console.log(`[Main] User input: "${userInput}"`);

  // Parse request
  const request = parseCourseRequest(userInput);
  if (!request) {
    console.error("[Main] Failed to parse user input");
    return;
  }

  console.log("[Main] Parsed request:", request);

  // Generate course (with test userId)
  const testUserId = "test_user_" + Date.now();
  const result = await generateCourse(request, testUserId);
  console.log("[Main] Result:", result);

  if (result.success) {
    console.log(`\n✅ Course generated successfully!`);
    console.log(`📚 Course Link: ${result.courseLink}`);
  } else {
    console.log(`\n❌ Failed to generate course: ${result.message}`);
  }
}

// Run if called directly
if (require.main === module) {
  main().catch(console.error);
}

export default { generateCourse, parseCourseRequest };


path: src/config.ts
content: /**
 * Configuration management
 *
 * Reads credentials from environment variables or OpenClaw config
 * Never hardcode credentials in source code!
 */

export interface EdustemConfig {
  username: string;
  [REDACTED_SECRET];
}

/**
 * Get Edustem API credentials from environment
 */
export function getEdustemConfig(): EdustemConfig {
  const username = process.env.EDUSTEM_USERNAME;
  const [REDACTED_SECRET];

  if (!username || !password) {
    throw new Error(
      "Missing Edustem credentials. Set EDUSTEM_USERNAME and EDUSTEM_PASSWORD environment variables.",
    );
  }

  return { username, password };
}

/**
 * Alternative: Get credentials from OpenClaw gateway token/API
 * This would be called when running as an OpenClaw Skill
 */
export function getEdustemConfigFromGateway(): EdustemConfig {
  // In production, this would fetch from OpenClaw's secure credential store
  // For now, fall back to environment variables
  return getEdustemConfig();
}

export default {
  getEdustemConfig,
  getEdustemConfigFromGateway,
};


path: src/edustem-api.ts
content: import axios, { AxiosError } from "axios";
import FormData from "form-data";

const API_BASE_URL = "https://6bb95bf119bf.ngrok-free.app/api/v1";
const LESSON_BASE_URL = "https://6bb95bf119bf.ngrok-free.app/ai-lesson";

/**
 * Types
 */

export interface LoginResponse {
  status: number;
  message: string;
  data: {
    [REDACTED_SECRET];
  };
}

export interface CreateLessonPlanPayload {
  subject: string;
  year_level?: string;
  teaching_time_minutes?: string;
  topic: string;
  curriculum_text: string;
  elaborations: string;
  teacher_notes: string;
  concepts?: string;
  subject_specific_instructions?: string;
  sections?: Array<{
    title: string;
    artefact_types: string[];
  }>;
}

export interface CreateLessonPlanResponse {
  status: number;
  message: string | null;
  data: {
    lesson_ref: string;
    unit_ref: string | null;
    owner_ref: string;
    title: string;
    description: string;
    duration_minutes: number | null;
    seq_num: number;
    lesson_plan_ref: string;
    is_lesson_plan_accepted: boolean | null;
    lesson_type_name: string;
  };
  success: boolean;
}

export interface AcceptLessonPlanResponse {
  status: number;
  message: string | null;
  data: {
    lesson_ref: string;
    unit_ref: string | null;
    owner_ref: string;
    title: string;
    description: string;
    duration_minutes: number | null;
    seq_num: number;
    lesson_plan_ref: string;
    is_lesson_plan_accepted: boolean;
    lesson_type_name: string;
  };
  success: boolean;
}

/**
 * API Functions
 */

/**
 * Login to Edustem API and get JWT token
 */
export async function login(
  username: string,
  [REDACTED_SECRET],
): Promise<string> {
  try {
    const form = new FormData();
    form.append("username", username);
    form.append("password", password);

    const response = await axios.post<LoginResponse>(
      `${API_BASE_URL}/login/`,
      form,
      {
        headers: form.getHeaders(),
      },
    );

    if (response.data.status !== 200) {
      throw new Error(`Login failed with status ${response.data.status}`);
    }

    return response.data.data.token;
  } catch (error) {
    throw handleError("Login failed", error);
  }
}

/**
 * Create a lesson plan
 */
export async function createLessonPlan(
  [REDACTED_SECRET],
  payload: CreateLessonPlanPayload,
): Promise<CreateLessonPlanResponse> {
  try {
    // Build the metadata object with defaults
    const metadata = {
      subject: payload.subject,
      year_level: payload.year_level || "7",
      teaching_time_minutes: payload.teaching_time_minutes || "45",
      topic: payload.topic,
      curriculum_text: payload.curriculum_text,
      elaborations: payload.elaborations,
      teacher_notes: payload.teacher_notes,
      concepts: payload.concepts || "",
      subject_specific_instructions:
        payload.subject_specific_instructions || "",
      sections: payload.sections || [
        {
          title: "Focus the learning",
          artefact_types: [
            "LEARNING_INTENTIONS_AND_SUCCESS_CRITERIA",
            "HOOK_AND_LESSON_OPENING",
            "CONTENT_EXPLANATION",
            "PRESENTATION",
            "MULTIPLE_CHOICE_QUIZ",
            "WORKSHEETS",
          ],
        },
      ],
    };

    const response = await axios.post<CreateLessonPlanResponse>(
      `${API_BASE_URL}/ai-lesson/create/`,
      { metadata },
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
      },
    );

    console.log("[API] Create lesson plan response:", JSON.stringify(response.data, null, 2));

    if (!response.data.success) {
      const errorMsg = (response.data as any).error || response.data.message || "Unknown error";
      throw new Error(`Lesson plan creation failed: ${errorMsg}`);
    }

    return response.data;
  } catch (error) {
    throw handleError("Failed to create lesson plan", error);
  }
}

/**
 * Accept a lesson plan (confirm it)
 */
export async function acceptLessonPlan(
  [REDACTED_SECRET],
  lessonRef: string,
): Promise<AcceptLessonPlanResponse> {
  try {
    const form = new FormData();
    form.append("lesson_ref", lessonRef);

    const response = await axios.post<AcceptLessonPlanResponse>(
      `${API_BASE_URL}/ai-lesson/accept-lesson-plan/`,
      form,
      {
        headers: {
          ...form.getHeaders(),
          Authorization: `Bearer ${token}`,
        },
      },
    );

    if (!response.data.success) {
      throw new Error(`Accept lesson plan failed: ${response.data.message}`);
    }

    return response.data;
  } catch (error) {
    throw handleError("Failed to accept lesson plan", error);
  }
}

/**
 * Generate the lesson URL
 */
export function generateLessonUrl(lessonRef: string): string {
  return `${LESSON_BASE_URL}/${lessonRef}`;
}

/**
 * Error handling utility
 */
function handleError(context: string, error: unknown): Error {
  if (axios.isAxiosError(error)) {
    const axiosError = error as AxiosError<any>;
    const status = axiosError.response?.status;
    const message =
      axiosError.response?.data?.message ||
      axiosError.message ||
      "Unknown error";
    return new Error(`${context}: ${status ? `[${status}]` : ""} ${message}`);
  }

  if (error instanceof Error) {
    return new Error(`${context}: ${error.message}`);
  }

  return new Error(`${context}: ${String(error)}`);
}

/**
 * Export all functions and utilities
 */
export default {
  login,
  createLessonPlan,
  acceptLessonPlan,
  generateLessonUrl,
};


path: src/index.ts
content: /**
 * AI Course Agent - Main Entry Point
 *
 * OpenClaw Skill for auto-generating AI education courses.
 * Supports natural language input in Chinese.
 *
 * Usage:
 *   import { isCourseLessonRequest, processUserMessage } from 'ai-course-agent';
 *   if (isCourseLessonRequest(userInput)) {
 *     const response = await processUserMessage(userInput);
 *   }
 */

export {
  isCourseLessonRequest,
  processUserMessage,
  formatCourseResponse,
  handleCourseGenerationRequest,
} from "./integration";
export { generateCourse, parseCourseRequest } from "./agent";
export { generateTeacherNotes, gatherCurriculumContent } from "./utils";
export {
  login,
  createLessonPlan,
  acceptLessonPlan,
  generateLessonUrl,
} from "./edustem-api";
export {
  chargeUser,
  getBalance,
  getPaymentLink,
  handleBilling,
} from "./skillpay";
export type { CourseRequest, GeneratedCourseResponse } from "./agent";
export type {
  LoginResponse,
  CreateLessonPlanPayload,
  CreateLessonPlanResponse,
} from "./edustem-api";


path: src/integration.ts
content: /**
 * Integration with OpenClaw Main Session
 *
 * This module detects course generation requests from user input
 * and automatically executes the course generation flow.
 */

import { generateCourse, parseCourseRequest } from "./agent";

/**
 * Check if user input is a course generation request
 */
export function isCourseLessonRequest(message: string): boolean {
  // Keywords that indicate a course generation request
  const keywords = [
    "帮我生成",
    "帮我创建",
    "帮我制作",
    "我想生成",
    "我想创建",
    "生成",
    "创建",
    "课程",
  ];

  // Check if message contains at least "课程" and one of the action verbs
  const hasCourseKeyword = /课程/.test(message);
  const hasActionVerb = keywords.some((kw) => message.includes(kw));

  // Must contain year_level pattern (supports both digits and Chinese numerals)
  // Matches: 6年级, 7年级, 六年级, 七年级, etc.
  const hasPattern = /(\d+|[一二三四五六七八九十零]+)年级/.test(message);

  return hasCourseKeyword && hasActionVerb && hasPattern;
}

/**
 * Handle course generation request from user input
 *
 * This is the main entry point for automatic course generation.
 * 
 * @param userInput - User's message
 * @param userId - User identifier for billing (defaults to 'default_user' if not provided)
 */
export async function handleCourseGenerationRequest(
  userInput: string,
  userId: string = 'default_user',
): Promise<{
  success: boolean;
  message: string;
  courseLink?: string;
  lessonRef?: string;
}> {
  console.log(
    `[Integration] Detecting course generation request: "${userInput}"`,
  );

  // Check if this is a course request
  if (!isCourseLessonRequest(userInput)) {
    console.log("[Integration] Not a course generation request");
    return {
      success: false,
      message: "Not a course generation request",
    };
  }

  console.log("[Integration] Course generation request detected!");

  // Parse the request
  const request = parseCourseRequest(userInput);
  if (!request) {
    console.log("[Integration] Failed to parse course request");
    return {
      success: false,
      message:
        '无法理解你的请求。请按照这样的格式：\n"帮我生成[X年级][科目][内容]的课程"\n例如：帮我生成6年级数学分数乘除法的课程',
    };
  }

  console.log("[Integration] Parsed request:", request);

  // Generate the course (with billing)
  console.log(`[Integration] Generating course for user: ${userId}...`);
  const result = await generateCourse(request, userId);

  return result;
}

/**
 * Format the response for user display
 *
 * For Telegram: Returns plain text format with URL
 * Telegram will auto-detect and make the URL clickable
 */
export function formatCourseResponse(result: {
  success: boolean;
  message: string;
  courseLink?: string;
  lessonRef?: string;
}): string {
  if (!result.success) {
    return `❌ ${result.message}`;
  }

  // Return in plain text format with URL embedded
  // With blank line between message and link for better readability
  // Telegram will auto-detect the URL as clickable
  return `✅ ${result.message}\n\n📚 课程链接: ${result.courseLink}`;
}

/**
 * Main integration function to call from main session
 *
 * Usage in main session:
 * ```
 * import { processUserMessage } from './integration';
 *
 * // When user sends a message:
 * const userId = req.user.sub; // or any user identifier
 * const response = await processUserMessage(userInput, userId);
 * // Send response to user
 * ```
 * 
 * @param userInput - User's message
 * @param userId - User identifier for billing (defaults to 'default_user')
 */
export async function processUserMessage(
  userInput: string, 
  userId: string = 'default_user'
): Promise<string> {
  try {
    const result = await handleCourseGenerationRequest(userInput, userId);
    return formatCourseResponse(result);
  } catch (error) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    console.error("[Integration] Error processing message:", errorMsg);
    return `⚠️ 处理请求时出错: ${errorMsg}`;
  }
}

export default {
  isCourseLessonRequest,
  handleCourseGenerationRequest,
  formatCourseResponse,
  processUserMessage,
};


path: src/main-session-integration.md
content: # Main Session Integration Guide

## How to Integrate into OpenClaw Main Session

### Method 1: Direct Import (Recommended)

In your main session message handler, add this at the beginning:

```typescript
import {
  processUserMessage,
  isCourseLessonRequest,
} from "./agent/src/integration";

async function handleUserMessage(userInput: string) {
  // Check if this is a course generation request
  if (isCourseLessonRequest(userInput)) {
    console.log("🎓 Course generation request detected!");
    const response = await processUserMessage(userInput);
    return response;
  }

  // Otherwise, handle as normal conversation
  return await normalConversationHandler(userInput);
}
```

### Method 2: Spawn Sub-Agent (Alternative)

If you prefer isolation:

```typescript
import { sessions_spawn } from "../openclaw";

const result = await sessions_spawn({
  agentId: "course-generator",
  task: userInput,
});
```

### Supported Input Patterns

The system automatically detects these patterns:

✅ `"帮我生成6年级数学分数乘除法的课程"`
✅ `"帮我创建一个七年级语文从百草园到三味书屋的课程"`
✅ `"帮我制作9年级英语日常会话的课程"`
✅ `"生成8年级科学地球和宇宙的课程"`

### Expected Output

**Success Response:**

```
✅ 成功为6年级数学分数乘除法生成课程！

📚 **课程链接:**
https://6bb95bf119bf.ngrok-free.app/ai-lesson/356967b7-95dc-4619-9051-c121e36c4659
```

**Failure Response:**

```
❌ 生成课程失败: [error message]
```

## Files

- `src/integration.ts` - Main integration functions
  - `isCourseLessonRequest(message)` - Detect if input is a course request
  - `handleCourseGenerationRequest(userInput)` - Process the request
  - `processUserMessage(userInput)` - End-to-end processing with formatting

- `src/agent.ts` - Core course generation
  - `generateCourse(request)` - Execute the full flow

- `src/utils.ts` - Helpers
  - `gatherCurriculumContent()` - Gather course metadata
  - `generateTeacherNotes()` - Format teacher instructions

## Build & Deploy

```bash
# Build TypeScript
npm run build

# Test locally
npm run test

# Push to GitHub (already done)
git push origin main
```

## Next Steps

1. ✅ Agent code complete
2. ✅ Skill integration complete
3. ✅ Local testing working
4. ⏳ **TODO: Integrate into main session**
   - Copy/import `src/integration.ts` logic
   - Add to your message handler
   - Test with real user input

5. ⏳ Add web search for real curriculum content
6. ⏳ Improve NLP parsing for more complex inputs


path: src/run-voice.ts
content: import { processUserMessage, isCourseLessonRequest } from "./integration";

const userInput = "帮我生成六年级数学分数乘除法的课程";

async function main() {
  console.log(`\n[语音识别] ${userInput}\n`);

  if (!isCourseLessonRequest(userInput)) {
    console.log("[Agent] 不是课程生成请求");
    return;
  }

  console.log("[Agent] 🎓 检测到课程生成请求！处理中...\n");

  const response = await processUserMessage(userInput);
  console.log(`[Agent 响应]\n${response}\n`);
}

main().catch(console.error);


path: src/run.ts
content: import { processUserMessage, isCourseLessonRequest } from "./integration";

const userInput = "帮我创建一个七年级语文从百草园到三味书屋的课程";

async function main() {
  console.log(`\n[User] ${userInput}\n`);

  if (!isCourseLessonRequest(userInput)) {
    console.log("[Agent] Not a course request");
    return;
  }

  console.log("[Agent] 🎓 Course request detected! Processing...\n");

  const response = await processUserMessage(userInput);
  console.log(`[Agent Response]\n${response}\n`);
}

main().catch(console.error);


path: src/skillpay.ts
content: /**
 * SkillPay Billing SDK — Node.js
 *
 * [EN] 1 USDT = 1000 tokens | 1 call = 1 token | Min deposit 8 USDT
 * [中文] 1 USDT = 1000 tokens | 每次 1 token | 最低充值 8 USDT
 * [日本語] 1 USDT = 1000トークン | 1回1トークン | 最低8 USDT
 * [한국어] 1 USDT = 1000토큰 | 1회 1토큰 | 최소 8 USDT
 * [Русский] 1 USDT = 1000 токенов | 1 вызов = 1 токен | Мин. 8 USDT
 *
 * Endpoints:
 * POST /billing/charge — Deduct / 扣费 / 課金 / 차감 / Списание
 * GET /billing/balance — Balance / 余额 / 残高 / 잔액 / Баланс
 * POST /billing/payment-link — Pay link / 充值 / 入金 / 결제 / Оплата
 */

import axios from 'axios';

const BILLING_URL = 'https://skillpay.me/api/v1/billing';

interface ChargeResult {
  ok: boolean;
  balance: number;
  payment_url?: string;
}

interface BillingConfig {
  [REDACTED_SECRET];
  skillId: string;
}

/**
 * Get billing configuration (hardcoded - DO NOT expose to users!)
 * 
 * These values belong to the skill author and are used to receive payments.
 * Users cannot and should not modify these values.
 */
function getBillingConfig(): BillingConfig {
  // Hardcoded credentials - payments go to skill author's account
  const [REDACTED_SECRET]';
  const skillId = '476d912d-e597-4be0-a031-6ffe2adf3b13';

  return { apiKey, skillId };
}

/**
 * Get headers for billing API
 */
function getHeaders(): Record<string, string> {
  const { apiKey } = getBillingConfig();
  return {
    'X-API-Key': apiKey,
    'Content-Type': 'application/json',
  };
}

/**
 * Charge user for skill usage
 * 扣费 / 課金 / 차감 / Списание
 *
 * @param userId - User identifier
 * @param amount - Amount to charge (default: 0 = use default rate)
 * @returns Charge result with balance and payment URL if insufficient
 */
export async function chargeUser(
  userId: string,
  amount: number = 0,
): Promise<ChargeResult> {
  const { skillId } = getBillingConfig();
  const headers = getHeaders();

  try {
    const { data } = await axios.post(
      BILLING_URL + '/charge',
      {
        user_id: userId,
        skill_id: skillId,
        amount,
      },
      { headers },
    );

    if (data.success) {
      return { ok: true, balance: data.balance };
    }

    return {
      ok: false,
      balance: data.balance,
      payment_url: data.payment_url,
    };
  } catch (error) {
    console.error('[SkillPay] Charge error:', error);
    throw new Error('Failed to charge user. Please try again later.');
  }
}

/**
 * Get user balance
 * 余额 / 残高 / 잔액 / Баланс
 *
 * @param userId - User identifier
 * @returns Current balance in tokens
 */
export async function getBalance(userId: string): Promise<number> {
  const headers = getHeaders();

  try {
    const { data } = await axios.get(BILLING_URL + '/balance', {
      params: { user_id: userId },
      headers,
    });

    return data.balance;
  } catch (error) {
    console.error('[SkillPay] Get balance error:', error);
    throw new Error('Failed to get balance. Please try again later.');
  }
}

/**
 * Generate payment link for user top-up
 * 充值链接 / 入金リンク / 결제링크 / Ссылка на оплату
 *
 * @param userId - User identifier
 * @param amount - Amount to top up in USDT (default: 8 USDT min)
 * @returns Payment URL
 */
export async function getPaymentLink(
  userId: string,
  amount: number = 8,
): Promise<string> {
  const headers = getHeaders();

  try {
    const { data } = await axios.post(
      BILLING_URL + '/payment-link',
      {
        user_id: userId,
        amount,
      },
      { headers },
    );

    return data.payment_url;
  } catch (error) {
    console.error('[SkillPay] Get payment link error:', error);
    throw new Error('Failed to generate payment link. Please try again later.');
  }
}

/**
 * Handle billing for skill execution
 * 处理技能计费 / スキル課金処理 / 스킬 결제 처리 / Обработка оплаты навыка
 *
 * Usage example:
 * ```typescript
 * const billing = await handleBilling(userId);
 * if (!billing.ok) {
 *   return { success: false, message: billing.message, paymentUrl: billing.paymentUrl };
 * }
 * // ✅ Proceed with skill execution
 * ```
 *
 * @param userId - User identifier
 * @returns Billing result with status and payment URL if needed
 */
export async function handleBilling(userId: string): Promise<{
  ok: boolean;
  message: string;
  paymentUrl?: string;
  balance?: number;
}> {
  try {
    const charge = await chargeUser(userId);

    if (charge.ok) {
      return {
        ok: true,
        message: `✅ 扣费成功，剩余余额: ${charge.balance} tokens`,
        balance: charge.balance,
      };
    }

    // Insufficient balance
    return {
      ok: false,
      message: `❌ 余额不足 (当前: ${charge.balance} tokens)`,
      paymentUrl: charge.payment_url,
      balance: charge.balance,
    };
  } catch (error) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    console.error('[SkillPay] Billing error:', errorMsg);
    return {
      ok: false,
      message: `⚠️ 计费系统错误: ${errorMsg}`,
    };
  }
}

export default {
  chargeUser,
  getBalance,
  getPaymentLink,
  handleBilling,
};


path: src/test.ts
content: /**
 * Test file for local testing
 *
 * Run with: npm run test
 */

import { generateCourse, parseCourseRequest } from "./agent";

async function runTests() {
  console.log("=".repeat(60));
  console.log("AI Course Agent - Local Test");
  console.log("=".repeat(60));

  // Test cases
  const testCases = [
    "帮我生成6年级数学分数乘除法的课程",
    "帮我创建一个七年级语文从百草园到三味书屋的课程",
    "帮我生成9年级英语日常会话的课程",
  ];

  for (const testCase of testCases) {
    console.log(`\n📝 Input: "${testCase}"`);

    // Parse request
    const request = parseCourseRequest(testCase);
    if (!request) {
      console.log("❌ Failed to parse input");
      continue;
    }

    console.log(`✓ Parsed: ${JSON.stringify(request)}`);

    // Generate course (with test userId)
    const testUserId = `test_user_${Date.now()}`;
    console.log(`⏳ Generating course for user: ${testUserId}...`);
    const result = await generateCourse(request, testUserId);

    if (result.success) {
      console.log(`✅ Success!`);
      console.log(`   Message: ${result.message}`);
      console.log(`   Link: ${result.courseLink}`);
      console.log(`   Lesson Ref: ${result.lessonRef}`);
    } else {
      console.log(`❌ Failed: ${result.message}`);
    }

    console.log("-".repeat(60));
  }

  console.log("\n✨ Test completed!");
}

// Run tests
runTests().catch(console.error);


path: src/utils.ts
content: /**
 * Utility functions for course generation
 */

import axios from "axios";

export interface LessonMetadata {
  subject: string;
  topic: string;
  curriculum_text: string;
  elaborations: string;
  teacher_notes: string;
}

/**
 * Generate teacher_notes with proper formatting
 *
 * CRITICAL: The teacher_notes must follow this exact format:
 * "我需要你帮我生成这个主题的课程内容，特别是需要讲解{{topic}}。...让学生参与度高。。。。。。。。。。。。"
 *
 * The 12 trailing dots (。) are REQUIRED and mark the end of the instruction.
 * System parses until it finds these dots. Missing them = malformed request!
 */
export function generateTeacherNotes(subject: string, topic: string): string {
  const dots = "。".repeat(12); // System expects exactly 12 dots
  return `我需要你帮我生成这个主题的课程内容，特别是需要讲解${topic}。这些需要在内容和练习题里都体现。用中文来写所有的内容，因为这将会被用来在《${subject}》课程上使用。让学生参与度高，更好理解课程内容。 ${dots}`;
}

/**
 * Fetch curriculum content
 *
 * This function gathers the curriculum_text and elaborations needed for the lesson plan.
 * Currently uses placeholder data; in production, this would:
 * - Search the web for curriculum standards
 * - Query an LLM for content generation
 * - Fetch from a curriculum database
 */
export async function gatherCurriculumContent(
  subject: string,
  topic: string,
  grade: string,
): Promise<LessonMetadata> {
  try {
    console.log(
      `[Utils] Gathering content for ${grade}年级 ${subject} - ${topic}`,
    );

    // In production, you could:
    // 1. Search web for curriculum info
    // 2. Query LLM for content generation
    // 3. Fetch from curriculum database

    // For now, generate placeholder content based on input
    const curriculum_text = generateCurriculumText(grade, subject, topic);
    const elaborations = generateElaborations(subject, topic);

    return {
      subject,
      topic,
      curriculum_text,
      elaborations,
      teacher_notes: generateTeacherNotes(subject, topic),
    };
  } catch (error) {
    console.error("[Utils] Error gathering curriculum content:", error);
    throw error;
  }
}

/**
 * Generate curriculum text (placeholder implementation)
 *
 * In production, this would fetch actual curriculum standards or generate via LLM.
 */
function generateCurriculumText(
  grade: string,
  subject: string,
  topic: string,
): string {
  const gradeText = `${grade}年级`;

  switch (subject) {
    case "数学":
      return `${gradeText}${subject}课程 - ${topic}

基础概念：
- ${topic}是${gradeText}${subject}的重要内容
- 学生需要掌握基本的计算方法
- 需要理解背后的数学原理

学习目标：
- 能够正确进行${topic}的运算
- 理解${topic}的算法原理
- 能够应用到实际问题中

难点：
- 理解${topic}的规则
- 正确处理特殊情况
- 应用到复杂问题`;

    case "语文":
      return `${gradeText}${subject}课程 - ${topic}

文章背景：
- ${topic}是${gradeText}${subject}阅读教学的经典课文
- 体现了${subject}的核心价值观和审美要求
- 对学生的语言发展和思维能力有重要意义

主要内容：
- 故事情节和人物塑造
- 语言特色和修辞手法
- 主题思想和文学价值

教学重点：
- 深入理解文章内涵
- 学习优美的语言表达
- 培养审美和批判能力`;

    case "英语":
      return `${gradeText}${subject}课程 - ${topic}

课程描述：
- ${topic}是${gradeText}${subject}的重要语法/话题
- 覆盖实用的日常表达
- 符合${gradeText}学生的语言水平

学习内容：
- 核心词汇和短语
- 语法结构和用法
- 实际交际场景

学习目标：
- 掌握${topic}的核心表达
- 能够在真实情境中应用
- 提高综合语言能力`;

    default:
      return `${gradeText}${subject}课程 - ${topic}

课程概览：
这是${gradeText}${subject}课程中关于${topic}的教学内容。

主要内容：
- ${topic}的基本概念
- 相关的关键知识点
- 实际应用场景

学习目标：
- 理解${topic}的核心内容
- 掌握相关的技能和方法
- 能够应用到实际中`;
  }
}

/**
 * Generate elaborations (支撑细节)
 *
 * Elaborations provide additional context and depth for the lesson.
 */
function generateElaborations(subject: string, topic: string): string {
  return `本课程对${topic}这一主题进行了深入的教学设计，包括：

1. 知识结构完整：从基础概念到应用实践，形成完整的知识体系
2. 循序渐进：按照学生的认知规律，由浅入深地组织教学内容
3. 理论与实践结合：不仅讲解理论知识，还提供大量的实例和练习
4. 多角度思考：从多个角度分析${topic}，帮助学生形成全面的理解
5. 学生中心：充分考虑学生的学习特点和兴趣，提高学习的主动性和参与度

通过本课程的学习，学生能够：
- 深刻理解${topic}的本质
- 掌握相关的知识和技能
- 学会用这些知识解决实际问题
- 培养终身学习的能力`;
}

/**
 * Validate curriculum metadata before sending to API
 */
export function validateMetadata(metadata: LessonMetadata): boolean {
  // Check required fields
  if (!metadata.subject || !metadata.topic) {
    console.error("[Utils] Missing required fields: subject or topic");
    return false;
  }

  // Check teacher_notes format (must end with dots)
  if (!metadata.teacher_notes.includes("。")) {
    console.error("[Utils] Invalid teacher_notes: missing required dots");
    return false;
  }

  // Check minimum content length
  if (metadata.curriculum_text.length < 50) {
    console.error("[Utils] curriculum_text is too short");
    return false;
  }

  return true;
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": {
      "api-edustem-skill": ["../api-edustem-skill/dist/index"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}


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