mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
coding-agent: change Pi skills to use SKILL.md convention
Breaking change: Pi skills must now be named SKILL.md inside a directory, matching Codex CLI format. Previously any *.md file was treated as a skill. Migrate by renaming ~/.pi/agent/skills/foo.md to ~/.pi/agent/skills/foo/SKILL.md
This commit is contained in:
parent
236856aa48
commit
3b2b9abffc
5 changed files with 123 additions and 153 deletions
|
|
@ -3608,23 +3608,6 @@ export const MODELS = {
|
|||
contextWindow: 262144,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meituan/longcat-flash-chat:free": {
|
||||
id: "meituan/longcat-flash-chat:free",
|
||||
name: "Meituan: LongCat Flash Chat (free)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen-plus-2025-07-28": {
|
||||
id: "qwen/qwen-plus-2025-07-28",
|
||||
name: "Qwen: Qwen Plus 0728",
|
||||
|
|
@ -5750,23 +5733,6 @@ export const MODELS = {
|
|||
contextWindow: 32768,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"cohere/command-r-08-2024": {
|
||||
id: "cohere/command-r-08-2024",
|
||||
name: "Cohere: Command R (08-2024)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 0.6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"cohere/command-r-plus-08-2024": {
|
||||
id: "cohere/command-r-plus-08-2024",
|
||||
name: "Cohere: Command R+ (08-2024)",
|
||||
|
|
@ -5784,6 +5750,23 @@ export const MODELS = {
|
|||
contextWindow: 128000,
|
||||
maxTokens: 4000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"cohere/command-r-08-2024": {
|
||||
id: "cohere/command-r-08-2024",
|
||||
name: "Cohere: Command R (08-2024)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 0.6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"sao10k/l3.1-euryale-70b": {
|
||||
id: "sao10k/l3.1-euryale-70b",
|
||||
name: "Sao10K: Llama 3.1 Euryale 70B v2.2",
|
||||
|
|
@ -5852,23 +5835,6 @@ export const MODELS = {
|
|||
contextWindow: 131072,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-405b-instruct": {
|
||||
id: "meta-llama/llama-3.1-405b-instruct",
|
||||
name: "Meta: Llama 3.1 405B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 3.5,
|
||||
output: 3.5,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 130815,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-70b-instruct": {
|
||||
id: "meta-llama/llama-3.1-70b-instruct",
|
||||
name: "Meta: Llama 3.1 70B Instruct",
|
||||
|
|
@ -5886,6 +5852,23 @@ export const MODELS = {
|
|||
contextWindow: 131072,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-405b-instruct": {
|
||||
id: "meta-llama/llama-3.1-405b-instruct",
|
||||
name: "Meta: Llama 3.1 405B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 3.5,
|
||||
output: 3.5,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 130815,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mistral-nemo": {
|
||||
id: "mistralai/mistral-nemo",
|
||||
name: "Mistral: Mistral Nemo",
|
||||
|
|
@ -5903,9 +5886,9 @@ export const MODELS = {
|
|||
contextWindow: 131072,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-4o-mini-2024-07-18": {
|
||||
id: "openai/gpt-4o-mini-2024-07-18",
|
||||
name: "OpenAI: GPT-4o-mini (2024-07-18)",
|
||||
"openai/gpt-4o-mini": {
|
||||
id: "openai/gpt-4o-mini",
|
||||
name: "OpenAI: GPT-4o-mini",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
|
|
@ -5920,9 +5903,9 @@ export const MODELS = {
|
|||
contextWindow: 128000,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-4o-mini": {
|
||||
id: "openai/gpt-4o-mini",
|
||||
name: "OpenAI: GPT-4o-mini",
|
||||
"openai/gpt-4o-mini-2024-07-18": {
|
||||
id: "openai/gpt-4o-mini-2024-07-18",
|
||||
name: "OpenAI: GPT-4o-mini (2024-07-18)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
|
|
@ -6073,23 +6056,6 @@ export const MODELS = {
|
|||
contextWindow: 128000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3-70b-instruct": {
|
||||
id: "meta-llama/llama-3-70b-instruct",
|
||||
name: "Meta: Llama 3 70B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.3,
|
||||
output: 0.39999999999999997,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 8192,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3-8b-instruct": {
|
||||
id: "meta-llama/llama-3-8b-instruct",
|
||||
name: "Meta: Llama 3 8B Instruct",
|
||||
|
|
@ -6107,6 +6073,23 @@ export const MODELS = {
|
|||
contextWindow: 8192,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3-70b-instruct": {
|
||||
id: "meta-llama/llama-3-70b-instruct",
|
||||
name: "Meta: Llama 3 70B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.3,
|
||||
output: 0.39999999999999997,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 8192,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mixtral-8x22b-instruct": {
|
||||
id: "mistralai/mixtral-8x22b-instruct",
|
||||
name: "Mistral: Mixtral 8x22B Instruct",
|
||||
|
|
@ -6192,23 +6175,6 @@ export const MODELS = {
|
|||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-3.5-turbo-0613": {
|
||||
id: "openai/gpt-3.5-turbo-0613",
|
||||
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 4095,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-4-turbo-preview": {
|
||||
id: "openai/gpt-4-turbo-preview",
|
||||
name: "OpenAI: GPT-4 Turbo Preview",
|
||||
|
|
@ -6226,6 +6192,23 @@ export const MODELS = {
|
|||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-3.5-turbo-0613": {
|
||||
id: "openai/gpt-3.5-turbo-0613",
|
||||
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 4095,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mistral-tiny": {
|
||||
id: "mistralai/mistral-tiny",
|
||||
name: "Mistral Tiny",
|
||||
|
|
@ -6294,9 +6277,9 @@ export const MODELS = {
|
|||
contextWindow: 16385,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-4-0314": {
|
||||
id: "openai/gpt-4-0314",
|
||||
name: "OpenAI: GPT-4 (older v0314)",
|
||||
"openai/gpt-4": {
|
||||
id: "openai/gpt-4",
|
||||
name: "OpenAI: GPT-4",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
|
|
@ -6311,9 +6294,9 @@ export const MODELS = {
|
|||
contextWindow: 8191,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-4": {
|
||||
id: "openai/gpt-4",
|
||||
name: "OpenAI: GPT-4",
|
||||
"openai/gpt-4-0314": {
|
||||
id: "openai/gpt-4-0314",
|
||||
name: "OpenAI: GPT-4 (older v0314)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
|
|
|
|||
|
|
@ -8,22 +8,14 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Pi skills now use `SKILL.md` convention**: Pi skills must now be named `SKILL.md` inside a directory, matching Codex CLI format. Previously any `*.md` file was treated as a skill. Migrate by renaming `~/.pi/agent/skills/foo.md` to `~/.pi/agent/skills/foo/SKILL.md`.
|
||||
|
||||
### Added
|
||||
|
||||
- Display loaded skills on startup in interactive mode
|
||||
|
||||
### Fixed
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
### Added
|
||||
|
||||
### Fixed
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
## [0.19.0] - 2025-12-12
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ Skills are discovered from these locations (in order of priority, later wins on
|
|||
1. `~/.codex/skills/**/SKILL.md` (Codex CLI user skills, recursive)
|
||||
2. `~/.claude/skills/*/SKILL.md` (Claude Code user skills)
|
||||
3. `<cwd>/.claude/skills/*/SKILL.md` (Claude Code project skills)
|
||||
4. `~/.pi/agent/skills/**/*.md` (Pi user skills, recursive)
|
||||
5. `<cwd>/.pi/skills/**/*.md` (Pi project skills, recursive)
|
||||
4. `~/.pi/agent/skills/**/SKILL.md` (Pi user skills, recursive)
|
||||
5. `<cwd>/.pi/skills/**/SKILL.md` (Pi project skills, recursive)
|
||||
|
||||
Skill names and descriptions are listed in the system prompt. When a task matches a skill's description, the agent uses the `read` tool to load it.
|
||||
|
||||
|
|
@ -43,13 +43,13 @@ The parser only supports single-line `key: value` syntax. Multiline YAML blocks
|
|||
|
||||
### Variables
|
||||
|
||||
`{baseDir}` is replaced with the directory containing the skill file. Use it to reference bundled scripts or resources.
|
||||
Use `{baseDir}` as a placeholder for the skill's directory. The agent is told each skill's base directory and will substitute it when following the instructions.
|
||||
|
||||
### Subdirectories (Pi Skills)
|
||||
### Subdirectories
|
||||
|
||||
Pi skills in subdirectories use colon-separated names:
|
||||
- `~/.pi/agent/skills/db/migrate.md` → `db:migrate`
|
||||
- `<cwd>/.pi/skills/aws/s3/upload.md` → `aws:s3:upload`
|
||||
Pi and Codex skills in subdirectories use colon-separated names:
|
||||
- `~/.pi/agent/skills/db/migrate/SKILL.md` → `db:migrate`
|
||||
- `<cwd>/.pi/skills/aws/s3/upload/SKILL.md` → `aws:s3:upload`
|
||||
|
||||
## Claude Code Compatibility
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export interface Skill {
|
|||
source: SkillSource;
|
||||
}
|
||||
|
||||
type SkillFormat = "pi" | "claude" | "codex";
|
||||
type SkillFormat = "recursive" | "claude";
|
||||
|
||||
function stripQuotes(value: string): string {
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
|
|
@ -60,7 +60,13 @@ function parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; bod
|
|||
return { frontmatter, body };
|
||||
}
|
||||
|
||||
function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat, subdir: string = ""): Skill[] {
|
||||
function loadSkillsFromDir(
|
||||
dir: string,
|
||||
source: SkillSource,
|
||||
format: SkillFormat,
|
||||
useColonPath: boolean = false,
|
||||
subdir: string = "",
|
||||
): Skill[] {
|
||||
const skills: Skill[] = [];
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
|
|
@ -81,11 +87,12 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat
|
|||
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (format === "pi") {
|
||||
if (format === "recursive") {
|
||||
// Recursive format: scan directories, look for SKILL.md files
|
||||
if (entry.isDirectory()) {
|
||||
const newSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;
|
||||
skills.push(...loadSkillsFromDir(fullPath, source, format, newSubdir));
|
||||
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
skills.push(...loadSkillsFromDir(fullPath, source, format, useColonPath, newSubdir));
|
||||
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
||||
try {
|
||||
const rawContent = readFileSync(fullPath, "utf-8");
|
||||
const { frontmatter } = parseFrontmatter(rawContent);
|
||||
|
|
@ -94,19 +101,22 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat
|
|||
continue;
|
||||
}
|
||||
|
||||
const nameFromFile = entry.name.slice(0, -3);
|
||||
const name = frontmatter.name || (subdir ? `${subdir}:${nameFromFile}` : nameFromFile);
|
||||
const skillDir = dirname(fullPath);
|
||||
// useColonPath: db:migrate (pi), otherwise just: migrate (codex)
|
||||
const nameFromPath = useColonPath ? subdir || basename(skillDir) : basename(skillDir);
|
||||
const name = frontmatter.name || nameFromPath;
|
||||
|
||||
skills.push({
|
||||
name,
|
||||
description: frontmatter.description,
|
||||
filePath: fullPath,
|
||||
baseDir: dirname(fullPath),
|
||||
baseDir: skillDir,
|
||||
source,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
} else if (format === "claude") {
|
||||
// Claude format: only one level deep, each directory must contain SKILL.md
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -136,30 +146,6 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat
|
|||
source,
|
||||
});
|
||||
} catch {}
|
||||
} else if (format === "codex") {
|
||||
if (entry.isDirectory()) {
|
||||
skills.push(...loadSkillsFromDir(fullPath, source, format));
|
||||
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
||||
try {
|
||||
const rawContent = readFileSync(fullPath, "utf-8");
|
||||
const { frontmatter } = parseFrontmatter(rawContent);
|
||||
|
||||
if (!frontmatter.description) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const skillDir = dirname(fullPath);
|
||||
const name = frontmatter.name || basename(skillDir);
|
||||
|
||||
skills.push({
|
||||
name,
|
||||
description: frontmatter.description,
|
||||
filePath: fullPath,
|
||||
baseDir: skillDir,
|
||||
source,
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
|
@ -170,28 +156,31 @@ function loadSkillsFromDir(dir: string, source: SkillSource, format: SkillFormat
|
|||
export function loadSkills(): Skill[] {
|
||||
const skillMap = new Map<string, Skill>();
|
||||
|
||||
// Codex: recursive, simple directory name
|
||||
const codexUserDir = join(homedir(), ".codex", "skills");
|
||||
for (const skill of loadSkillsFromDir(codexUserDir, "codex-user", "codex")) {
|
||||
for (const skill of loadSkillsFromDir(codexUserDir, "codex-user", "recursive", false)) {
|
||||
skillMap.set(skill.name, skill);
|
||||
}
|
||||
|
||||
// Claude: single level only
|
||||
const claudeUserDir = join(homedir(), ".claude", "skills");
|
||||
for (const skill of loadSkillsFromDir(claudeUserDir, "claude-user", "claude")) {
|
||||
for (const skill of loadSkillsFromDir(claudeUserDir, "claude-user", "claude", false)) {
|
||||
skillMap.set(skill.name, skill);
|
||||
}
|
||||
|
||||
const claudeProjectDir = resolve(process.cwd(), ".claude", "skills");
|
||||
for (const skill of loadSkillsFromDir(claudeProjectDir, "claude-project", "claude")) {
|
||||
for (const skill of loadSkillsFromDir(claudeProjectDir, "claude-project", "claude", false)) {
|
||||
skillMap.set(skill.name, skill);
|
||||
}
|
||||
|
||||
// Pi: recursive, colon-separated path names
|
||||
const globalSkillsDir = join(homedir(), CONFIG_DIR_NAME, "agent", "skills");
|
||||
for (const skill of loadSkillsFromDir(globalSkillsDir, "user", "pi")) {
|
||||
for (const skill of loadSkillsFromDir(globalSkillsDir, "user", "recursive", true)) {
|
||||
skillMap.set(skill.name, skill);
|
||||
}
|
||||
|
||||
const projectSkillsDir = resolve(process.cwd(), CONFIG_DIR_NAME, "skills");
|
||||
for (const skill of loadSkillsFromDir(projectSkillsDir, "project", "pi")) {
|
||||
for (const skill of loadSkillsFromDir(projectSkillsDir, "project", "recursive", true)) {
|
||||
skillMap.set(skill.name, skill);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@
|
|||
},
|
||||
{
|
||||
"path": "../../moms"
|
||||
},
|
||||
{
|
||||
"path": "../../.pi/agent"
|
||||
},
|
||||
{
|
||||
"path": "../pi-skills"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue