feat(agent): add skills system with profile integration (#21)

* chore(deps): add yaml package for skill parsing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(agent): add skills system

Implement a skills system inspired by moltbot's approach:

- Skills are markdown files (SKILL.md) with YAML frontmatter
- Multi-source loading with precedence: bundled < user < workspace
- Eligibility filtering based on platform, binaries, and env vars
- Skills are automatically included in agent system prompt
- New AgentOptions: enableSkills, skillsBaseDir, extraSkillDirs

Includes two bundled skills:
- commit: Git commit helper with conventional commit guidelines
- code-review: Code review checklist and best practices

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(skills): use profile-based skills instead of workspace

Change skill loading from workspace-based (.skills/) to profile-based:
- Skills now load from ~/.super-multica/agent-profiles/<profileId>/skills/
- Remove workspace and user skill sources
- Simplify to only bundled and profile sources
- Profile skills have higher precedence than bundled

This is more appropriate for non-coding agents where skills are
associated with agent identity rather than working directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jiayuan 2026-01-30 05:21:57 +08:00 committed by GitHub
parent 9b3ffd1e90
commit 50ae997ab4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 839 additions and 7 deletions

View file

@ -53,6 +53,7 @@
"socket.io-client": "^4.8.3",
"turndown": "^7.2.2",
"undici": "^7.19.2",
"uuid": "^13.0.0"
"uuid": "^13.0.0",
"yaml": "^2.8.2"
}
}

3
pnpm-lock.yaml generated
View file

@ -80,6 +80,9 @@ importers:
uuid:
specifier: ^13.0.0
version: 13.0.0
yaml:
specifier: ^2.8.2
version: 2.8.2
devDependencies:
'@types/node':
specifier: ^25.0.10

View file

@ -0,0 +1,81 @@
---
name: Code Review
description: Review code for bugs, security issues, and best practices
version: 1.0.0
metadata:
emoji: "🔍"
tags:
- code-quality
- security
- review
---
## Instructions
When the user asks you to review code, follow these guidelines:
### Review Checklist
1. **Correctness**
- Does the code do what it's supposed to do?
- Are there any logic errors?
- Are edge cases handled?
2. **Security**
- Input validation and sanitization
- SQL injection vulnerabilities
- XSS vulnerabilities
- Command injection
- Path traversal
- Sensitive data exposure
- Authentication/authorization issues
3. **Code Quality**
- Is the code readable and maintainable?
- Are variable/function names descriptive?
- Is there unnecessary complexity?
- Are there code duplications?
4. **Performance**
- Are there obvious performance issues?
- N+1 queries
- Unnecessary loops or computations
- Memory leaks
5. **Error Handling**
- Are errors properly caught and handled?
- Are error messages helpful?
- Is there proper logging?
6. **Testing**
- Are there tests for the new code?
- Do the tests cover edge cases?
### Review Format
Structure your review as follows:
```
## Summary
[Brief overview of what the code does and overall assessment]
## Critical Issues
[Must-fix issues: bugs, security vulnerabilities]
## Suggestions
[Improvements and best practices recommendations]
## Questions
[Clarifications needed about intent or design decisions]
## Positive Aspects
[Good practices observed in the code]
```
### Guidelines
- Be constructive, not critical
- Explain the "why" behind suggestions
- Provide concrete examples for improvements
- Prioritize issues by severity
- Acknowledge good practices

75
skills/commit/SKILL.md Normal file
View file

