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:
commit
64878d9fe1
7 changed files with 302 additions and 107 deletions
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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?**
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
||||
## 未来工具
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue