@mariozechner/pi-coding-agent 0.27.3 → 0.27.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +96 -18
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction.d.ts +7 -0
- package/dist/core/compaction.d.ts.map +1 -1
- package/dist/core/compaction.js +26 -0
- package/dist/core/compaction.js.map +1 -1
- package/dist/core/export-html.d.ts +11 -2
- package/dist/core/export-html.d.ts.map +1 -1
- package/dist/core/export-html.js +551 -94
- package/dist/core/export-html.js.map +1 -1
- package/dist/core/hooks/types.d.ts +20 -2
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +17 -6
- package/dist/core/skills.js.map +1 -1
- package/docs/hooks.md +54 -1
- package/package.json +6 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAqB5D,MAAM,WAAW,gBAAgB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,KAAK;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB;AAwGD,MAAM,WAAW,wBAAwB;IACxC,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,gBAAgB,CAGrF;AAmHD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAuB7D;AAWD,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACxD,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA+E5E","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.js\";\nimport type { SkillsSettings } from \"./settings-manager.js\";\n\n/**\n * Standard frontmatter fields per Agent Skills spec.\n * See: https://agentskills.io/specification#frontmatter-required\n */\nconst ALLOWED_FRONTMATTER_FIELDS = new Set([\n\t\"name\",\n\t\"description\",\n\t\"license\",\n\t\"compatibility\",\n\t\"metadata\",\n\t\"allowed-tools\",\n]);\n\n/** Max name length per spec */\nconst MAX_NAME_LENGTH = 64;\n\n/** Max description length per spec */\nconst MAX_DESCRIPTION_LENGTH = 1024;\n\nexport interface SkillFrontmatter {\n\tname?: string;\n\tdescription?: string;\n\t[key: string]: unknown;\n}\n\nexport interface Skill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsource: string;\n}\n\nexport interface SkillWarning {\n\tskillPath: string;\n\tmessage: string;\n}\n\nexport interface LoadSkillsResult {\n\tskills: Skill[];\n\twarnings: SkillWarning[];\n}\n\ntype SkillFormat = \"recursive\" | \"claude\";\n\nfunction stripQuotes(value: string): string {\n\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; body: string; allKeys: string[] } {\n\tconst frontmatter: SkillFrontmatter = {};\n\tconst allKeys: string[] = [];\n\n\tconst normalizedContent = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\tif (!normalizedContent.startsWith(\"---\")) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst endIndex = normalizedContent.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst frontmatterBlock = normalizedContent.slice(4, endIndex);\n\tconst body = normalizedContent.slice(endIndex + 4).trim();\n\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w[\\w-]*):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tconst key = match[1];\n\t\t\tconst value = stripQuotes(match[2].trim());\n\t\t\tallKeys.push(key);\n\t\t\tif (key === \"name\") {\n\t\t\t\tfrontmatter.name = value;\n\t\t\t} else if (key === \"description\") {\n\t\t\t\tfrontmatter.description = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { frontmatter, body, allKeys };\n}\n\n/**\n * Validate skill name per Agent Skills spec.\n * Returns array of validation error messages (empty if valid).\n */\nfunction validateName(name: string, parentDirName: string): string[] {\n\tconst errors: string[] = [];\n\n\tif (name !== parentDirName) {\n\t\terrors.push(`name \"${name}\" does not match parent directory \"${parentDirName}\"`);\n\t}\n\n\tif (name.length > MAX_NAME_LENGTH) {\n\t\terrors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n\t}\n\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\terrors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);\n\t}\n\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) {\n\t\terrors.push(`name must not start or end with a hyphen`);\n\t}\n\n\tif (name.includes(\"--\")) {\n\t\terrors.push(`name must not contain consecutive hyphens`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Validate description per Agent Skills spec.\n */\nfunction validateDescription(description: string | undefined): string[] {\n\tconst errors: string[] = [];\n\n\tif (!description || description.trim() === \"\") {\n\t\terrors.push(`description is required`);\n\t} else if (description.length > MAX_DESCRIPTION_LENGTH) {\n\t\terrors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Check for unknown frontmatter fields.\n */\nfunction validateFrontmatterFields(keys: string[]): string[] {\n\tconst errors: string[] = [];\n\tfor (const key of keys) {\n\t\tif (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {\n\t\t\terrors.push(`unknown frontmatter field \"${key}\"`);\n\t\t}\n\t}\n\treturn errors;\n}\n\nexport interface LoadSkillsFromDirOptions {\n\t/** Directory to scan for skills */\n\tdir: string;\n\t/** Source identifier for these skills */\n\tsource: string;\n}\n\n/**\n * Load skills from a directory recursively.\n * Skills are directories containing a SKILL.md file with frontmatter including a description.\n */\nexport function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {\n\tconst { dir, source } = options;\n\treturn loadSkillsFromDirInternal(dir, source, \"recursive\");\n}\n\nfunction loadSkillsFromDirInternal(dir: string, source: string, format: SkillFormat): LoadSkillsResult {\n\tconst skills: Skill[] = [];\n\tconst warnings: SkillWarning[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn { skills, warnings };\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip node_modules to avoid scanning dependencies\n\t\t\tif (entry.name === \"node_modules\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (format === \"recursive\") {\n\t\t\t\t// Recursive format: scan directories, look for SKILL.md files\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\tconst subResult = loadSkillsFromDirInternal(fullPath, source, format);\n\t\t\t\t\tskills.push(...subResult.skills);\n\t\t\t\t\twarnings.push(...subResult.warnings);\n\t\t\t\t} else if (entry.isFile() && entry.name === \"SKILL.md\") {\n\t\t\t\t\tconst result = loadSkillFromFile(fullPath, source);\n\t\t\t\t\tif (result.skill) {\n\t\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t\t}\n\t\t\t\t\twarnings.push(...result.warnings);\n\t\t\t\t}\n\t\t\t} else if (format === \"claude\") {\n\t\t\t\t// Claude format: only one level deep, each directory must contain SKILL.md\n\t\t\t\tif (!entry.isDirectory()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst skillFile = join(fullPath, \"SKILL.md\");\n\t\t\t\tif (!existsSync(skillFile)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst result = loadSkillFromFile(skillFile, source);\n\t\t\t\tif (result.skill) {\n\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t}\n\t\t\t\twarnings.push(...result.warnings);\n\t\t\t}\n\t\t}\n\t} catch {}\n\n\treturn { skills, warnings };\n}\n\nfunction loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; warnings: SkillWarning[] } {\n\tconst warnings: SkillWarning[] = [];\n\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, allKeys } = parseFrontmatter(rawContent);\n\t\tconst skillDir = dirname(filePath);\n\t\tconst parentDirName = basename(skillDir);\n\n\t\t// Validate frontmatter fields\n\t\tconst fieldErrors = validateFrontmatterFields(allKeys);\n\t\tfor (const error of fieldErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Validate description\n\t\tconst descErrors = validateDescription(frontmatter.description);\n\t\tfor (const error of descErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Use name from frontmatter, or fall back to parent directory name\n\t\tconst name = frontmatter.name || parentDirName;\n\n\t\t// Validate name\n\t\tconst nameErrors = validateName(name, parentDirName);\n\t\tfor (const error of nameErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Still load the skill even with warnings (unless description is completely missing)\n\t\tif (!frontmatter.description || frontmatter.description.trim() === \"\") {\n\t\t\treturn { skill: null, warnings };\n\t\t}\n\n\t\treturn {\n\t\t\tskill: {\n\t\t\t\tname,\n\t\t\t\tdescription: frontmatter.description,\n\t\t\t\tfilePath,\n\t\t\t\tbaseDir: skillDir,\n\t\t\t\tsource,\n\t\t\t},\n\t\t\twarnings,\n\t\t};\n\t} catch {\n\t\treturn { skill: null, warnings };\n\t}\n}\n\n/**\n * Format skills for inclusion in a system prompt.\n * Uses XML format per Agent Skills standard.\n * See: https://agentskills.io/integrate-skills\n */\nexport function formatSkillsForPrompt(skills: Skill[]): string {\n\tif (skills.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst lines = [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t];\n\n\tfor (const skill of skills) {\n\t\tlines.push(\" <skill>\");\n\t\tlines.push(` <name>${escapeXml(skill.name)}</name>`);\n\t\tlines.push(` <description>${escapeXml(skill.description)}</description>`);\n\t\tlines.push(` <location>${escapeXml(skill.filePath)}</location>`);\n\t\tlines.push(\" </skill>\");\n\t}\n\n\tlines.push(\"</available_skills>\");\n\n\treturn lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\nexport interface LoadSkillsOptions extends SkillsSettings {\n\t/** Working directory for project-local skills. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global skills. Default: ~/.pi/agent */\n\tagentDir?: string;\n}\n\n/**\n * Load skills from all configured locations.\n * Returns skills and any validation warnings.\n */\nexport function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {\n\tconst {\n\t\tcwd = process.cwd(),\n\t\tagentDir,\n\t\tenableCodexUser = true,\n\t\tenableClaudeUser = true,\n\t\tenableClaudeProject = true,\n\t\tenablePiUser = true,\n\t\tenablePiProject = true,\n\t\tcustomDirectories = [],\n\t\tignoredSkills = [],\n\t\tincludeSkills = [],\n\t} = options;\n\n\t// Resolve agentDir - if not provided, use default from config\n\tconst resolvedAgentDir = agentDir ?? getAgentDir();\n\n\tconst skillMap = new Map<string, Skill>();\n\tconst allWarnings: SkillWarning[] = [];\n\tconst collisionWarnings: SkillWarning[] = [];\n\n\t// Check if skill name matches any of the include patterns\n\tfunction matchesIncludePatterns(name: string): boolean {\n\t\tif (includeSkills.length === 0) return true; // No filter = include all\n\t\treturn includeSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\t// Check if skill name matches any of the ignore patterns\n\tfunction matchesIgnorePatterns(name: string): boolean {\n\t\tif (ignoredSkills.length === 0) return false;\n\t\treturn ignoredSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\tfunction addSkills(result: LoadSkillsResult) {\n\t\tallWarnings.push(...result.warnings);\n\t\tfor (const skill of result.skills) {\n\t\t\t// Apply ignore filter (glob patterns) - takes precedence over include\n\t\t\tif (matchesIgnorePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Apply include filter (glob patterns)\n\t\t\tif (!matchesIncludePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst existing = skillMap.get(skill.name);\n\t\t\tif (existing) {\n\t\t\t\tcollisionWarnings.push({\n\t\t\t\t\tskillPath: skill.filePath,\n\t\t\t\t\tmessage: `name collision: \"${skill.name}\" already loaded from ${existing.filePath}, skipping this one`,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tskillMap.set(skill.name, skill);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (enableCodexUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".codex\", \"skills\"), \"codex-user\", \"recursive\"));\n\t}\n\tif (enableClaudeUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".claude\", \"skills\"), \"claude-user\", \"claude\"));\n\t}\n\tif (enableClaudeProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, \".claude\", \"skills\"), \"claude-project\", \"claude\"));\n\t}\n\tif (enablePiUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, \"skills\"), \"user\", \"recursive\"));\n\t}\n\tif (enablePiProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, \"skills\"), \"project\", \"recursive\"));\n\t}\n\tfor (const customDir of customDirectories) {\n\t\taddSkills(loadSkillsFromDirInternal(customDir.replace(/^~(?=$|[\\\\/])/, homedir()), \"custom\", \"recursive\"));\n\t}\n\n\treturn {\n\t\tskills: Array.from(skillMap.values()),\n\t\twarnings: [...allWarnings, ...collisionWarnings],\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAqB5D,MAAM,WAAW,gBAAgB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,KAAK;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB;AAwGD,MAAM,WAAW,wBAAwB;IACxC,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,gBAAgB,CAGrF;AA6HD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAuB7D;AAWD,MAAM,WAAW,iBAAkB,SAAQ,cAAc;IACxD,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA+E5E","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.js\";\nimport type { SkillsSettings } from \"./settings-manager.js\";\n\n/**\n * Standard frontmatter fields per Agent Skills spec.\n * See: https://agentskills.io/specification#frontmatter-required\n */\nconst ALLOWED_FRONTMATTER_FIELDS = new Set([\n\t\"name\",\n\t\"description\",\n\t\"license\",\n\t\"compatibility\",\n\t\"metadata\",\n\t\"allowed-tools\",\n]);\n\n/** Max name length per spec */\nconst MAX_NAME_LENGTH = 64;\n\n/** Max description length per spec */\nconst MAX_DESCRIPTION_LENGTH = 1024;\n\nexport interface SkillFrontmatter {\n\tname?: string;\n\tdescription?: string;\n\t[key: string]: unknown;\n}\n\nexport interface Skill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsource: string;\n}\n\nexport interface SkillWarning {\n\tskillPath: string;\n\tmessage: string;\n}\n\nexport interface LoadSkillsResult {\n\tskills: Skill[];\n\twarnings: SkillWarning[];\n}\n\ntype SkillFormat = \"recursive\" | \"claude\";\n\nfunction stripQuotes(value: string): string {\n\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; body: string; allKeys: string[] } {\n\tconst frontmatter: SkillFrontmatter = {};\n\tconst allKeys: string[] = [];\n\n\tconst normalizedContent = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\tif (!normalizedContent.startsWith(\"---\")) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst endIndex = normalizedContent.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst frontmatterBlock = normalizedContent.slice(4, endIndex);\n\tconst body = normalizedContent.slice(endIndex + 4).trim();\n\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w[\\w-]*):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tconst key = match[1];\n\t\t\tconst value = stripQuotes(match[2].trim());\n\t\t\tallKeys.push(key);\n\t\t\tif (key === \"name\") {\n\t\t\t\tfrontmatter.name = value;\n\t\t\t} else if (key === \"description\") {\n\t\t\t\tfrontmatter.description = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { frontmatter, body, allKeys };\n}\n\n/**\n * Validate skill name per Agent Skills spec.\n * Returns array of validation error messages (empty if valid).\n */\nfunction validateName(name: string, parentDirName: string): string[] {\n\tconst errors: string[] = [];\n\n\tif (name !== parentDirName) {\n\t\terrors.push(`name \"${name}\" does not match parent directory \"${parentDirName}\"`);\n\t}\n\n\tif (name.length > MAX_NAME_LENGTH) {\n\t\terrors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n\t}\n\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\terrors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);\n\t}\n\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) {\n\t\terrors.push(`name must not start or end with a hyphen`);\n\t}\n\n\tif (name.includes(\"--\")) {\n\t\terrors.push(`name must not contain consecutive hyphens`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Validate description per Agent Skills spec.\n */\nfunction validateDescription(description: string | undefined): string[] {\n\tconst errors: string[] = [];\n\n\tif (!description || description.trim() === \"\") {\n\t\terrors.push(`description is required`);\n\t} else if (description.length > MAX_DESCRIPTION_LENGTH) {\n\t\terrors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Check for unknown frontmatter fields.\n */\nfunction validateFrontmatterFields(keys: string[]): string[] {\n\tconst errors: string[] = [];\n\tfor (const key of keys) {\n\t\tif (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {\n\t\t\terrors.push(`unknown frontmatter field \"${key}\"`);\n\t\t}\n\t}\n\treturn errors;\n}\n\nexport interface LoadSkillsFromDirOptions {\n\t/** Directory to scan for skills */\n\tdir: string;\n\t/** Source identifier for these skills */\n\tsource: string;\n}\n\n/**\n * Load skills from a directory recursively.\n * Skills are directories containing a SKILL.md file with frontmatter including a description.\n */\nexport function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {\n\tconst { dir, source } = options;\n\treturn loadSkillsFromDirInternal(dir, source, \"recursive\");\n}\n\nfunction loadSkillsFromDirInternal(dir: string, source: string, format: SkillFormat): LoadSkillsResult {\n\tconst skills: Skill[] = [];\n\tconst warnings: SkillWarning[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn { skills, warnings };\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip node_modules to avoid scanning dependencies\n\t\t\tif (entry.name === \"node_modules\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\t// For symlinks, check if they point to a directory and follow them\n\t\t\tlet isDirectory = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDirectory = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\t// Broken symlink, skip it\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (format === \"recursive\") {\n\t\t\t\t// Recursive format: scan directories, look for SKILL.md files\n\t\t\t\tif (isDirectory) {\n\t\t\t\t\tconst subResult = loadSkillsFromDirInternal(fullPath, source, format);\n\t\t\t\t\tskills.push(...subResult.skills);\n\t\t\t\t\twarnings.push(...subResult.warnings);\n\t\t\t\t} else if (isFile && entry.name === \"SKILL.md\") {\n\t\t\t\t\tconst result = loadSkillFromFile(fullPath, source);\n\t\t\t\t\tif (result.skill) {\n\t\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t\t}\n\t\t\t\t\twarnings.push(...result.warnings);\n\t\t\t\t}\n\t\t\t} else if (format === \"claude\") {\n\t\t\t\t// Claude format: only one level deep, each directory must contain SKILL.md\n\t\t\t\tif (!isDirectory) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst skillFile = join(fullPath, \"SKILL.md\");\n\t\t\t\tif (!existsSync(skillFile)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst result = loadSkillFromFile(skillFile, source);\n\t\t\t\tif (result.skill) {\n\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t}\n\t\t\t\twarnings.push(...result.warnings);\n\t\t\t}\n\t\t}\n\t} catch {}\n\n\treturn { skills, warnings };\n}\n\nfunction loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; warnings: SkillWarning[] } {\n\tconst warnings: SkillWarning[] = [];\n\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, allKeys } = parseFrontmatter(rawContent);\n\t\tconst skillDir = dirname(filePath);\n\t\tconst parentDirName = basename(skillDir);\n\n\t\t// Validate frontmatter fields\n\t\tconst fieldErrors = validateFrontmatterFields(allKeys);\n\t\tfor (const error of fieldErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Validate description\n\t\tconst descErrors = validateDescription(frontmatter.description);\n\t\tfor (const error of descErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Use name from frontmatter, or fall back to parent directory name\n\t\tconst name = frontmatter.name || parentDirName;\n\n\t\t// Validate name\n\t\tconst nameErrors = validateName(name, parentDirName);\n\t\tfor (const error of nameErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Still load the skill even with warnings (unless description is completely missing)\n\t\tif (!frontmatter.description || frontmatter.description.trim() === \"\") {\n\t\t\treturn { skill: null, warnings };\n\t\t}\n\n\t\treturn {\n\t\t\tskill: {\n\t\t\t\tname,\n\t\t\t\tdescription: frontmatter.description,\n\t\t\t\tfilePath,\n\t\t\t\tbaseDir: skillDir,\n\t\t\t\tsource,\n\t\t\t},\n\t\t\twarnings,\n\t\t};\n\t} catch {\n\t\treturn { skill: null, warnings };\n\t}\n}\n\n/**\n * Format skills for inclusion in a system prompt.\n * Uses XML format per Agent Skills standard.\n * See: https://agentskills.io/integrate-skills\n */\nexport function formatSkillsForPrompt(skills: Skill[]): string {\n\tif (skills.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst lines = [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t];\n\n\tfor (const skill of skills) {\n\t\tlines.push(\" <skill>\");\n\t\tlines.push(` <name>${escapeXml(skill.name)}</name>`);\n\t\tlines.push(` <description>${escapeXml(skill.description)}</description>`);\n\t\tlines.push(` <location>${escapeXml(skill.filePath)}</location>`);\n\t\tlines.push(\" </skill>\");\n\t}\n\n\tlines.push(\"</available_skills>\");\n\n\treturn lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\nexport interface LoadSkillsOptions extends SkillsSettings {\n\t/** Working directory for project-local skills. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global skills. Default: ~/.pi/agent */\n\tagentDir?: string;\n}\n\n/**\n * Load skills from all configured locations.\n * Returns skills and any validation warnings.\n */\nexport function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {\n\tconst {\n\t\tcwd = process.cwd(),\n\t\tagentDir,\n\t\tenableCodexUser = true,\n\t\tenableClaudeUser = true,\n\t\tenableClaudeProject = true,\n\t\tenablePiUser = true,\n\t\tenablePiProject = true,\n\t\tcustomDirectories = [],\n\t\tignoredSkills = [],\n\t\tincludeSkills = [],\n\t} = options;\n\n\t// Resolve agentDir - if not provided, use default from config\n\tconst resolvedAgentDir = agentDir ?? getAgentDir();\n\n\tconst skillMap = new Map<string, Skill>();\n\tconst allWarnings: SkillWarning[] = [];\n\tconst collisionWarnings: SkillWarning[] = [];\n\n\t// Check if skill name matches any of the include patterns\n\tfunction matchesIncludePatterns(name: string): boolean {\n\t\tif (includeSkills.length === 0) return true; // No filter = include all\n\t\treturn includeSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\t// Check if skill name matches any of the ignore patterns\n\tfunction matchesIgnorePatterns(name: string): boolean {\n\t\tif (ignoredSkills.length === 0) return false;\n\t\treturn ignoredSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\tfunction addSkills(result: LoadSkillsResult) {\n\t\tallWarnings.push(...result.warnings);\n\t\tfor (const skill of result.skills) {\n\t\t\t// Apply ignore filter (glob patterns) - takes precedence over include\n\t\t\tif (matchesIgnorePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Apply include filter (glob patterns)\n\t\t\tif (!matchesIncludePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst existing = skillMap.get(skill.name);\n\t\t\tif (existing) {\n\t\t\t\tcollisionWarnings.push({\n\t\t\t\t\tskillPath: skill.filePath,\n\t\t\t\t\tmessage: `name collision: \"${skill.name}\" already loaded from ${existing.filePath}, skipping this one`,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tskillMap.set(skill.name, skill);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (enableCodexUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".codex\", \"skills\"), \"codex-user\", \"recursive\"));\n\t}\n\tif (enableClaudeUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".claude\", \"skills\"), \"claude-user\", \"claude\"));\n\t}\n\tif (enableClaudeProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, \".claude\", \"skills\"), \"claude-project\", \"claude\"));\n\t}\n\tif (enablePiUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, \"skills\"), \"user\", \"recursive\"));\n\t}\n\tif (enablePiProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, \"skills\"), \"project\", \"recursive\"));\n\t}\n\tfor (const customDir of customDirectories) {\n\t\taddSkills(loadSkillsFromDirInternal(customDir.replace(/^~(?=$|[\\\\/])/, homedir()), \"custom\", \"recursive\"));\n\t}\n\n\treturn {\n\t\tskills: Array.from(skillMap.values()),\n\t\twarnings: [...allWarnings, ...collisionWarnings],\n\t};\n}\n"]}
|
package/dist/core/skills.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
2
2
|
import { minimatch } from "minimatch";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { basename, dirname, join, resolve } from "path";
|
|
@@ -126,18 +126,29 @@ function loadSkillsFromDirInternal(dir, source, format) {
|
|
|
126
126
|
if (entry.name === "node_modules") {
|
|
127
127
|
continue;
|
|
128
128
|
}
|
|
129
|
+
const fullPath = join(dir, entry.name);
|
|
130
|
+
// For symlinks, check if they point to a directory and follow them
|
|
131
|
+
let isDirectory = entry.isDirectory();
|
|
132
|
+
let isFile = entry.isFile();
|
|
129
133
|
if (entry.isSymbolicLink()) {
|
|
130
|
-
|
|
134
|
+
try {
|
|
135
|
+
const stats = statSync(fullPath);
|
|
136
|
+
isDirectory = stats.isDirectory();
|
|
137
|
+
isFile = stats.isFile();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Broken symlink, skip it
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
131
143
|
}
|
|
132
|
-
const fullPath = join(dir, entry.name);
|
|
133
144
|
if (format === "recursive") {
|
|
134
145
|
// Recursive format: scan directories, look for SKILL.md files
|
|
135
|
-
if (
|
|
146
|
+
if (isDirectory) {
|
|
136
147
|
const subResult = loadSkillsFromDirInternal(fullPath, source, format);
|
|
137
148
|
skills.push(...subResult.skills);
|
|
138
149
|
warnings.push(...subResult.warnings);
|
|
139
150
|
}
|
|
140
|
-
else if (
|
|
151
|
+
else if (isFile && entry.name === "SKILL.md") {
|
|
141
152
|
const result = loadSkillFromFile(fullPath, source);
|
|
142
153
|
if (result.skill) {
|
|
143
154
|
skills.push(result.skill);
|
|
@@ -147,7 +158,7 @@ function loadSkillsFromDirInternal(dir, source, format) {
|
|
|
147
158
|
}
|
|
148
159
|
else if (format === "claude") {
|
|
149
160
|
// Claude format: only one level deep, each directory must contain SKILL.md
|
|
150
|
-
if (!
|
|
161
|
+
if (!isDirectory) {
|
|
151
162
|
continue;
|
|
152
163
|
}
|
|
153
164
|
const skillFile = join(fullPath, "SKILL.md");
|
package/dist/core/skills.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.js\";\nimport type { SkillsSettings } from \"./settings-manager.js\";\n\n/**\n * Standard frontmatter fields per Agent Skills spec.\n * See: https://agentskills.io/specification#frontmatter-required\n */\nconst ALLOWED_FRONTMATTER_FIELDS = new Set([\n\t\"name\",\n\t\"description\",\n\t\"license\",\n\t\"compatibility\",\n\t\"metadata\",\n\t\"allowed-tools\",\n]);\n\n/** Max name length per spec */\nconst MAX_NAME_LENGTH = 64;\n\n/** Max description length per spec */\nconst MAX_DESCRIPTION_LENGTH = 1024;\n\nexport interface SkillFrontmatter {\n\tname?: string;\n\tdescription?: string;\n\t[key: string]: unknown;\n}\n\nexport interface Skill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsource: string;\n}\n\nexport interface SkillWarning {\n\tskillPath: string;\n\tmessage: string;\n}\n\nexport interface LoadSkillsResult {\n\tskills: Skill[];\n\twarnings: SkillWarning[];\n}\n\ntype SkillFormat = \"recursive\" | \"claude\";\n\nfunction stripQuotes(value: string): string {\n\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; body: string; allKeys: string[] } {\n\tconst frontmatter: SkillFrontmatter = {};\n\tconst allKeys: string[] = [];\n\n\tconst normalizedContent = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\tif (!normalizedContent.startsWith(\"---\")) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst endIndex = normalizedContent.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst frontmatterBlock = normalizedContent.slice(4, endIndex);\n\tconst body = normalizedContent.slice(endIndex + 4).trim();\n\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w[\\w-]*):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tconst key = match[1];\n\t\t\tconst value = stripQuotes(match[2].trim());\n\t\t\tallKeys.push(key);\n\t\t\tif (key === \"name\") {\n\t\t\t\tfrontmatter.name = value;\n\t\t\t} else if (key === \"description\") {\n\t\t\t\tfrontmatter.description = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { frontmatter, body, allKeys };\n}\n\n/**\n * Validate skill name per Agent Skills spec.\n * Returns array of validation error messages (empty if valid).\n */\nfunction validateName(name: string, parentDirName: string): string[] {\n\tconst errors: string[] = [];\n\n\tif (name !== parentDirName) {\n\t\terrors.push(`name \"${name}\" does not match parent directory \"${parentDirName}\"`);\n\t}\n\n\tif (name.length > MAX_NAME_LENGTH) {\n\t\terrors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n\t}\n\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\terrors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);\n\t}\n\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) {\n\t\terrors.push(`name must not start or end with a hyphen`);\n\t}\n\n\tif (name.includes(\"--\")) {\n\t\terrors.push(`name must not contain consecutive hyphens`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Validate description per Agent Skills spec.\n */\nfunction validateDescription(description: string | undefined): string[] {\n\tconst errors: string[] = [];\n\n\tif (!description || description.trim() === \"\") {\n\t\terrors.push(`description is required`);\n\t} else if (description.length > MAX_DESCRIPTION_LENGTH) {\n\t\terrors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Check for unknown frontmatter fields.\n */\nfunction validateFrontmatterFields(keys: string[]): string[] {\n\tconst errors: string[] = [];\n\tfor (const key of keys) {\n\t\tif (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {\n\t\t\terrors.push(`unknown frontmatter field \"${key}\"`);\n\t\t}\n\t}\n\treturn errors;\n}\n\nexport interface LoadSkillsFromDirOptions {\n\t/** Directory to scan for skills */\n\tdir: string;\n\t/** Source identifier for these skills */\n\tsource: string;\n}\n\n/**\n * Load skills from a directory recursively.\n * Skills are directories containing a SKILL.md file with frontmatter including a description.\n */\nexport function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {\n\tconst { dir, source } = options;\n\treturn loadSkillsFromDirInternal(dir, source, \"recursive\");\n}\n\nfunction loadSkillsFromDirInternal(dir: string, source: string, format: SkillFormat): LoadSkillsResult {\n\tconst skills: Skill[] = [];\n\tconst warnings: SkillWarning[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn { skills, warnings };\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip node_modules to avoid scanning dependencies\n\t\t\tif (entry.name === \"node_modules\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tif (format === \"recursive\") {\n\t\t\t\t// Recursive format: scan directories, look for SKILL.md files\n\t\t\t\tif (entry.isDirectory()) {\n\t\t\t\t\tconst subResult = loadSkillsFromDirInternal(fullPath, source, format);\n\t\t\t\t\tskills.push(...subResult.skills);\n\t\t\t\t\twarnings.push(...subResult.warnings);\n\t\t\t\t} else if (entry.isFile() && entry.name === \"SKILL.md\") {\n\t\t\t\t\tconst result = loadSkillFromFile(fullPath, source);\n\t\t\t\t\tif (result.skill) {\n\t\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t\t}\n\t\t\t\t\twarnings.push(...result.warnings);\n\t\t\t\t}\n\t\t\t} else if (format === \"claude\") {\n\t\t\t\t// Claude format: only one level deep, each directory must contain SKILL.md\n\t\t\t\tif (!entry.isDirectory()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst skillFile = join(fullPath, \"SKILL.md\");\n\t\t\t\tif (!existsSync(skillFile)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst result = loadSkillFromFile(skillFile, source);\n\t\t\t\tif (result.skill) {\n\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t}\n\t\t\t\twarnings.push(...result.warnings);\n\t\t\t}\n\t\t}\n\t} catch {}\n\n\treturn { skills, warnings };\n}\n\nfunction loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; warnings: SkillWarning[] } {\n\tconst warnings: SkillWarning[] = [];\n\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, allKeys } = parseFrontmatter(rawContent);\n\t\tconst skillDir = dirname(filePath);\n\t\tconst parentDirName = basename(skillDir);\n\n\t\t// Validate frontmatter fields\n\t\tconst fieldErrors = validateFrontmatterFields(allKeys);\n\t\tfor (const error of fieldErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Validate description\n\t\tconst descErrors = validateDescription(frontmatter.description);\n\t\tfor (const error of descErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Use name from frontmatter, or fall back to parent directory name\n\t\tconst name = frontmatter.name || parentDirName;\n\n\t\t// Validate name\n\t\tconst nameErrors = validateName(name, parentDirName);\n\t\tfor (const error of nameErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Still load the skill even with warnings (unless description is completely missing)\n\t\tif (!frontmatter.description || frontmatter.description.trim() === \"\") {\n\t\t\treturn { skill: null, warnings };\n\t\t}\n\n\t\treturn {\n\t\t\tskill: {\n\t\t\t\tname,\n\t\t\t\tdescription: frontmatter.description,\n\t\t\t\tfilePath,\n\t\t\t\tbaseDir: skillDir,\n\t\t\t\tsource,\n\t\t\t},\n\t\t\twarnings,\n\t\t};\n\t} catch {\n\t\treturn { skill: null, warnings };\n\t}\n}\n\n/**\n * Format skills for inclusion in a system prompt.\n * Uses XML format per Agent Skills standard.\n * See: https://agentskills.io/integrate-skills\n */\nexport function formatSkillsForPrompt(skills: Skill[]): string {\n\tif (skills.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst lines = [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t];\n\n\tfor (const skill of skills) {\n\t\tlines.push(\" <skill>\");\n\t\tlines.push(` <name>${escapeXml(skill.name)}</name>`);\n\t\tlines.push(` <description>${escapeXml(skill.description)}</description>`);\n\t\tlines.push(` <location>${escapeXml(skill.filePath)}</location>`);\n\t\tlines.push(\" </skill>\");\n\t}\n\n\tlines.push(\"</available_skills>\");\n\n\treturn lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\nexport interface LoadSkillsOptions extends SkillsSettings {\n\t/** Working directory for project-local skills. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global skills. Default: ~/.pi/agent */\n\tagentDir?: string;\n}\n\n/**\n * Load skills from all configured locations.\n * Returns skills and any validation warnings.\n */\nexport function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {\n\tconst {\n\t\tcwd = process.cwd(),\n\t\tagentDir,\n\t\tenableCodexUser = true,\n\t\tenableClaudeUser = true,\n\t\tenableClaudeProject = true,\n\t\tenablePiUser = true,\n\t\tenablePiProject = true,\n\t\tcustomDirectories = [],\n\t\tignoredSkills = [],\n\t\tincludeSkills = [],\n\t} = options;\n\n\t// Resolve agentDir - if not provided, use default from config\n\tconst resolvedAgentDir = agentDir ?? getAgentDir();\n\n\tconst skillMap = new Map<string, Skill>();\n\tconst allWarnings: SkillWarning[] = [];\n\tconst collisionWarnings: SkillWarning[] = [];\n\n\t// Check if skill name matches any of the include patterns\n\tfunction matchesIncludePatterns(name: string): boolean {\n\t\tif (includeSkills.length === 0) return true; // No filter = include all\n\t\treturn includeSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\t// Check if skill name matches any of the ignore patterns\n\tfunction matchesIgnorePatterns(name: string): boolean {\n\t\tif (ignoredSkills.length === 0) return false;\n\t\treturn ignoredSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\tfunction addSkills(result: LoadSkillsResult) {\n\t\tallWarnings.push(...result.warnings);\n\t\tfor (const skill of result.skills) {\n\t\t\t// Apply ignore filter (glob patterns) - takes precedence over include\n\t\t\tif (matchesIgnorePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Apply include filter (glob patterns)\n\t\t\tif (!matchesIncludePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst existing = skillMap.get(skill.name);\n\t\t\tif (existing) {\n\t\t\t\tcollisionWarnings.push({\n\t\t\t\t\tskillPath: skill.filePath,\n\t\t\t\t\tmessage: `name collision: \"${skill.name}\" already loaded from ${existing.filePath}, skipping this one`,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tskillMap.set(skill.name, skill);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (enableCodexUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".codex\", \"skills\"), \"codex-user\", \"recursive\"));\n\t}\n\tif (enableClaudeUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".claude\", \"skills\"), \"claude-user\", \"claude\"));\n\t}\n\tif (enableClaudeProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, \".claude\", \"skills\"), \"claude-project\", \"claude\"));\n\t}\n\tif (enablePiUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, \"skills\"), \"user\", \"recursive\"));\n\t}\n\tif (enablePiProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, \"skills\"), \"project\", \"recursive\"));\n\t}\n\tfor (const customDir of customDirectories) {\n\t\taddSkills(loadSkillsFromDirInternal(customDir.replace(/^~(?=$|[\\\\/])/, homedir()), \"custom\", \"recursive\"));\n\t}\n\n\treturn {\n\t\tskills: Array.from(skillMap.values()),\n\t\twarnings: [...allWarnings, ...collisionWarnings],\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.js\";\nimport type { SkillsSettings } from \"./settings-manager.js\";\n\n/**\n * Standard frontmatter fields per Agent Skills spec.\n * See: https://agentskills.io/specification#frontmatter-required\n */\nconst ALLOWED_FRONTMATTER_FIELDS = new Set([\n\t\"name\",\n\t\"description\",\n\t\"license\",\n\t\"compatibility\",\n\t\"metadata\",\n\t\"allowed-tools\",\n]);\n\n/** Max name length per spec */\nconst MAX_NAME_LENGTH = 64;\n\n/** Max description length per spec */\nconst MAX_DESCRIPTION_LENGTH = 1024;\n\nexport interface SkillFrontmatter {\n\tname?: string;\n\tdescription?: string;\n\t[key: string]: unknown;\n}\n\nexport interface Skill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsource: string;\n}\n\nexport interface SkillWarning {\n\tskillPath: string;\n\tmessage: string;\n}\n\nexport interface LoadSkillsResult {\n\tskills: Skill[];\n\twarnings: SkillWarning[];\n}\n\ntype SkillFormat = \"recursive\" | \"claude\";\n\nfunction stripQuotes(value: string): string {\n\tif ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n\t\treturn value.slice(1, -1);\n\t}\n\treturn value;\n}\n\nfunction parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; body: string; allKeys: string[] } {\n\tconst frontmatter: SkillFrontmatter = {};\n\tconst allKeys: string[] = [];\n\n\tconst normalizedContent = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n\n\tif (!normalizedContent.startsWith(\"---\")) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst endIndex = normalizedContent.indexOf(\"\\n---\", 3);\n\tif (endIndex === -1) {\n\t\treturn { frontmatter, body: normalizedContent, allKeys };\n\t}\n\n\tconst frontmatterBlock = normalizedContent.slice(4, endIndex);\n\tconst body = normalizedContent.slice(endIndex + 4).trim();\n\n\tfor (const line of frontmatterBlock.split(\"\\n\")) {\n\t\tconst match = line.match(/^(\\w[\\w-]*):\\s*(.*)$/);\n\t\tif (match) {\n\t\t\tconst key = match[1];\n\t\t\tconst value = stripQuotes(match[2].trim());\n\t\t\tallKeys.push(key);\n\t\t\tif (key === \"name\") {\n\t\t\t\tfrontmatter.name = value;\n\t\t\t} else if (key === \"description\") {\n\t\t\t\tfrontmatter.description = value;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { frontmatter, body, allKeys };\n}\n\n/**\n * Validate skill name per Agent Skills spec.\n * Returns array of validation error messages (empty if valid).\n */\nfunction validateName(name: string, parentDirName: string): string[] {\n\tconst errors: string[] = [];\n\n\tif (name !== parentDirName) {\n\t\terrors.push(`name \"${name}\" does not match parent directory \"${parentDirName}\"`);\n\t}\n\n\tif (name.length > MAX_NAME_LENGTH) {\n\t\terrors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n\t}\n\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\terrors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);\n\t}\n\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) {\n\t\terrors.push(`name must not start or end with a hyphen`);\n\t}\n\n\tif (name.includes(\"--\")) {\n\t\terrors.push(`name must not contain consecutive hyphens`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Validate description per Agent Skills spec.\n */\nfunction validateDescription(description: string | undefined): string[] {\n\tconst errors: string[] = [];\n\n\tif (!description || description.trim() === \"\") {\n\t\terrors.push(`description is required`);\n\t} else if (description.length > MAX_DESCRIPTION_LENGTH) {\n\t\terrors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);\n\t}\n\n\treturn errors;\n}\n\n/**\n * Check for unknown frontmatter fields.\n */\nfunction validateFrontmatterFields(keys: string[]): string[] {\n\tconst errors: string[] = [];\n\tfor (const key of keys) {\n\t\tif (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {\n\t\t\terrors.push(`unknown frontmatter field \"${key}\"`);\n\t\t}\n\t}\n\treturn errors;\n}\n\nexport interface LoadSkillsFromDirOptions {\n\t/** Directory to scan for skills */\n\tdir: string;\n\t/** Source identifier for these skills */\n\tsource: string;\n}\n\n/**\n * Load skills from a directory recursively.\n * Skills are directories containing a SKILL.md file with frontmatter including a description.\n */\nexport function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {\n\tconst { dir, source } = options;\n\treturn loadSkillsFromDirInternal(dir, source, \"recursive\");\n}\n\nfunction loadSkillsFromDirInternal(dir: string, source: string, format: SkillFormat): LoadSkillsResult {\n\tconst skills: Skill[] = [];\n\tconst warnings: SkillWarning[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn { skills, warnings };\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\")) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip node_modules to avoid scanning dependencies\n\t\t\tif (entry.name === \"node_modules\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\t// For symlinks, check if they point to a directory and follow them\n\t\t\tlet isDirectory = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDirectory = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\t// Broken symlink, skip it\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (format === \"recursive\") {\n\t\t\t\t// Recursive format: scan directories, look for SKILL.md files\n\t\t\t\tif (isDirectory) {\n\t\t\t\t\tconst subResult = loadSkillsFromDirInternal(fullPath, source, format);\n\t\t\t\t\tskills.push(...subResult.skills);\n\t\t\t\t\twarnings.push(...subResult.warnings);\n\t\t\t\t} else if (isFile && entry.name === \"SKILL.md\") {\n\t\t\t\t\tconst result = loadSkillFromFile(fullPath, source);\n\t\t\t\t\tif (result.skill) {\n\t\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t\t}\n\t\t\t\t\twarnings.push(...result.warnings);\n\t\t\t\t}\n\t\t\t} else if (format === \"claude\") {\n\t\t\t\t// Claude format: only one level deep, each directory must contain SKILL.md\n\t\t\t\tif (!isDirectory) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst skillFile = join(fullPath, \"SKILL.md\");\n\t\t\t\tif (!existsSync(skillFile)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst result = loadSkillFromFile(skillFile, source);\n\t\t\t\tif (result.skill) {\n\t\t\t\t\tskills.push(result.skill);\n\t\t\t\t}\n\t\t\t\twarnings.push(...result.warnings);\n\t\t\t}\n\t\t}\n\t} catch {}\n\n\treturn { skills, warnings };\n}\n\nfunction loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; warnings: SkillWarning[] } {\n\tconst warnings: SkillWarning[] = [];\n\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter, allKeys } = parseFrontmatter(rawContent);\n\t\tconst skillDir = dirname(filePath);\n\t\tconst parentDirName = basename(skillDir);\n\n\t\t// Validate frontmatter fields\n\t\tconst fieldErrors = validateFrontmatterFields(allKeys);\n\t\tfor (const error of fieldErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Validate description\n\t\tconst descErrors = validateDescription(frontmatter.description);\n\t\tfor (const error of descErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Use name from frontmatter, or fall back to parent directory name\n\t\tconst name = frontmatter.name || parentDirName;\n\n\t\t// Validate name\n\t\tconst nameErrors = validateName(name, parentDirName);\n\t\tfor (const error of nameErrors) {\n\t\t\twarnings.push({ skillPath: filePath, message: error });\n\t\t}\n\n\t\t// Still load the skill even with warnings (unless description is completely missing)\n\t\tif (!frontmatter.description || frontmatter.description.trim() === \"\") {\n\t\t\treturn { skill: null, warnings };\n\t\t}\n\n\t\treturn {\n\t\t\tskill: {\n\t\t\t\tname,\n\t\t\t\tdescription: frontmatter.description,\n\t\t\t\tfilePath,\n\t\t\t\tbaseDir: skillDir,\n\t\t\t\tsource,\n\t\t\t},\n\t\t\twarnings,\n\t\t};\n\t} catch {\n\t\treturn { skill: null, warnings };\n\t}\n}\n\n/**\n * Format skills for inclusion in a system prompt.\n * Uses XML format per Agent Skills standard.\n * See: https://agentskills.io/integrate-skills\n */\nexport function formatSkillsForPrompt(skills: Skill[]): string {\n\tif (skills.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst lines = [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t];\n\n\tfor (const skill of skills) {\n\t\tlines.push(\" <skill>\");\n\t\tlines.push(` <name>${escapeXml(skill.name)}</name>`);\n\t\tlines.push(` <description>${escapeXml(skill.description)}</description>`);\n\t\tlines.push(` <location>${escapeXml(skill.filePath)}</location>`);\n\t\tlines.push(\" </skill>\");\n\t}\n\n\tlines.push(\"</available_skills>\");\n\n\treturn lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&/g, \"&\")\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/\"/g, \""\")\n\t\t.replace(/'/g, \"'\");\n}\n\nexport interface LoadSkillsOptions extends SkillsSettings {\n\t/** Working directory for project-local skills. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global skills. Default: ~/.pi/agent */\n\tagentDir?: string;\n}\n\n/**\n * Load skills from all configured locations.\n * Returns skills and any validation warnings.\n */\nexport function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {\n\tconst {\n\t\tcwd = process.cwd(),\n\t\tagentDir,\n\t\tenableCodexUser = true,\n\t\tenableClaudeUser = true,\n\t\tenableClaudeProject = true,\n\t\tenablePiUser = true,\n\t\tenablePiProject = true,\n\t\tcustomDirectories = [],\n\t\tignoredSkills = [],\n\t\tincludeSkills = [],\n\t} = options;\n\n\t// Resolve agentDir - if not provided, use default from config\n\tconst resolvedAgentDir = agentDir ?? getAgentDir();\n\n\tconst skillMap = new Map<string, Skill>();\n\tconst allWarnings: SkillWarning[] = [];\n\tconst collisionWarnings: SkillWarning[] = [];\n\n\t// Check if skill name matches any of the include patterns\n\tfunction matchesIncludePatterns(name: string): boolean {\n\t\tif (includeSkills.length === 0) return true; // No filter = include all\n\t\treturn includeSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\t// Check if skill name matches any of the ignore patterns\n\tfunction matchesIgnorePatterns(name: string): boolean {\n\t\tif (ignoredSkills.length === 0) return false;\n\t\treturn ignoredSkills.some((pattern) => minimatch(name, pattern));\n\t}\n\n\tfunction addSkills(result: LoadSkillsResult) {\n\t\tallWarnings.push(...result.warnings);\n\t\tfor (const skill of result.skills) {\n\t\t\t// Apply ignore filter (glob patterns) - takes precedence over include\n\t\t\tif (matchesIgnorePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Apply include filter (glob patterns)\n\t\t\tif (!matchesIncludePatterns(skill.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst existing = skillMap.get(skill.name);\n\t\t\tif (existing) {\n\t\t\t\tcollisionWarnings.push({\n\t\t\t\t\tskillPath: skill.filePath,\n\t\t\t\t\tmessage: `name collision: \"${skill.name}\" already loaded from ${existing.filePath}, skipping this one`,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tskillMap.set(skill.name, skill);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (enableCodexUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".codex\", \"skills\"), \"codex-user\", \"recursive\"));\n\t}\n\tif (enableClaudeUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(homedir(), \".claude\", \"skills\"), \"claude-user\", \"claude\"));\n\t}\n\tif (enableClaudeProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, \".claude\", \"skills\"), \"claude-project\", \"claude\"));\n\t}\n\tif (enablePiUser) {\n\t\taddSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, \"skills\"), \"user\", \"recursive\"));\n\t}\n\tif (enablePiProject) {\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, \"skills\"), \"project\", \"recursive\"));\n\t}\n\tfor (const customDir of customDirectories) {\n\t\taddSkills(loadSkillsFromDirInternal(customDir.replace(/^~(?=$|[\\\\/])/, homedir()), \"custom\", \"recursive\"));\n\t}\n\n\treturn {\n\t\tskills: Array.from(skillMap.values()),\n\t\twarnings: [...allWarnings, ...collisionWarnings],\n\t};\n}\n"]}
|
package/docs/hooks.md
CHANGED
|
@@ -130,6 +130,11 @@ user clears session (/clear)
|
|
|
130
130
|
├─► session (reason: "before_clear", can cancel)
|
|
131
131
|
└─► session (reason: "clear", AFTER clear)
|
|
132
132
|
|
|
133
|
+
context compaction (auto or /compact)
|
|
134
|
+
│
|
|
135
|
+
├─► session (reason: "before_compact", can cancel or provide custom summary)
|
|
136
|
+
└─► session (reason: "compact", AFTER compaction)
|
|
137
|
+
|
|
133
138
|
user exits (double Ctrl+C or Ctrl+D)
|
|
134
139
|
│
|
|
135
140
|
└─► session (reason: "shutdown")
|
|
@@ -147,7 +152,7 @@ pi.on("session", async (event, ctx) => {
|
|
|
147
152
|
// event.sessionFile: string | null - current session file (null with --no-session)
|
|
148
153
|
// event.previousSessionFile: string | null - previous session file
|
|
149
154
|
// event.reason: "start" | "before_switch" | "switch" | "before_clear" | "clear" |
|
|
150
|
-
// "before_branch" | "branch" | "shutdown"
|
|
155
|
+
// "before_branch" | "branch" | "before_compact" | "compact" | "shutdown"
|
|
151
156
|
// event.targetTurnIndex: number - only for "before_branch" and "branch"
|
|
152
157
|
|
|
153
158
|
// Cancel a before_* action:
|
|
@@ -168,10 +173,26 @@ pi.on("session", async (event, ctx) => {
|
|
|
168
173
|
- `before_switch` / `switch`: User switched sessions (`/resume`)
|
|
169
174
|
- `before_clear` / `clear`: User cleared the session (`/clear`)
|
|
170
175
|
- `before_branch` / `branch`: User branched the session (`/branch`)
|
|
176
|
+
- `before_compact` / `compact`: Context compaction (auto or `/compact`)
|
|
171
177
|
- `shutdown`: Process is exiting (double Ctrl+C, Ctrl+D, or SIGTERM)
|
|
172
178
|
|
|
173
179
|
For `before_branch` and `branch` events, `event.targetTurnIndex` contains the entry index being branched from.
|
|
174
180
|
|
|
181
|
+
For `before_compact` events, additional fields are available:
|
|
182
|
+
- `event.cutPoint`: Where the context will be cut (`firstKeptEntryIndex`, `isSplitTurn`)
|
|
183
|
+
- `event.messagesToSummarize`: Messages that will be summarized
|
|
184
|
+
- `event.tokensBefore`: Current context token count
|
|
185
|
+
- `event.model`: Model to use for summarization
|
|
186
|
+
- `event.apiKey`: API key for the model
|
|
187
|
+
- `event.customInstructions`: Optional custom focus for summary (from `/compact` command)
|
|
188
|
+
|
|
189
|
+
Return `{ compactionEntry }` to provide a custom summary instead of the default. The `compactionEntry` must have: `type: "compaction"`, `timestamp`, `summary`, `firstKeptEntryIndex` (from `cutPoint`), `tokensBefore`.
|
|
190
|
+
|
|
191
|
+
For `compact` events (after compaction):
|
|
192
|
+
- `event.compactionEntry`: The saved compaction entry
|
|
193
|
+
- `event.tokensBefore`: Token count before compaction
|
|
194
|
+
- `event.fromHook`: Whether the compaction entry was provided by a hook
|
|
195
|
+
|
|
175
196
|
### agent_start / agent_end
|
|
176
197
|
|
|
177
198
|
Fired once per user prompt.
|
|
@@ -603,6 +624,38 @@ export default function (pi: HookAPI) {
|
|
|
603
624
|
}
|
|
604
625
|
```
|
|
605
626
|
|
|
627
|
+
### Custom Compaction
|
|
628
|
+
|
|
629
|
+
Use a cheaper model for summarization, or implement your own compaction strategy.
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks";
|
|
633
|
+
import type { CompactionEntry } from "@mariozechner/pi-coding-agent";
|
|
634
|
+
|
|
635
|
+
export default function (pi: HookAPI) {
|
|
636
|
+
pi.on("session", async (event, ctx) => {
|
|
637
|
+
if (event.reason !== "before_compact") return;
|
|
638
|
+
|
|
639
|
+
// Example: Use a simpler summarization approach
|
|
640
|
+
const messages = event.messagesToSummarize;
|
|
641
|
+
const summary = messages
|
|
642
|
+
.filter((m) => m.role === "user")
|
|
643
|
+
.map((m) => `- ${typeof m.content === "string" ? m.content.slice(0, 100) : "[complex]"}`)
|
|
644
|
+
.join("\n");
|
|
645
|
+
|
|
646
|
+
const compactionEntry: CompactionEntry = {
|
|
647
|
+
type: "compaction",
|
|
648
|
+
timestamp: new Date().toISOString(),
|
|
649
|
+
summary: `User requests:\n${summary}`,
|
|
650
|
+
firstKeptEntryIndex: event.cutPoint.firstKeptEntryIndex,
|
|
651
|
+
tokensBefore: event.tokensBefore,
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
return { compactionEntry };
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
606
659
|
## Mode Behavior
|
|
607
660
|
|
|
608
661
|
Hooks behave differently depending on the run mode:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-coding-agent",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.5",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -39,15 +39,16 @@
|
|
|
39
39
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@mariozechner/pi-agent-core": "^0.27.
|
|
43
|
-
"@mariozechner/pi-ai": "^0.27.
|
|
44
|
-
"@mariozechner/pi-tui": "^0.27.
|
|
42
|
+
"@mariozechner/pi-agent-core": "^0.27.5",
|
|
43
|
+
"@mariozechner/pi-ai": "^0.27.5",
|
|
44
|
+
"@mariozechner/pi-tui": "^0.27.5",
|
|
45
45
|
"chalk": "^5.5.0",
|
|
46
46
|
"cli-highlight": "^2.1.11",
|
|
47
47
|
"diff": "^8.0.2",
|
|
48
48
|
"file-type": "^21.1.1",
|
|
49
49
|
"glob": "^11.0.3",
|
|
50
|
-
"jiti": "^2.6.1"
|
|
50
|
+
"jiti": "^2.6.1",
|
|
51
|
+
"marked": "^15.0.12"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@types/diff": "^7.0.2",
|