@ -0,0 +1,75 @@
---
name: Git Commit Helper
description: Create well-formatted git commits following conventional commit standards
version: 1.0.0
metadata:
emoji: "📝"
requiresBinaries:
- git
tags:
- git
- developer-tools
---
## Instructions
When the user asks you to create a commit or commit their changes, follow these guidelines:
### Step 1: Review Changes
1. Run `git status` to see what files have changed
2. Run `git diff` to see the actual changes
3. If there are staged changes, also run `git diff --staged`
### Step 2: Analyze and Group Changes
Group related changes into logical commits:
- Feature additions
- Bug fixes
- Refactoring (no functional change)
- Documentation
- Tests
- Configuration/dependencies
### Step 3: Create Atomic Commits
For each logical group of changes:
1. Stage only the relevant files: `git add <file1> <file2>`
2. Create a commit with conventional message format
### Commit Message Format
Use conventional commits:
- `feat`: New feature
- `fix`: Bug fix
- `refactor`: Code refactoring (no functional change)
- `docs`: Documentation changes
- `test`: Adding or updating tests
- `chore`: Build, config, dependencies
Format: `<type>(<scope>): <description>`
Example: `feat(auth): add user login endpoint`
### Rules
- Each commit should be independently meaningful and buildable
- Related test files should be committed with their implementation
- Never create empty commits
- Never combine unrelated changes in one commit
- Keep commit messages concise but descriptive
- If all changes are related to one logical unit, a single commit is fine
### Example
If the user modified:
- `src/api/user.ts` (added new endpoint)
- `src/api/user.test.ts` (tests for new endpoint)
- `src/utils/format.ts` (refactored helper)
- `README.md` (updated docs)
Create three commits:
1. `git add src/api/user.ts src/api/user.test.ts && git commit -m "feat(api): add user profile endpoint"`
2. `git add src/utils/format.ts && git commit -m "refactor(utils): simplify date formatting logic"`
3. `git add README.md && git commit -m "docs: update API documentation"`

View file

@ -2,3 +2,4 @@ export * from "./runner.js";
export * from "./types.js";
export * from "./profile/index.js";
export * from "./context-window/index.js";
export * from "./skills/index.js";

View file

@ -5,6 +5,7 @@ import { createAgentOutput } from "./output.js";
import { resolveModel, resolveTools } from "./tools.js";
import { SessionManager } from "./session/session-manager.js";
import { ProfileManager } from "./profile/index.js";
import { SkillManager } from "./skills/index.js";
import {
checkContextWindow,
DEFAULT_CONTEXT_TOKENS,
@ -43,6 +44,7 @@ export class Agent {
private readonly output;
private readonly session: SessionManager;
private readonly profile?: ProfileManager;
private readonly skillManager?: SkillManager;
private readonly contextWindowGuard: ContextWindowGuardResult;
private readonly debug: boolean;
@ -57,7 +59,7 @@ export class Agent {
this.agent = new PiAgentCore();
// 加载 Agent Profile如果指定了 profileId
// Load Agent Profile (if profileId is specified)
let systemPrompt: string | undefined;
if (options.profileId) {
this.profile = new ProfileManager({
@ -65,13 +67,29 @@ export class Agent {
baseDir: options.profileBaseDir,
});
systemPrompt = this.profile.buildSystemPrompt();
if (systemPrompt) {
this.agent.setSystemPrompt(systemPrompt);
}
} else if (options.systemPrompt) {
// 直接使用传入的 systemPrompt
// Use provided systemPrompt directly
systemPrompt = options.systemPrompt;
this.agent.setSystemPrompt(options.systemPrompt);
}
// Initialize SkillManager (enabled by default)
if (options.enableSkills !== false) {
this.skillManager = new SkillManager({
profileId: options.profileId,
profileBaseDir: options.profileBaseDir,
extraDirs: options.extraSkillDirs,
});
// Append skills prompt to system prompt
const skillsPrompt = this.skillManager.buildSkillsPrompt();
if (skillsPrompt) {
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillsPrompt}` : skillsPrompt;
}
}
// Set the combined system prompt
if (systemPrompt) {
this.agent.setSystemPrompt(systemPrompt);
}
this.sessionId = options.sessionId ?? uuidv7();

View file

@ -0,0 +1,110 @@
/**
* Skill Eligibility Checker
*
* Filter skills based on platform, binaries, and environment requirements
*/
import { execSync } from "node:child_process";
import type { Skill, EligibilityResult } from "./types.js";
/**
* Check if a binary exists in PATH
*
* @param binary - Binary name to check
* @returns True if binary exists
*/
function binaryExists(binary: string): boolean {
try {
// Use 'which' on Unix, 'where' on Windows
const cmd = process.platform === "win32" ? `where ${binary}` : `which ${binary}`;
execSync(cmd, { stdio: "ignore" });
return true;
} catch {
return false;
}
}
/**
* Check if an environment variable is set
*
* @param envVar - Environment variable name
* @returns True if set (even if empty string)
*/
function envExists(envVar: string): boolean {
return envVar in process.env;
}
/**
* Check if a skill is eligible based on its requirements
*
* @param skill - Skill to check
* @param platform - Platform to check against (defaults to current)
* @returns Eligibility result with reasons if ineligible
*/
export function checkEligibility(
skill: Skill,
platform: NodeJS.Platform = process.platform,
): EligibilityResult {
const reasons: string[] = [];
const metadata = skill.frontmatter.metadata;
// No metadata means no requirements
if (!metadata) {
return { eligible: true };
}
// Platform check
if (metadata.platforms && metadata.platforms.length > 0) {
if (!metadata.platforms.includes(platform)) {
reasons.push(
`Platform '${platform}' not supported (requires: ${metadata.platforms.join(", ")})`,
);
}
}
// Binary requirements check
if (metadata.requiresBinaries && metadata.requiresBinaries.length > 0) {
for (const binary of metadata.requiresBinaries) {
if (!binaryExists(binary)) {
reasons.push(`Required binary not found: ${binary}`);
}
}
}
// Environment variable check
if (metadata.requiresEnv && metadata.requiresEnv.length > 0) {
for (const envVar of metadata.requiresEnv) {
if (!envExists(envVar)) {
reasons.push(`Required environment variable not set: ${envVar}`);
}
}
}
return {
eligible: reasons.length === 0,
reasons: reasons.length > 0 ? reasons : undefined,
};
}
/**
* Filter skills by eligibility
*
* @param skills - Map of skills to filter
* @param platform - Platform to check against
* @returns Map containing only eligible skills
*/
export function filterEligibleSkills(
skills: Map<string, Skill>,
platform?: NodeJS.Platform,
): Map<string, Skill> {
const eligible = new Map<string, Skill>();
for (const [id, skill] of skills) {
const result = checkEligibility(skill, platform);
if (result.eligible) {
eligible.set(id, skill);
}
}
return eligible;
}

166
src/agent/skills/index.ts Normal file
View file

@ -0,0 +1,166 @@
/**
* Skills Module
*
* Manages skill loading, eligibility filtering, and system prompt generation
*/
import type { Skill, SkillManagerOptions } from "./types.js";
import { loadAllSkills, getBundledSkillsDir, getProfileSkillsDir } from "./loader.js";
import { filterEligibleSkills, checkEligibility } from "./eligibility.js";
// Re-export types and utilities
export type {
Skill,
SkillFrontmatter,
SkillMetadata,
SkillSource,
SkillManagerOptions,
EligibilityResult,
} from "./types.js";
export { SKILL_FILE, SKILL_SOURCE_PRECEDENCE } from "./types.js";
export { checkEligibility, filterEligibleSkills } from "./eligibility.js";
export { parseFrontmatter, parseSkillFile } from "./parser.js";
export { loadAllSkills, getBundledSkillsDir, getProfileSkillsDir } from "./loader.js";
/**
* SkillManager - Loads and manages skills
*
* Provides access to skills from multiple sources with precedence handling
* and eligibility filtering.
*/
export class SkillManager {
private readonly options: SkillManagerOptions;
private skills: Map<string, Skill> | undefined;
private eligibleSkills: Map<string, Skill> | undefined;
constructor(options: SkillManagerOptions = {}) {
this.options = options;
}
/**
* Ensure skills are loaded (lazy loading)
*/
private ensureLoaded(): void {
if (this.skills) return;
this.skills = loadAllSkills(this.options);
this.eligibleSkills = filterEligibleSkills(this.skills, this.options.platform);
}
/**
* Get all loaded skills (including ineligible)
*/
getAllSkills(): Map<string, Skill> {
this.ensureLoaded();
return this.skills!;
}
/**
* Get only eligible skills
*/
getEligibleSkills(): Map<string, Skill> {
this.ensureLoaded();
return this.eligibleSkills!;
}
/**
* Get a specific skill by ID (only from eligible skills)
*
* @param skillId - Skill identifier
* @returns Skill or undefined if not found or ineligible
*/
getSkill(skillId: string): Skill | undefined {
this.ensureLoaded();
return this.eligibleSkills!.get(skillId);
}
/**
* Get skill by ID from all skills (including ineligible)
*
* @param skillId - Skill identifier
* @returns Skill or undefined if not found
*/
getSkillFromAll(skillId: string): Skill | undefined {
this.ensureLoaded();
return this.skills!.get(skillId);
}
/**
* Reload skills from disk
* Clears cache and reloads on next access
*/
reload(): void {
this.skills = undefined;
this.eligibleSkills = undefined;
}
/**
* Build skills section for system prompt
*
* Generates formatted documentation of all eligible skills
* for inclusion in the agent's system prompt.
*
* @returns Formatted skill documentation or empty string if no skills
*/
buildSkillsPrompt(): string {
this.ensureLoaded();
if (this.eligibleSkills!.size === 0) {
return "";
}
const parts: string[] = [];
parts.push("# Available Skills\n");
parts.push("You have access to the following skills:\n");
for (const [id, skill] of this.eligibleSkills!) {
const emoji = skill.frontmatter.metadata?.emoji ?? "🔧";
const name = skill.frontmatter.name;
const desc = skill.frontmatter.description ?? "No description provided";
parts.push(`## ${emoji} ${name} (${id})`);
parts.push(`${desc}\n`);
// Include full instructions
if (skill.instructions) {
parts.push(skill.instructions);
parts.push("");
}
}
return parts.join("\n");
}
/**
* Get skill instructions for a specific skill
*
* @param skillId - Skill identifier
* @returns Instructions markdown or undefined if not found
*/
getSkillInstructions(skillId: string): string | undefined {
const skill = this.getSkill(skillId);
return skill?.instructions;
}
/**
* List skill IDs with their display info
*
* @returns Array of skill info for display
*/
listSkills(): Array<{ id: string; name: string; emoji: string; description: string }> {
this.ensureLoaded();
const result: Array<{ id: string; name: string; emoji: string; description: string }> = [];
for (const [id, skill] of this.eligibleSkills!) {
result.push({
id,
name: skill.frontmatter.name,
emoji: skill.frontmatter.metadata?.emoji ?? "🔧",
description: skill.frontmatter.description ?? "No description",
});
}
return result;
}
}

142
src/agent/skills/loader.ts Normal file
View file

@ -0,0 +1,142 @@
/**
* Skills Loader
*
* Multi-source loading with precedence handling
*/
import { existsSync, readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import type { Skill, SkillSource, SkillManagerOptions } from "./types.js";
import { SKILL_FILE, SKILL_SOURCE_PRECEDENCE } from "./types.js";
import { parseSkillFile } from "./parser.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
/** Default profile base directory */
const DEFAULT_PROFILE_BASE_DIR = join(homedir(), ".super-multica", "agent-profiles");
/** Bundled skills directory (relative to package) */
const BUNDLED_DIR = join(__dirname, "../../../skills");
/**
* Discover skill directories in a given base path
* A valid skill directory contains a SKILL.md file
*
* @param baseDir - Base directory to search
* @returns Array of absolute paths to skill directories
*/
function discoverSkillDirs(baseDir: string): string[] {
if (!existsSync(baseDir)) {
return [];
}
try {
const entries = readdirSync(baseDir);
return entries
.map((name) => join(baseDir, name))
.filter((path) => {
try {
if (!statSync(path).isDirectory()) {
return false;
}
return existsSync(join(path, SKILL_FILE));
} catch {
return false;
}
});
} catch {
return [];
}
}
/**
* Load all skills from a source directory
*
* @param baseDir - Base directory containing skill subdirectories
* @param source - Source type for loaded skills
* @returns Array of loaded skills
*/
function loadSkillsFromSource(baseDir: string, source: SkillSource): Skill[] {
const skillDirs = discoverSkillDirs(baseDir);
const skills: Skill[] = [];
for (const dir of skillDirs) {
const skillId = dir.split("/").pop();
if (!skillId) continue;
const filePath = join(dir, SKILL_FILE);
const skill = parseSkillFile(filePath, skillId, source);
if (skill) {
skills.push(skill);
}
}
return skills;
}
/**
* Get profile skills directory path
*
* @param profileId - Agent profile ID
* @param profileBaseDir - Profile base directory
* @returns Path to profile skills directory
*/
export function getProfileSkillsDir(profileId: string, profileBaseDir?: string): string {
const baseDir = profileBaseDir ?? DEFAULT_PROFILE_BASE_DIR;
return join(baseDir, profileId, "skills");
}
/**
* Load all skills from all sources, applying precedence
* Higher precedence sources override skills with the same ID
*
* Loading order (lowest to highest precedence):
* 1. bundled - Package bundled skills
* 2. extra - User-configured extra directories
* 3. profile - ~/.super-multica/agent-profiles/<profileId>/skills/
*
* @param options - Loader options
* @returns Map of skill ID to Skill
*/
export function loadAllSkills(options: SkillManagerOptions = {}): Map<string, Skill> {
const skillMap = new Map<string, Skill>();
// Define sources in order of precedence (lowest first)
const sources: Array<[string, SkillSource]> = [
// Bundled skills (lowest precedence)
[BUNDLED_DIR, "bundled"],
// Extra directories (treated as bundled)
...(options.extraDirs ?? []).map((d): [string, SkillSource] => [d, "bundled"]),
];
// Add profile skills if profileId is provided (highest precedence)
if (options.profileId) {
const profileSkillsDir = getProfileSkillsDir(options.profileId, options.profileBaseDir);
sources.push([profileSkillsDir, "profile"]);
}
for (const [dir, source] of sources) {
const skills = loadSkillsFromSource(dir, source);
for (const skill of skills) {
const existing = skillMap.get(skill.id);
// Higher precedence overwrites lower
if (
!existing ||
SKILL_SOURCE_PRECEDENCE[source] > SKILL_SOURCE_PRECEDENCE[existing.source]
) {
skillMap.set(skill.id, skill);
}
}
}
return skillMap;
}
/**
* Get path to bundled skills directory
*/
export function getBundledSkillsDir(): string {
return BUNDLED_DIR;
}

132
src/agent/skills/parser.ts Normal file
View file

@ -0,0 +1,132 @@
/**
* SKILL.md Parser
*
* Parse skill files with YAML frontmatter and markdown body
*/
import { readFileSync } from "node:fs";
import { parse as parseYaml } from "yaml";
import type { Skill, SkillFrontmatter, SkillSource } from "./types.js";
/**
* Parse YAML frontmatter from markdown content
*
* @param content - Raw markdown content
* @returns Tuple of [frontmatter object or null, body content]
*/
export function parseFrontmatter(content: string): [Record<string, unknown> | null, string] {
// Match frontmatter between --- delimiters at the start
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) {
return [null, content.trim()];
}
const frontmatterRaw = match[1] ?? "";
const body = match[2] ?? "";
if (!frontmatterRaw) {
return [null, content.trim()];
}
try {
const frontmatter = parseYaml(frontmatterRaw) as Record<string, unknown>;
return [frontmatter, body.trim()];
} catch {
// Invalid YAML, return null frontmatter
return [null, content.trim()];
}
}
/**
* Validate and coerce frontmatter to SkillFrontmatter type
*
* @param raw - Raw parsed frontmatter
* @returns Validated SkillFrontmatter or null if invalid
*/
function validateFrontmatter(raw: Record<string, unknown>): SkillFrontmatter | null {
// Name is required
if (typeof raw.name !== "string" || raw.name.trim() === "") {
return null;
}
const frontmatter: SkillFrontmatter = {
name: raw.name.trim(),
};
if (typeof raw.description === "string") {
frontmatter.description = raw.description;
}
if (typeof raw.version === "string") {
frontmatter.version = raw.version;
}
if (typeof raw.author === "string") {
frontmatter.author = raw.author;
}
if (typeof raw.homepage === "string") {
frontmatter.homepage = raw.homepage;
}
// Parse metadata if present
if (typeof raw.metadata === "object" && raw.metadata !== null) {
const meta = raw.metadata as Record<string, unknown>;
frontmatter.metadata = {
emoji: typeof meta.emoji === "string" ? meta.emoji : undefined,
requiresEnv: Array.isArray(meta.requiresEnv)
? meta.requiresEnv.filter((v): v is string => typeof v === "string")
: undefined,
requiresBinaries: Array.isArray(meta.requiresBinaries)
? meta.requiresBinaries.filter((v): v is string => typeof v === "string")
: undefined,
platforms: Array.isArray(meta.platforms)
? meta.platforms.filter((v): v is string => typeof v === "string")
: undefined,
tags: Array.isArray(meta.tags)
? meta.tags.filter((v): v is string => typeof v === "string")
: undefined,
};
}
return frontmatter;
}
/**
* Parse a SKILL.md file into a Skill object
*
* @param filePath - Full path to SKILL.md file
* @param skillId - Unique identifier for the skill
* @param source - Source type of the skill
* @returns Parsed Skill or null if invalid
*/
export function parseSkillFile(
filePath: string,
skillId: string,
source: SkillSource,
): Skill | null {
try {
const content = readFileSync(filePath, "utf-8");
const [rawFrontmatter, instructions] = parseFrontmatter(content);
if (!rawFrontmatter) {
return null;
}
const frontmatter = validateFrontmatter(rawFrontmatter);
if (!frontmatter) {
return null;
}
return {
id: skillId,
frontmatter,
instructions,
source,
filePath,
};
} catch {
// File read error or other issues
return null;
}
}

97
src/agent/skills/types.ts Normal file
View file

@ -0,0 +1,97 @@
/**
* Skills Module Types
*
* Type definitions for the skills system
*/
/**
* Skill metadata for eligibility and display
*/
export interface SkillMetadata {
/** Emoji for display (e.g., "📝") */
emoji?: string | undefined;
/** Required environment variables */
requiresEnv?: string[] | undefined;
/** Required binaries in PATH */
requiresBinaries?: string[] | undefined;
/** Supported platforms (darwin, linux, win32) */
platforms?: string[] | undefined;
/** Skill tags for categorization */
tags?: string[] | undefined;
}
/**
* SKILL.md frontmatter schema
*/
export interface SkillFrontmatter {
/** Skill name (required) */
name: string;
/** Human-readable description */
description?: string | undefined;
/** Skill version */
version?: string | undefined;
/** Author information */
author?: string | undefined;
/** Homepage/documentation URL */
homepage?: string | undefined;
/** Skill-specific metadata */
metadata?: SkillMetadata | undefined;
}
/**
* Skill source type with precedence (lower value = lower priority)
*/
export type SkillSource = "bundled" | "profile";
/**
* Skill source precedence values
*/
export const SKILL_SOURCE_PRECEDENCE: Record<SkillSource, number> = {
bundled: 0,
profile: 1,
};
/**
* Parsed skill entry
*/
export interface Skill {
/** Unique skill identifier (directory name) */
id: string;
/** Parsed frontmatter */
frontmatter: SkillFrontmatter;
/** Skill instructions (markdown body after frontmatter) */
instructions: string;
/** Source type */
source: SkillSource;
/** Full path to SKILL.md */
filePath: string;
}
/**
* Skill Manager options
*/
export interface SkillManagerOptions {
/** Agent profile ID (for profile-specific skills) */
profileId?: string | undefined;
/** Profile base directory, defaults to ~/.super-multica/agent-profiles */
profileBaseDir?: string | undefined;
/** Additional directories to search for skills */
extraDirs?: string[] | undefined;
/** Platform override (for testing) */
platform?: NodeJS.Platform | undefined;
}
/**
* Skill eligibility check result
*/
export interface EligibilityResult {
/** Whether the skill is eligible */
eligible: boolean;
/** Reasons for ineligibility */
reasons?: string[] | undefined;
}
/**
* Filename constant for skill definition file
*/
export const SKILL_FILE = "SKILL.md";

View file

@ -48,4 +48,10 @@ export type AgentOptions = {
/** Enable debug logging */
debug?: boolean | undefined;
// === Skills Configuration ===
/** Enable skills system (default: true) */
enableSkills?: boolean | undefined;
/** Additional directories to search for skills */
extraSkillDirs?: string[] | undefined;
};