@sentry/warden 0.0.0
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/.agents/skills/find-bugs/SKILL.md +75 -0
- package/.agents/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/.agents/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.agents/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.claude/settings.json +57 -0
- package/.claude/settings.local.json +88 -0
- package/.claude/skills/agent-prompt/SKILL.md +54 -0
- package/.claude/skills/agent-prompt/references/agentic-patterns.md +94 -0
- package/.claude/skills/agent-prompt/references/anti-patterns.md +140 -0
- package/.claude/skills/agent-prompt/references/context-design.md +124 -0
- package/.claude/skills/agent-prompt/references/core-principles.md +75 -0
- package/.claude/skills/agent-prompt/references/model-guidance.md +118 -0
- package/.claude/skills/agent-prompt/references/output-formats.md +98 -0
- package/.claude/skills/agent-prompt/references/skill-structure.md +115 -0
- package/.claude/skills/agent-prompt/references/system-prompts.md +115 -0
- package/.claude/skills/notseer/SKILL.md +131 -0
- package/.claude/skills/skill-writer/SKILL.md +140 -0
- package/.claude/skills/testing-guidelines/SKILL.md +132 -0
- package/.claude/skills/warden-skill/SKILL.md +250 -0
- package/.claude/skills/warden-skill/references/config-schema.md +133 -0
- package/.dex/config.toml +2 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +54 -0
- package/.github/workflows/warden.yml +40 -0
- package/AGENTS.md +89 -0
- package/CONTRIBUTING.md +60 -0
- package/LICENSE +105 -0
- package/README.md +43 -0
- package/SPEC.md +263 -0
- package/action.yml +87 -0
- package/assets/favicon.png +0 -0
- package/assets/warden-icon-bw.svg +5 -0
- package/assets/warden-icon-purple.png +0 -0
- package/assets/warden-icon-purple.svg +5 -0
- package/docs/.claude/settings.local.json +11 -0
- package/docs/astro.config.mjs +43 -0
- package/docs/package.json +19 -0
- package/docs/pnpm-lock.yaml +4000 -0
- package/docs/public/favicon.svg +5 -0
- package/docs/src/components/Code.astro +141 -0
- package/docs/src/components/PackageManagerTabs.astro +183 -0
- package/docs/src/components/Terminal.astro +212 -0
- package/docs/src/layouts/Base.astro +380 -0
- package/docs/src/pages/cli.astro +167 -0
- package/docs/src/pages/config.astro +394 -0
- package/docs/src/pages/guide.astro +449 -0
- package/docs/src/pages/index.astro +490 -0
- package/docs/src/styles/global.css +551 -0
- package/docs/tsconfig.json +3 -0
- package/docs/vercel.json +5 -0
- package/eslint.config.js +33 -0
- package/package.json +73 -0
- package/src/action/index.ts +1 -0
- package/src/action/main.ts +868 -0
- package/src/cli/args.test.ts +477 -0
- package/src/cli/args.ts +415 -0
- package/src/cli/commands/add.ts +447 -0
- package/src/cli/commands/init.test.ts +136 -0
- package/src/cli/commands/init.ts +132 -0
- package/src/cli/commands/setup-app/browser.ts +38 -0
- package/src/cli/commands/setup-app/credentials.ts +45 -0
- package/src/cli/commands/setup-app/manifest.ts +48 -0
- package/src/cli/commands/setup-app/server.ts +172 -0
- package/src/cli/commands/setup-app.ts +156 -0
- package/src/cli/commands/sync.ts +114 -0
- package/src/cli/context.ts +131 -0
- package/src/cli/files.test.ts +155 -0
- package/src/cli/files.ts +89 -0
- package/src/cli/fix.test.ts +310 -0
- package/src/cli/fix.ts +387 -0
- package/src/cli/git.test.ts +119 -0
- package/src/cli/git.ts +318 -0
- package/src/cli/index.ts +14 -0
- package/src/cli/main.ts +672 -0
- package/src/cli/output/box.ts +235 -0
- package/src/cli/output/formatters.test.ts +187 -0
- package/src/cli/output/formatters.ts +269 -0
- package/src/cli/output/icons.ts +13 -0
- package/src/cli/output/index.ts +44 -0
- package/src/cli/output/ink-runner.tsx +337 -0
- package/src/cli/output/jsonl.test.ts +347 -0
- package/src/cli/output/jsonl.ts +126 -0
- package/src/cli/output/reporter.ts +435 -0
- package/src/cli/output/tasks.ts +374 -0
- package/src/cli/output/tty.test.ts +117 -0
- package/src/cli/output/tty.ts +60 -0
- package/src/cli/output/verbosity.test.ts +40 -0
- package/src/cli/output/verbosity.ts +31 -0
- package/src/cli/terminal.test.ts +148 -0
- package/src/cli/terminal.ts +301 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.test.ts +313 -0
- package/src/config/loader.ts +103 -0
- package/src/config/schema.ts +168 -0
- package/src/config/writer.test.ts +119 -0
- package/src/config/writer.ts +84 -0
- package/src/diff/classify.test.ts +162 -0
- package/src/diff/classify.ts +92 -0
- package/src/diff/coalesce.test.ts +208 -0
- package/src/diff/coalesce.ts +133 -0
- package/src/diff/context.test.ts +226 -0
- package/src/diff/context.ts +201 -0
- package/src/diff/index.ts +4 -0
- package/src/diff/parser.test.ts +212 -0
- package/src/diff/parser.ts +149 -0
- package/src/event/context.ts +132 -0
- package/src/event/index.ts +2 -0
- package/src/event/schedule-context.ts +101 -0
- package/src/examples/examples.integration.test.ts +66 -0
- package/src/examples/index.test.ts +101 -0
- package/src/examples/index.ts +122 -0
- package/src/examples/setup.ts +25 -0
- package/src/index.ts +115 -0
- package/src/output/dedup.test.ts +419 -0
- package/src/output/dedup.ts +607 -0
- package/src/output/github-checks.test.ts +300 -0
- package/src/output/github-checks.ts +476 -0
- package/src/output/github-issues.ts +329 -0
- package/src/output/index.ts +5 -0
- package/src/output/issue-renderer.ts +197 -0
- package/src/output/renderer.test.ts +727 -0
- package/src/output/renderer.ts +217 -0
- package/src/output/stale.test.ts +375 -0
- package/src/output/stale.ts +155 -0
- package/src/output/types.ts +34 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/runner.test.ts +806 -0
- package/src/sdk/runner.ts +1232 -0
- package/src/skills/index.ts +36 -0
- package/src/skills/loader.test.ts +300 -0
- package/src/skills/loader.ts +423 -0
- package/src/skills/remote.test.ts +704 -0
- package/src/skills/remote.ts +604 -0
- package/src/triggers/matcher.test.ts +277 -0
- package/src/triggers/matcher.ts +152 -0
- package/src/types/index.ts +194 -0
- package/src/utils/async.ts +18 -0
- package/src/utils/index.test.ts +84 -0
- package/src/utils/index.ts +50 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +11 -0
- package/warden.toml +19 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import type { SkillDefinition, ToolName } from '../config/schema.js';
|
|
6
|
+
import { ToolNameSchema } from '../config/schema.js';
|
|
7
|
+
|
|
8
|
+
export class SkillLoaderError extends Error {
|
|
9
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = 'SkillLoaderError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A loaded skill with its source entry path.
|
|
17
|
+
*/
|
|
18
|
+
export interface LoadedSkill {
|
|
19
|
+
skill: SkillDefinition;
|
|
20
|
+
/** The entry name (file or directory) where the skill was found */
|
|
21
|
+
entry: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Cache for loaded skills directories to avoid repeated disk reads */
|
|
25
|
+
const skillsCache = new Map<string, Map<string, LoadedSkill>>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Conventional skill directories, checked in priority order.
|
|
29
|
+
*
|
|
30
|
+
* Skills are discovered from these directories in order:
|
|
31
|
+
* 1. .warden/skills - Warden-specific skills (highest priority)
|
|
32
|
+
* 2. .agents/skills - General agent skills (shared across tools)
|
|
33
|
+
* 3. .claude/skills - Claude Code skills (for compatibility)
|
|
34
|
+
*
|
|
35
|
+
* Skills follow the agentskills.io specification:
|
|
36
|
+
* - skill-name/SKILL.md (directory with SKILL.md inside - preferred)
|
|
37
|
+
* - skill-name.md (flat markdown with SKILL.md frontmatter format)
|
|
38
|
+
*
|
|
39
|
+
* When a skill name exists in multiple directories, the first one found wins.
|
|
40
|
+
* This allows project-specific skills in .warden/skills to override shared skills.
|
|
41
|
+
*/
|
|
42
|
+
export const SKILL_DIRECTORIES = [
|
|
43
|
+
'.warden/skills',
|
|
44
|
+
'.agents/skills',
|
|
45
|
+
'.claude/skills',
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a string looks like a path (contains path separators or starts with .)
|
|
50
|
+
*/
|
|
51
|
+
function isSkillPath(nameOrPath: string): boolean {
|
|
52
|
+
return nameOrPath.includes('/') || nameOrPath.includes('\\') || nameOrPath.startsWith('.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a skill path, handling absolute paths, tilde expansion, and relative paths.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveSkillPath(nameOrPath: string, repoRoot?: string): string {
|
|
59
|
+
// Expand ~ to home directory
|
|
60
|
+
if (nameOrPath.startsWith('~/')) {
|
|
61
|
+
return join(homedir(), nameOrPath.slice(2));
|
|
62
|
+
}
|
|
63
|
+
if (nameOrPath === '~') {
|
|
64
|
+
return homedir();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Absolute path - use as-is
|
|
68
|
+
if (isAbsolute(nameOrPath)) {
|
|
69
|
+
return nameOrPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Relative path - join with repoRoot if available
|
|
73
|
+
return repoRoot ? join(repoRoot, nameOrPath) : nameOrPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Clear the skills cache. Useful for testing or when skills may have changed.
|
|
78
|
+
*/
|
|
79
|
+
export function clearSkillsCache(): void {
|
|
80
|
+
skillsCache.clear();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse YAML frontmatter from a markdown file.
|
|
85
|
+
* Returns the frontmatter object and the body content.
|
|
86
|
+
*/
|
|
87
|
+
function parseMarkdownFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
|
|
88
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
89
|
+
if (!match) {
|
|
90
|
+
throw new SkillLoaderError('Invalid SKILL.md: missing YAML frontmatter');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const [, yamlContent, body] = match;
|
|
94
|
+
|
|
95
|
+
// Simple YAML parser for frontmatter (handles basic key: value pairs)
|
|
96
|
+
const frontmatter: Record<string, unknown> = {};
|
|
97
|
+
let currentKey: string | null = null;
|
|
98
|
+
let inMetadata = false;
|
|
99
|
+
const metadata: Record<string, string> = {};
|
|
100
|
+
|
|
101
|
+
for (const line of (yamlContent ?? '').split('\n')) {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
104
|
+
|
|
105
|
+
if (line.startsWith(' ') && inMetadata) {
|
|
106
|
+
// Nested metadata value
|
|
107
|
+
const metaMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
108
|
+
if (metaMatch && metaMatch[1]) {
|
|
109
|
+
metadata[metaMatch[1]] = metaMatch[2]?.replace(/^["']|["']$/g, '') ?? '';
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
inMetadata = false;
|
|
115
|
+
const keyMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
116
|
+
if (keyMatch && keyMatch[1]) {
|
|
117
|
+
currentKey = keyMatch[1];
|
|
118
|
+
const value = (keyMatch[2] ?? '').trim();
|
|
119
|
+
|
|
120
|
+
if (currentKey === 'metadata' && !value) {
|
|
121
|
+
inMetadata = true;
|
|
122
|
+
frontmatter[currentKey] = metadata;
|
|
123
|
+
} else if (value) {
|
|
124
|
+
frontmatter[currentKey] = value.replace(/^["']|["']$/g, '');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { frontmatter, body: body ?? '' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get valid tool name suggestions for error messages.
|
|
134
|
+
*/
|
|
135
|
+
function getValidToolNames(): string {
|
|
136
|
+
return ToolNameSchema.options.join(', ');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse allowed-tools from agentskills.io format to our format.
|
|
141
|
+
* agentskills.io uses space-delimited: "Read Grep Glob"
|
|
142
|
+
* We use array: ["Read", "Grep", "Glob"]
|
|
143
|
+
*/
|
|
144
|
+
function parseAllowedTools(
|
|
145
|
+
allowedTools: unknown,
|
|
146
|
+
onWarning?: (message: string) => void
|
|
147
|
+
): ToolName[] | undefined {
|
|
148
|
+
if (typeof allowedTools !== 'string') {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const tools = allowedTools.split(/\s+/).filter(Boolean);
|
|
153
|
+
const validTools: ToolName[] = [];
|
|
154
|
+
|
|
155
|
+
for (const tool of tools) {
|
|
156
|
+
const result = ToolNameSchema.safeParse(tool);
|
|
157
|
+
if (result.success) {
|
|
158
|
+
validTools.push(result.data);
|
|
159
|
+
} else {
|
|
160
|
+
onWarning?.(
|
|
161
|
+
`Invalid tool name '${tool}' in allowed-tools (ignored). Valid tools: ${getValidToolNames()}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return validTools.length > 0 ? validTools : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Options for loading a skill from markdown.
|
|
171
|
+
*/
|
|
172
|
+
export interface LoadSkillFromMarkdownOptions {
|
|
173
|
+
/** Callback for reporting warnings (e.g., invalid tool names) */
|
|
174
|
+
onWarning?: (message: string) => void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Load a skill from a SKILL.md file (agentskills.io format).
|
|
179
|
+
*/
|
|
180
|
+
export async function loadSkillFromMarkdown(
|
|
181
|
+
filePath: string,
|
|
182
|
+
options?: LoadSkillFromMarkdownOptions
|
|
183
|
+
): Promise<SkillDefinition> {
|
|
184
|
+
let content: string;
|
|
185
|
+
try {
|
|
186
|
+
content = await readFile(filePath, 'utf-8');
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new SkillLoaderError(`Failed to read skill file: ${filePath}`, { cause: error });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const { frontmatter, body } = parseMarkdownFrontmatter(content);
|
|
192
|
+
|
|
193
|
+
if (!frontmatter['name'] || typeof frontmatter['name'] !== 'string') {
|
|
194
|
+
throw new SkillLoaderError(`Invalid SKILL.md: missing 'name' in frontmatter`);
|
|
195
|
+
}
|
|
196
|
+
if (!frontmatter['description'] || typeof frontmatter['description'] !== 'string') {
|
|
197
|
+
throw new SkillLoaderError(`Invalid SKILL.md: missing 'description' in frontmatter`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const allowedTools = parseAllowedTools(frontmatter['allowed-tools'], options?.onWarning);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
name: frontmatter['name'],
|
|
204
|
+
description: frontmatter['description'],
|
|
205
|
+
prompt: body.trim(),
|
|
206
|
+
tools: allowedTools ? { allowed: allowedTools } : undefined,
|
|
207
|
+
rootDir: dirname(filePath),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Load a skill from a file (agentskills.io format .md files).
|
|
213
|
+
*/
|
|
214
|
+
export async function loadSkillFromFile(filePath: string): Promise<SkillDefinition> {
|
|
215
|
+
const ext = extname(filePath).toLowerCase();
|
|
216
|
+
|
|
217
|
+
if (ext === '.md') {
|
|
218
|
+
return loadSkillFromMarkdown(filePath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw new SkillLoaderError(`Unsupported skill file: ${filePath}. Skills must be .md files following the agentskills.io format.`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Options for loading skills from a directory.
|
|
226
|
+
*/
|
|
227
|
+
export interface LoadSkillsOptions {
|
|
228
|
+
/** Callback for reporting warnings (e.g., failed skill loading) */
|
|
229
|
+
onWarning?: (message: string) => void;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Load all skills from a directory.
|
|
234
|
+
*
|
|
235
|
+
* Supports the agentskills.io specification:
|
|
236
|
+
* - skill-name/SKILL.md (directory with SKILL.md inside - preferred)
|
|
237
|
+
* - skill-name.md (flat markdown with SKILL.md frontmatter format)
|
|
238
|
+
*
|
|
239
|
+
* Results are cached to avoid repeated disk reads.
|
|
240
|
+
*
|
|
241
|
+
* @returns Map of skill name to LoadedSkill (includes entry path for tracking)
|
|
242
|
+
*/
|
|
243
|
+
export async function loadSkillsFromDirectory(
|
|
244
|
+
dirPath: string,
|
|
245
|
+
options?: LoadSkillsOptions
|
|
246
|
+
): Promise<Map<string, LoadedSkill>> {
|
|
247
|
+
// Check cache first
|
|
248
|
+
const cached = skillsCache.get(dirPath);
|
|
249
|
+
if (cached) {
|
|
250
|
+
return cached;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const skills = new Map<string, LoadedSkill>();
|
|
254
|
+
|
|
255
|
+
let entries: string[];
|
|
256
|
+
try {
|
|
257
|
+
entries = await readdir(dirPath);
|
|
258
|
+
} catch {
|
|
259
|
+
skillsCache.set(dirPath, skills);
|
|
260
|
+
return skills;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Process entries following agentskills.io format priority:
|
|
264
|
+
// 1. Directories with SKILL.md (preferred)
|
|
265
|
+
// 2. Flat .md files with valid SKILL.md frontmatter
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
const entryPath = join(dirPath, entry);
|
|
268
|
+
|
|
269
|
+
// Check for agentskills.io format: skill-name/SKILL.md (preferred)
|
|
270
|
+
const skillMdPath = join(entryPath, 'SKILL.md');
|
|
271
|
+
if (existsSync(skillMdPath)) {
|
|
272
|
+
try {
|
|
273
|
+
const skill = await loadSkillFromMarkdown(skillMdPath, { onWarning: options?.onWarning });
|
|
274
|
+
skills.set(skill.name, { skill, entry });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
options?.onWarning?.(`Failed to load skill from ${skillMdPath}: ${message}`);
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check for flat .md files (with SKILL.md format frontmatter)
|
|
283
|
+
if (entry.endsWith('.md')) {
|
|
284
|
+
try {
|
|
285
|
+
const skill = await loadSkillFromMarkdown(entryPath, { onWarning: options?.onWarning });
|
|
286
|
+
skills.set(skill.name, { skill, entry });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// Skip files without YAML frontmatter (e.g., README.md, documentation)
|
|
289
|
+
// But warn about files that have frontmatter but are malformed
|
|
290
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
291
|
+
if (!message.includes('missing YAML frontmatter')) {
|
|
292
|
+
options?.onWarning?.(`Failed to load skill from ${entry}: ${message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
skillsCache.set(dirPath, skills);
|
|
299
|
+
return skills;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A discovered skill with source metadata.
|
|
304
|
+
*/
|
|
305
|
+
export interface DiscoveredSkill {
|
|
306
|
+
skill: SkillDefinition;
|
|
307
|
+
/** Relative directory path where the skill was found (e.g., "./.agents/skills") */
|
|
308
|
+
directory: string;
|
|
309
|
+
/** Full path to the skill */
|
|
310
|
+
path: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Discover all available skills from conventional directories.
|
|
315
|
+
*
|
|
316
|
+
* @param repoRoot - Repository root path for finding skills
|
|
317
|
+
* @param options - Options for skill loading (e.g., warning callback)
|
|
318
|
+
* @returns Map of skill name to discovered skill info
|
|
319
|
+
*/
|
|
320
|
+
export async function discoverAllSkills(
|
|
321
|
+
repoRoot?: string,
|
|
322
|
+
options?: LoadSkillsOptions
|
|
323
|
+
): Promise<Map<string, DiscoveredSkill>> {
|
|
324
|
+
const result = new Map<string, DiscoveredSkill>();
|
|
325
|
+
|
|
326
|
+
if (!repoRoot) {
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Scan conventional directories for skills
|
|
331
|
+
for (const dir of SKILL_DIRECTORIES) {
|
|
332
|
+
const dirPath = join(repoRoot, dir);
|
|
333
|
+
if (!existsSync(dirPath)) continue;
|
|
334
|
+
|
|
335
|
+
const skills = await loadSkillsFromDirectory(dirPath, options);
|
|
336
|
+
for (const [name, loaded] of skills) {
|
|
337
|
+
// First directory wins - don't overwrite existing skills
|
|
338
|
+
if (!result.has(name)) {
|
|
339
|
+
result.set(name, {
|
|
340
|
+
skill: loaded.skill,
|
|
341
|
+
directory: `./${dir}`,
|
|
342
|
+
path: join(dirPath, loaded.entry),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export interface ResolveSkillOptions {
|
|
352
|
+
/** Remote repository reference (e.g., "owner/repo" or "owner/repo@sha") */
|
|
353
|
+
remote?: string;
|
|
354
|
+
/** Skip network operations - only use cache for remote skills */
|
|
355
|
+
offline?: boolean;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Resolve a skill by name or path.
|
|
360
|
+
*
|
|
361
|
+
* Resolution order:
|
|
362
|
+
* 1. Remote repository (if remote option is set)
|
|
363
|
+
* 2. Direct path (if nameOrPath contains / or \ or starts with .)
|
|
364
|
+
* - Directory: load SKILL.md from it
|
|
365
|
+
* - File: load the .md file directly
|
|
366
|
+
* 3. Conventional directories (if repoRoot provided)
|
|
367
|
+
* - .warden/skills/{name}/SKILL.md or .warden/skills/{name}.md
|
|
368
|
+
* - .agents/skills/{name}/SKILL.md or .agents/skills/{name}.md
|
|
369
|
+
* - .claude/skills/{name}/SKILL.md or .claude/skills/{name}.md
|
|
370
|
+
*/
|
|
371
|
+
export async function resolveSkillAsync(
|
|
372
|
+
nameOrPath: string,
|
|
373
|
+
repoRoot?: string,
|
|
374
|
+
options?: ResolveSkillOptions
|
|
375
|
+
): Promise<SkillDefinition> {
|
|
376
|
+
const { remote, offline } = options ?? {};
|
|
377
|
+
|
|
378
|
+
// 1. Remote repository resolution takes priority when specified
|
|
379
|
+
if (remote) {
|
|
380
|
+
// Dynamic import to avoid circular dependencies
|
|
381
|
+
const { resolveRemoteSkill } = await import('./remote.js');
|
|
382
|
+
return resolveRemoteSkill(remote, nameOrPath, { offline });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 2. Direct path resolution
|
|
386
|
+
if (isSkillPath(nameOrPath)) {
|
|
387
|
+
const resolvedPath = resolveSkillPath(nameOrPath, repoRoot);
|
|
388
|
+
|
|
389
|
+
// Check if it's a directory with SKILL.md
|
|
390
|
+
const skillMdPath = join(resolvedPath, 'SKILL.md');
|
|
391
|
+
if (existsSync(skillMdPath)) {
|
|
392
|
+
return loadSkillFromMarkdown(skillMdPath);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check if it's a file directly
|
|
396
|
+
if (existsSync(resolvedPath)) {
|
|
397
|
+
return loadSkillFromFile(resolvedPath);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
throw new SkillLoaderError(`Skill not found at path: ${nameOrPath}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 3. Check conventional skill directories
|
|
404
|
+
if (repoRoot) {
|
|
405
|
+
for (const dir of SKILL_DIRECTORIES) {
|
|
406
|
+
const dirPath = join(repoRoot, dir);
|
|
407
|
+
|
|
408
|
+
// Check for skill-name/SKILL.md (preferred agentskills.io format)
|
|
409
|
+
const skillMdPath = join(dirPath, nameOrPath, 'SKILL.md');
|
|
410
|
+
if (existsSync(skillMdPath)) {
|
|
411
|
+
return loadSkillFromMarkdown(skillMdPath);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check for skill-name.md (flat markdown file with SKILL.md format)
|
|
415
|
+
const mdPath = join(dirPath, `${nameOrPath}.md`);
|
|
416
|
+
if (existsSync(mdPath)) {
|
|
417
|
+
return loadSkillFromMarkdown(mdPath);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
throw new SkillLoaderError(`Skill not found: ${nameOrPath}`);
|
|
423
|
+
}
|