Merge pull request #55 from multica-ai/feature/skills-profile-and-autocomplete-fix

feat(skills): add --profile option and fix autocomplete terminal issues
This commit is contained in:
Bohan Jiang 2026-02-02 14:13:06 +08:00 committed by GitHub
commit 64878d9fe1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 302 additions and 107 deletions

View file

@ -3,6 +3,8 @@
*
* Real-time dropdown autocomplete for terminal input
* No external dependencies - uses raw terminal control
*
* Falls back to simple readline when terminal doesn't support advanced features
*/
import * as readline from "readline";
@ -30,10 +32,7 @@ const CURSOR_TO_COL = (n: number) => `${ESC}[${n}G`;
const RESET = `${ESC}[0m`;
const INVERSE = `${ESC}[7m`;
const SHOW_CURSOR = `${ESC}[?25h`;
const SAVE_CURSOR = `${ESC}[s`;
const RESTORE_CURSOR = `${ESC}[u`;
const CLEAR_TO_END = `${ESC}[J`;
const CURSOR_DOWN = (n: number) => (n > 0 ? `${ESC}[${n}B` : "");
// Strip ANSI escape codes to get visual length
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
@ -41,10 +40,99 @@ function stripAnsi(str: string): string {
return str.replace(ANSI_REGEX, "");
}
/**
* Get the visual width of a string in terminal columns
* Full-width characters (CJK, etc.) take 2 columns
*/
function getStringWidth(str: string): number {
let width = 0;
for (const char of str) {
const code = char.codePointAt(0);
if (code === undefined) continue;
// Check for full-width characters:
// - CJK Unified Ideographs (Chinese, Japanese Kanji, Korean Hanja)
// - CJK Symbols and Punctuation
// - Hiragana, Katakana
// - Hangul
// - Full-width ASCII and symbols
if (
(code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
(code >= 0x2e80 && code <= 0x9fff) || // CJK
(code >= 0xac00 && code <= 0xd7a3) || // Hangul Syllables
(code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
(code >= 0xfe10 && code <= 0xfe1f) || // Vertical forms
(code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms
(code >= 0xff00 && code <= 0xff60) || // Full-width ASCII
(code >= 0xffe0 && code <= 0xffe6) || // Full-width symbols
(code >= 0x20000 && code <= 0x2ffff) // CJK Extension B and beyond
) {
width += 2;
} else {
width += 1;
}
}
return width;
}
/**
* Check if terminal supports advanced cursor control
*/
function isTerminalSupported(): boolean {
// Check TERM environment variable
const term = process.env.TERM;
if (!term) {
return false;
}
// Check if running in known unsupported environments
const unsupportedTerms = ["dumb", "emacs"];
if (unsupportedTerms.includes(term.toLowerCase())) {
return false;
}
// Check if stdout is a TTY
if (!process.stdout.isTTY) {
return false;
}
return true;
}
/**
* Simple readline input (fallback for unsupported terminals)
*/
function simpleInput(config: AutocompleteConfig): Promise<string> {
return new Promise((resolve) => {
const { prompt = "> " } = config;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
});
rl.question(prompt, (answer) => {
rl.close();
resolve(answer);
});
rl.on("close", () => {
resolve("");
});
});
}
/**
* Read a line with real-time autocomplete dropdown
* Falls back to simple readline on unsupported terminals
*/
export function autocompleteInput(config: AutocompleteConfig): Promise<string> {
// Fall back to simple input if terminal doesn't support advanced features
if (!isTerminalSupported()) {
return simpleInput(config);
}
return new Promise((resolve) => {
const { getSuggestions, prompt = "> ", maxSuggestions = 5 } = config;
@ -55,12 +143,14 @@ export function autocompleteInput(config: AutocompleteConfig): Promise<string> {
let cursorPos = 0;
let suggestions: AutocompleteOption[] = [];
let selectedIndex = -1;
let initialized = false;
let lastRenderedLines = 0; // Track how many lines we rendered (for cleanup)
// Enable raw mode
if (stdin.isTTY) {
stdin.setRawMode(true);
}
// Set up keypress events
readline.emitKeypressEvents(stdin);
const cleanup = () => {
@ -71,43 +161,40 @@ export function autocompleteInput(config: AutocompleteConfig): Promise<string> {
stdin.removeListener("keypress", onKeypress);
};
const render = () => {
if (!initialized) {
// First render - save cursor position as anchor
stdout.write(SAVE_CURSOR);
initialized = true;
} else {
// Restore to anchor and clear everything after it
stdout.write(RESTORE_CURSOR);
const clearDisplay = () => {
// Move to beginning of current line
stdout.write("\r");
// Clear current line
stdout.write(CLEAR_LINE);
// Clear any suggestion lines below
if (lastRenderedLines > 0) {
stdout.write(CLEAR_TO_END);
// Re-save in case terminal scrolled
stdout.write(SAVE_CURSOR);
}
};
const render = () => {
clearDisplay();
// Write prompt and input
stdout.write(`${prompt}${input}`);
// Calculate cursor position accounting for line wrapping
// Calculate cursor position accounting for line wrapping and wide characters
const termWidth = stdout.columns || 80;
const promptVisualLen = stripAnsi(prompt).length;
const cursorOffset = promptVisualLen + cursorPos;
const promptVisualWidth = getStringWidth(stripAnsi(prompt));
// Calculate visual width of input up to cursor position
const inputBeforeCursor = input.slice(0, cursorPos);
const inputVisualWidth = getStringWidth(inputBeforeCursor);
const cursorOffset = promptVisualWidth + inputVisualWidth;
// Handle edge case: when cursor is exactly at line boundary,
// show it at end of current line, not start of next line
let cursorRow: number;
let cursorCol: number;
if (cursorOffset > 0 && cursorOffset % termWidth === 0) {
cursorRow = cursorOffset / termWidth - 1;
cursorCol = termWidth;
} else {
cursorRow = Math.floor(cursorOffset / termWidth);
cursorCol = (cursorOffset % termWidth) + 1;
}
// Calculate total lines for suggestions positioning
const totalLength = promptVisualLen + input.length;
const totalLines = Math.ceil(totalLength / termWidth) || 1;
// Get and display suggestions if input starts with /
if (input.startsWith("/") && input.length > 1) {
suggestions = getSuggestions(input).slice(0, maxSuggestions);
@ -118,8 +205,7 @@ export function autocompleteInput(config: AutocompleteConfig): Promise<string> {
selectedIndex = suggestions.length - 1;
}
// Move to end of input text before showing suggestions
// Cursor is currently at end of text, just go to new line
// Move to new line for suggestions
stdout.write("\n");
for (let i = 0; i < suggestions.length; i++) {
@ -137,30 +223,26 @@ export function autocompleteInput(config: AutocompleteConfig): Promise<string> {
}
}
// Move cursor back up to input line (accounting for wrapped lines)
const linesFromEnd = totalLines - 1 - cursorRow;
stdout.write(CURSOR_UP(suggestions.length + linesFromEnd));
lastRenderedLines = suggestions.length;
// Move cursor back up to input line
stdout.write(CURSOR_UP(suggestions.length));
stdout.write(CURSOR_TO_COL(cursorCol));
} else {
lastRenderedLines = 0;
}
} else {
suggestions = [];
selectedIndex = -1;
lastRenderedLines = 0;
}
// Position cursor for wrapped text
// After writing, cursor is at end of text. Move to correct position.
// Go back to start of input block, then move to target row/col
const endRow = totalLines - 1;
if (endRow > cursorRow) {
stdout.write(CURSOR_UP(endRow - cursorRow));
}
// Position cursor correctly within input
stdout.write(CURSOR_TO_COL(cursorCol));
};
const submit = (value: string) => {
// Clear suggestions before submitting
stdout.write(RESTORE_CURSOR);
stdout.write(CLEAR_TO_END);
clearDisplay();
stdout.write(`${prompt}${value}\n`);
cleanup();
resolve(value);
@ -171,12 +253,14 @@ export function autocompleteInput(config: AutocompleteConfig): Promise<string> {
// Handle Ctrl+C
if (key.ctrl && key.name === "c") {
clearDisplay();
cleanup();
process.exit(0);
}
// Handle Ctrl+D (EOF)
if (key.ctrl && key.name === "d") {
clearDisplay();
cleanup();
stdout.write("\n");
resolve("");

View file

@ -29,6 +29,7 @@ interface ParsedArgs {
args: string[];
verbose: boolean;
force: boolean;
profile?: string;
}
function printHelp() {
@ -46,6 +47,7 @@ ${cyan("Commands:")}
${cyan("Options:")}
${yellow("-v, --verbose")} Show more details
${yellow("-f, --force")} Force overwrite existing skill
${yellow("-p, --profile")} <id> Install to specific profile's skills directory
${cyan("Source Formats:")} ${dim("(for add command)")}
owner/repo Clone entire repository
@ -68,6 +70,9 @@ ${cyan("Examples:")}
${dim("# Remove a skill")}
multica skills remove agent-skills
${dim("# Add skill to a specific profile")}
multica skills add owner/repo --profile my-agent
`);
}
@ -75,6 +80,7 @@ function parseArgs(argv: string[]): ParsedArgs {
const args = [...argv];
let verbose = false;
let force = false;
let profile: string | undefined;
const positional: string[] = [];
while (args.length > 0) {
@ -91,8 +97,13 @@ function parseArgs(argv: string[]): ParsedArgs {
continue;
}
if (arg === "--profile" || arg === "-p") {
profile = args.shift();
continue;
}
if (arg === "--help" || arg === "-h") {
return { command: "help", args: [], verbose, force };
return { command: "help", args: [], verbose, force, profile };
}
positional.push(arg);
@ -101,7 +112,7 @@ function parseArgs(argv: string[]): ParsedArgs {
const command = (positional[0] ?? "help") as Command;
const commandArgs = positional.slice(1);
return { command, args: commandArgs, verbose, force };
return { command, args: commandArgs, verbose, force, profile };
}
function cmdList(manager: SkillManager, verbose: boolean): void {
@ -399,12 +410,14 @@ async function cmdInstall(manager: SkillManager, skillId: string, installId?: st
}
}
async function cmdAdd(source: string, force: boolean): Promise<void> {
console.log(`\nAdding skill from '${source}'...`);
async function cmdAdd(source: string, force: boolean, profileId?: string): Promise<void> {
const destination = profileId ? `profile '${profileId}'` : "global skills";
console.log(`\nAdding skill from '${source}' to ${destination}...`);
const result = await addSkill({
source,
force,
profileId,
});
if (result.ok) {
@ -454,7 +467,7 @@ async function cmdListInstalled(): Promise<void> {
}
export async function skillsCommand(args: string[]): Promise<void> {
const { command, args: cmdArgs, verbose, force } = parseArgs(args);
const { command, args: cmdArgs, verbose, force, profile } = parseArgs(args);
if (command === "help") {
printHelp();
@ -464,14 +477,17 @@ export async function skillsCommand(args: string[]): Promise<void> {
switch (command) {
case "add":
if (!cmdArgs[0]) {
console.error("Usage: multica skills add <source> [--force]");
console.error("Usage: multica skills add <source> [--force] [--profile <id>]");
console.error("\nSource formats:");
console.error(" owner/repo Clone entire repository");
console.error(" owner/repo/skill-name Clone single skill directory");
console.error(" owner/repo@branch Clone specific branch/tag");
console.error("\nOptions:");
console.error(" --force, -f Overwrite existing skill");
console.error(" --profile, -p <id> Install to profile's skills directory");
process.exit(1);
}
await cmdAdd(cmdArgs[0], force);
await cmdAdd(cmdArgs[0], force, profile);
return;
case "remove":

View file

@ -203,6 +203,40 @@ Higher priority sources override skills with the same ID.
On first run, bundled skills are automatically copied to the managed directory (`~/.super-multica/skills/`). This makes them editable and allows users to customize or remove them.
### Adding Profile-Specific Skills
You can install skills directly to a profile using the `--profile` option:
```bash
# Install skill to a specific profile
multica skills add owner/repo --profile my-agent
# Install with force overwrite
multica skills add owner/repo/skill-name --profile my-agent --force
```
Alternatively, create them manually:
```bash
# Create profile skills directory
mkdir -p ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>
# Create the SKILL.md file
cat > ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>/SKILL.md << 'EOF'
---
name: My Profile Skill
version: 1.0.0
description: A skill specific to this profile
---
# Instructions
Your skill instructions here...
EOF
```
Profile skills automatically override managed skills with the same ID, allowing per-profile customization.
### Eligibility Filtering
After loading, skills are filtered by:
@ -219,13 +253,15 @@ Only skills passing all checks are marked as eligible.
## CLI Commands
All commands use the unified `multica` CLI (or `pnpm multica` during development).
### List Skills
```bash
pnpm skills:cli list # List all skills
pnpm skills:cli list -v # Verbose mode
pnpm skills:cli status # Summary status
pnpm skills:cli status <id> # Specific skill status
multica skills list # List all skills
multica skills list -v # Verbose mode
multica skills status # Summary status
multica skills status <id> # Specific skill status
```
### Install from GitHub
@ -247,32 +283,32 @@ anthropics/skills/
Install the entire repository (all 16 skills):
```bash
pnpm skills:cli add anthropics/skills
multica skills add anthropics/skills
# Installs to: ~/.super-multica/skills/skills/
# All skills available: algorithmic-art, brand-guidelines, pdf, etc.
```
Install a single skill only:
```bash
pnpm skills:cli add anthropics/skills/skills/pdf
multica skills add anthropics/skills/skills/pdf
# Installs to: ~/.super-multica/skills/pdf/
# Only the pdf skill is installed
```
Install from a specific branch or tag:
```bash
pnpm skills:cli add anthropics/skills@main
multica skills add anthropics/skills@main
```
Using full URL:
```bash
pnpm skills:cli add https://github.com/anthropics/skills
pnpm skills:cli add https://github.com/anthropics/skills/tree/main/skills/pdf
multica skills add https://github.com/anthropics/skills
multica skills add https://github.com/anthropics/skills/tree/main/skills/pdf
```
Force overwrite existing:
```bash
pnpm skills:cli add anthropics/skills --force
multica skills add anthropics/skills --force
```
**Supported formats:**
@ -288,15 +324,15 @@ pnpm skills:cli add anthropics/skills --force
### Remove Skills
```bash
pnpm skills:cli remove <name> # Remove installed skill
pnpm skills:cli remove # List installed skills
multica skills remove <name> # Remove installed skill
multica skills remove # List installed skills
```
### Install Dependencies
```bash
pnpm skills:cli install <id> # Install skill dependencies
pnpm skills:cli install <id> <install-id> # Specific install option
multica skills install <id> # Install skill dependencies
multica skills install <id> <install-id> # Specific install option
```
---
@ -308,8 +344,8 @@ The `status` command provides detailed diagnostics for understanding why skills
### Summary Status
```bash
pnpm skills:cli status # Show summary with grouping by issue type
pnpm skills:cli status -v # Verbose mode with hints
multica skills status # Show summary with grouping by issue type
multica skills status -v # Verbose mode with hints
```
Output shows:
@ -319,7 +355,7 @@ Output shows:
### Detailed Skill Status
```bash
pnpm skills:cli status <skill-id>
multica skills status <skill-id>
```
Output includes:

View file

@ -203,6 +203,40 @@ Skills 从两个来源加载,优先级从低到高:
首次运行时,内置 skills 会自动复制到 managed 目录(`~/.super-multica/skills/`)。这使得用户可以编辑或删除它们。
### 添加 Profile 专属 Skills
可以使用 `--profile` 选项直接安装 skills 到特定 profile
```bash
# 安装 skill 到特定 profile
multica skills add owner/repo --profile my-agent
# 强制覆盖安装
multica skills add owner/repo/skill-name --profile my-agent --force
```
也可以手动创建:
```bash
# 创建 profile skills 目录
mkdir -p ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>
# 创建 SKILL.md 文件
cat > ~/.super-multica/agent-profiles/<profile-id>/skills/<skill-name>/SKILL.md << 'EOF'
---
name: My Profile Skill
version: 1.0.0
description: 此 profile 专属的 skill
---
# 说明
你的 skill 说明内容...
EOF
```
Profile skills 会自动覆盖同 ID 的 managed skills允许按 profile 自定义。
### 资格过滤
加载后skills 会按以下条件过滤:
@ -219,13 +253,15 @@ Skills 从两个来源加载,优先级从低到高:
## CLI 命令
所有命令使用统一的 `multica` CLI开发时使用 `pnpm multica`)。
### 列出 Skills
```bash
pnpm skills:cli list # 列出所有 skills
pnpm skills:cli list -v # 详细模式
pnpm skills:cli status # 汇总状态
pnpm skills:cli status <id> # 特定 skill 状态
multica skills list # 列出所有 skills
multica skills list -v # 详细模式
multica skills status # 汇总状态
multica skills status <id> # 特定 skill 状态
```
### 从 GitHub 安装
@ -247,32 +283,32 @@ anthropics/skills/
安装整个仓库(所有 16 个 skills
```bash
pnpm skills:cli add anthropics/skills
multica skills add anthropics/skills
# 安装到:~/.super-multica/skills/skills/
# 所有 skills 可用algorithmic-art、brand-guidelines、pdf 等
```
只安装单个 skill
```bash
pnpm skills:cli add anthropics/skills/skills/pdf
multica skills add anthropics/skills/skills/pdf
# 安装到:~/.super-multica/skills/pdf/
# 只安装 pdf skill
```
从特定分支或标签安装:
```bash
pnpm skills:cli add anthropics/skills@main
multica skills add anthropics/skills@main
```
使用完整 URL
```bash
pnpm skills:cli add https://github.com/anthropics/skills
pnpm skills:cli add https://github.com/anthropics/skills/tree/main/skills/pdf
multica skills add https://github.com/anthropics/skills
multica skills add https://github.com/anthropics/skills/tree/main/skills/pdf
```
强制覆盖现有:
```bash
pnpm skills:cli add anthropics/skills --force
multica skills add anthropics/skills --force
```
**支持的格式:**
@ -288,15 +324,15 @@ pnpm skills:cli add anthropics/skills --force
### 移除 Skills
```bash
pnpm skills:cli remove <name> # 移除已安装的 skill
pnpm skills:cli remove # 列出已安装的 skills
multica skills remove <name> # 移除已安装的 skill
multica skills remove # 列出已安装的 skills
```
### 安装依赖
```bash
pnpm skills:cli install <id> # 安装 skill 依赖
pnpm skills:cli install <id> <install-id> # 特定安装选项
multica skills install <id> # 安装 skill 依赖
multica skills install <id> <install-id> # 特定安装选项
```
---
@ -308,8 +344,8 @@ pnpm skills:cli install <id> <install-id> # 特定安装选项
### 汇总状态
```bash
pnpm skills:cli status # 显示按问题类型分组的汇总
pnpm skills:cli status -v # 详细模式带提示
multica skills status # 显示按问题类型分组的汇总
multica skills status -v # 详细模式带提示
```
输出显示:
@ -319,7 +355,7 @@ pnpm skills:cli status -v # 详细模式带提示
### 详细 Skill 状态
```bash
pnpm skills:cli status <skill-id>
multica skills status <skill-id>
```
输出包括:
@ -387,7 +423,7 @@ import {
**Skill 未显示为符合条件?**
运行 `pnpm skills:cli status <skill-id>` 查看详细诊断及可操作的提示。
运行 `multica skills status <skill-id>` 查看详细诊断及可操作的提示。
**覆盖内置 skill**

View file

@ -32,6 +32,8 @@ export interface SkillAddRequest {
force?: boolean | undefined;
/** Timeout in milliseconds (default: 60000) */
timeoutMs?: number | undefined;
/** Profile ID to install to (installs to profile's skills directory instead of global) */
profileId?: string | undefined;
}
export interface SkillAddResult {
@ -66,6 +68,19 @@ const DEFAULT_TIMEOUT_MS = 60_000;
/** Skills directory: ~/.super-multica/skills */
const SKILLS_DIR = join(DATA_DIR, "skills");
/** Profile base directory: ~/.super-multica/agent-profiles */
const PROFILE_BASE_DIR = join(DATA_DIR, "agent-profiles");
/**
* Get the skills directory for a given profile or global
*/
function getSkillsDir(profileId?: string): string {
if (profileId) {
return join(PROFILE_BASE_DIR, profileId, "skills");
}
return SKILLS_DIR;
}
// ============================================================================
// Source Parsing
// ============================================================================
@ -396,9 +411,12 @@ async function addSkillInternal(request: SkillAddRequest): Promise<SkillAddResul
const { owner, repo, skillPath, ref } = parsed;
const repoUrl = `https://github.com/${owner}/${repo}.git`;
// Determine target directory based on profileId
const skillsDir = getSkillsDir(request.profileId);
// Determine target name
const targetName = request.name ?? (skillPath ? basename(skillPath) : repo);
const targetDir = join(SKILLS_DIR, targetName);
const targetDir = join(skillsDir, targetName);
// Check if exists
if (existsSync(targetDir) && !request.force) {
@ -409,7 +427,7 @@ async function addSkillInternal(request: SkillAddRequest): Promise<SkillAddResul
}
// Ensure skills directory exists
await mkdir(SKILLS_DIR, { recursive: true });
await mkdir(skillsDir, { recursive: true });
// Remove existing if force
if (existsSync(targetDir)) {
@ -492,12 +510,13 @@ async function addSkillInternal(request: SkillAddRequest): Promise<SkillAddResul
return dir === targetDir ? targetName : basename(dir);
});
const destination = request.profileId ? `profile '${request.profileId}'` : "global skills";
return {
ok: true,
message:
skillNames.length === 1
? `Added skill '${targetName}' to ${targetDir}`
: `Added ${skillNames.length} skills from ${owner}/${repo}`,
? `Added skill '${targetName}' to ${destination}`
: `Added ${skillNames.length} skills from ${owner}/${repo} to ${destination}`,
path: targetDir,
skills: skillNames.length > 0 ? skillNames : [targetName],
};

View file

@ -99,18 +99,20 @@ Profiles are predefined tool sets for common use cases:
### CLI Usage
All commands use the unified `multica` CLI (or `pnpm multica` during development).
```bash
# Use a specific profile
pnpm agent:cli --tools-profile coding "list files"
multica run --tools-profile coding "list files"
# Minimal profile with specific tools allowed
pnpm agent:cli --tools-profile minimal --tools-allow exec "run ls"
multica run --tools-profile minimal --tools-allow exec "run ls"
# Deny specific tools
pnpm agent:cli --tools-deny exec,process "read file.txt"
multica run --tools-deny exec,process "read file.txt"
# Use tool groups
pnpm agent:cli --tools-allow group:fs "read config.json"
multica run --tools-allow group:fs "read config.json"
```
### Programmatic Usage
@ -146,19 +148,19 @@ Use the tools CLI to inspect and test configurations:
```bash
# List all available tools
pnpm tools:cli list
multica tools list
# List tools after applying a profile
pnpm tools:cli list --profile coding
multica tools list --profile coding
# List tools with deny rules
pnpm tools:cli list --profile coding --deny exec
multica tools list --profile coding --deny exec
# Show all tool groups
pnpm tools:cli groups
multica tools groups
# Show all profiles
pnpm tools:cli profiles
multica tools profiles
```
## Policy System Details
@ -261,7 +263,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
Run the policy system tests:
```bash
npx tsx src/agent/tools/policy.test.ts
pnpm test src/agent/tools/policy.test.ts
```
## Agent Profile Integration
@ -315,7 +317,7 @@ When both Profile config and CLI options are provided:
# Profile has tools.profile = "coding"
# CLI adds --tools-deny exec
# Result: coding profile without exec tool
pnpm agent:cli --profile my-agent --tools-deny exec "list files"
multica run --profile my-agent --tools-deny exec "list files"
```
## Future Tools

View file

@ -99,18 +99,20 @@
### CLI 使用
所有命令使用统一的 `multica` CLI开发时使用 `pnpm multica`)。
```bash
# 使用特定配置文件
pnpm agent:cli --tools-profile coding "list files"
multica run --tools-profile coding "list files"
# 最小配置文件 + 允许特定工具
pnpm agent:cli --tools-profile minimal --tools-allow exec "run ls"
multica run --tools-profile minimal --tools-allow exec "run ls"
# 禁止特定工具
pnpm agent:cli --tools-deny exec,process "read file.txt"
multica run --tools-deny exec,process "read file.txt"
# 使用工具组
pnpm agent:cli --tools-allow group:fs "read config.json"
multica run --tools-allow group:fs "read config.json"
```
### 编程使用
@ -146,19 +148,19 @@ const agent = new Agent({
```bash
# 列出所有可用工具
pnpm tools:cli list
multica tools list
# 列出应用配置文件后的工具
pnpm tools:cli list --profile coding
multica tools list --profile coding
# 列出带有禁止规则的工具
pnpm tools:cli list --profile coding --deny exec
multica tools list --profile coding --deny exec
# 显示所有工具组
pnpm tools:cli groups
multica tools groups
# 显示所有配置文件
pnpm tools:cli profiles
multica tools profiles
```
## 策略系统详情
@ -261,7 +263,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
运行策略系统测试:
```bash
npx tsx src/agent/tools/policy.test.ts
pnpm test src/agent/tools/policy.test.ts
```
## Agent Profile 集成
@ -315,7 +317,7 @@ npx tsx src/agent/tools/policy.test.ts
# Profile 有 tools.profile = "coding"
# CLI 添加 --tools-deny exec
# 结果: coding 配置文件但没有 exec 工具
pnpm agent:cli --profile my-agent --tools-deny exec "list files"
multica run --profile my-agent --tools-deny exec "list files"
```
## 未来工具