@sweny-ai/core 0.1.86 → 0.1.88
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/dist/cli/config-file.d.ts +1 -1
- package/dist/cli/config-file.js +1 -1
- package/dist/cli/main.js +15 -7
- package/dist/cli/skill.d.ts +27 -0
- package/dist/cli/skill.js +200 -0
- package/dist/skills/custom-loader.js +5 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -12,4 +12,4 @@ export type FileConfig = Record<string, string | string[] | Record<string, unkno
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function loadConfigFile(cwd?: string): FileConfig;
|
|
14
14
|
/** Starter config written by `sweny new`. */
|
|
15
|
-
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex | gemini\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Cloud reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in: paste your project token from cloud.sweny.ai to enable run reporting.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See docs/mcp-servers
|
|
15
|
+
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex | gemini\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 Cloud reporting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Opt-in: paste your project token from cloud.sweny.ai to enable run reporting.\n# cloud-token: sweny_pk_...\n# Or set SWENY_CLOUD_TOKEN in your environment.\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";
|
package/dist/cli/config-file.js
CHANGED
|
@@ -139,7 +139,7 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
|
139
139
|
# ── MCP servers ───────────────────────────────────────────────────────
|
|
140
140
|
# Extend the coding agent with additional tools via MCP.
|
|
141
141
|
# Value is a JSON object — each key is a server name you choose.
|
|
142
|
-
# See docs/mcp-servers
|
|
142
|
+
# See https://docs.sweny.ai/advanced/mcp-servers/ for a full catalog with copy-paste configs.
|
|
143
143
|
#
|
|
144
144
|
# Example: GitHub MCP server (query PRs, issues, CI run logs)
|
|
145
145
|
# mcp-servers-json: '{"github":{"type":"stdio","command":"npx","args":["-y","@modelcontextprotocol/server-github@latest"],"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"ghp_..."}}}'
|
package/dist/cli/main.js
CHANGED
|
@@ -30,6 +30,7 @@ import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman,
|
|
|
30
30
|
import { checkProviderConnectivity } from "./check.js";
|
|
31
31
|
import { registerSetupCommand } from "./setup.js";
|
|
32
32
|
import { registerPublishCommand } from "./publish.js";
|
|
33
|
+
import { registerSkillCommand } from "./skill.js";
|
|
33
34
|
import { reportToCloud } from "./cloud-report.js";
|
|
34
35
|
import { runUpgrade, fetchLatestFromNpm } from "./upgrade.js";
|
|
35
36
|
import { maybeNudge, defaultCachePath } from "./version-check.js";
|
|
@@ -101,6 +102,7 @@ program
|
|
|
101
102
|
});
|
|
102
103
|
registerSetupCommand(program);
|
|
103
104
|
registerPublishCommand(program);
|
|
105
|
+
registerSkillCommand(program);
|
|
104
106
|
// ── Credential map builder ──────────────────────────────────────────
|
|
105
107
|
// buildCredentialMap lives in ./credentials.ts so tests can import it
|
|
106
108
|
// without triggering main.ts's top-level program.parse() side effect.
|
|
@@ -601,15 +603,21 @@ export async function workflowRunAction(file, options) {
|
|
|
601
603
|
console.error(chalk.red(`\n Workflow cannot run — missing required skills:\n`));
|
|
602
604
|
for (const err of validation.errors)
|
|
603
605
|
console.error(chalk.red(` \u2717 ${err}`));
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
606
|
+
const unknown = validation.missing.filter((m) => m.category === "unknown");
|
|
607
|
+
const envGaps = validation.missing.filter((m) => m.category !== "unknown" && m.missingEnv.length > 0);
|
|
608
|
+
if (unknown.length > 0) {
|
|
609
|
+
console.error(chalk.dim(`\n These skill IDs aren't built-in or discovered in .{claude,sweny,agents,gemini}/skills/.\n Scaffold one with:\n`));
|
|
610
|
+
for (const m of unknown) {
|
|
611
|
+
console.error(chalk.dim(` sweny skill new ${m.id}`));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (envGaps.length > 0) {
|
|
615
|
+
console.error(chalk.dim(`\n These skills are built-in but their env vars aren't set:`));
|
|
616
|
+
for (const m of envGaps) {
|
|
617
|
+
console.error(chalk.dim(` - ${m.id}: ${m.missingEnv.join(", ")}`));
|
|
610
618
|
}
|
|
611
619
|
}
|
|
612
|
-
console.error(
|
|
620
|
+
console.error(chalk.dim(`\n Run \`sweny skill list\` to see what's available.\n`));
|
|
613
621
|
process.exit(1);
|
|
614
622
|
return;
|
|
615
623
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny skill` command group — discover, list, and scaffold skills.
|
|
3
|
+
*
|
|
4
|
+
* Skills are the unit of capability a workflow node names in its `skills`
|
|
5
|
+
* array. Sweny ships built-ins (github, linear, slack, ...) and discovers
|
|
6
|
+
* custom skills from `.{gemini,agents,claude,sweny}/skills/<id>/SKILL.md`.
|
|
7
|
+
*
|
|
8
|
+
* This module owns the *authoring* side of that loop:
|
|
9
|
+
*
|
|
10
|
+
* sweny skill new <id> scaffold a SKILL.md template
|
|
11
|
+
* sweny skill list list built-in + custom skills together
|
|
12
|
+
*
|
|
13
|
+
* Discovery and validation already live in `../skills/custom-loader.js`
|
|
14
|
+
* and `../skills/index.js`; this CLI is a thin authoring shell over them.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
declare const VALID_CATEGORIES: readonly ["general", "git", "tasks", "notification", "observability", "data"];
|
|
18
|
+
type SkillCategory = (typeof VALID_CATEGORIES)[number];
|
|
19
|
+
/** Build the SKILL.md body for a fresh scaffold. Exported for tests. */
|
|
20
|
+
export declare function renderSkillTemplate(opts: {
|
|
21
|
+
id: string;
|
|
22
|
+
description: string;
|
|
23
|
+
category: SkillCategory;
|
|
24
|
+
}): string;
|
|
25
|
+
/** Register the `sweny skill` subcommand group on the parent program. */
|
|
26
|
+
export declare function registerSkillCommand(program: Command): void;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sweny skill` command group — discover, list, and scaffold skills.
|
|
3
|
+
*
|
|
4
|
+
* Skills are the unit of capability a workflow node names in its `skills`
|
|
5
|
+
* array. Sweny ships built-ins (github, linear, slack, ...) and discovers
|
|
6
|
+
* custom skills from `.{gemini,agents,claude,sweny}/skills/<id>/SKILL.md`.
|
|
7
|
+
*
|
|
8
|
+
* This module owns the *authoring* side of that loop:
|
|
9
|
+
*
|
|
10
|
+
* sweny skill new <id> scaffold a SKILL.md template
|
|
11
|
+
* sweny skill list list built-in + custom skills together
|
|
12
|
+
*
|
|
13
|
+
* Discovery and validation already live in `../skills/custom-loader.js`
|
|
14
|
+
* and `../skills/index.js`; this CLI is a thin authoring shell over them.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { builtinSkills } from "../skills/index.js";
|
|
20
|
+
import { configuredSkillsWithDiagnostics, discoverSkillsWithDiagnostics } from "../skills/custom-loader.js";
|
|
21
|
+
// Mirror of the loader's regex so authoring-side validation matches discovery.
|
|
22
|
+
const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
23
|
+
const VALID_CATEGORIES = ["general", "git", "tasks", "notification", "observability", "data"];
|
|
24
|
+
const HARNESS_DIRS = {
|
|
25
|
+
claude: ".claude/skills",
|
|
26
|
+
sweny: ".sweny/skills",
|
|
27
|
+
agents: ".agents/skills",
|
|
28
|
+
gemini: ".gemini/skills",
|
|
29
|
+
};
|
|
30
|
+
/** Build the SKILL.md body for a fresh scaffold. Exported for tests. */
|
|
31
|
+
export function renderSkillTemplate(opts) {
|
|
32
|
+
const { id, description, category } = opts;
|
|
33
|
+
const lines = [];
|
|
34
|
+
lines.push("---");
|
|
35
|
+
lines.push(`name: ${id}`);
|
|
36
|
+
lines.push(`description: ${description}`);
|
|
37
|
+
lines.push(`category: ${category}`);
|
|
38
|
+
lines.push("# config:");
|
|
39
|
+
lines.push("# EXAMPLE_ENV_VAR:");
|
|
40
|
+
lines.push("# description: Human-readable description shown by `sweny check`");
|
|
41
|
+
lines.push("# required: true");
|
|
42
|
+
lines.push("# mcp:");
|
|
43
|
+
lines.push("# command: npx");
|
|
44
|
+
lines.push('# args: ["-y", "@your-org/your-mcp-server"]');
|
|
45
|
+
lines.push("# env:");
|
|
46
|
+
lines.push("# SOMETHING: ${SOMETHING}");
|
|
47
|
+
lines.push("---");
|
|
48
|
+
lines.push("");
|
|
49
|
+
lines.push(`# ${id}`);
|
|
50
|
+
lines.push("");
|
|
51
|
+
lines.push(description);
|
|
52
|
+
lines.push("");
|
|
53
|
+
lines.push("## When to use");
|
|
54
|
+
lines.push("");
|
|
55
|
+
lines.push(`Reference this skill from a workflow node when the node needs ${description.toLowerCase()}.`);
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("```yaml");
|
|
58
|
+
lines.push("nodes:");
|
|
59
|
+
lines.push(" my_node:");
|
|
60
|
+
lines.push(" name: My Node");
|
|
61
|
+
lines.push(` instruction: >-`);
|
|
62
|
+
lines.push(` Use the ${id} capability described in this skill.`);
|
|
63
|
+
lines.push(` skills: [${id}]`);
|
|
64
|
+
lines.push("```");
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("## What this skill provides");
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push("Describe the contract this skill exposes. Custom skills are *instruction-only* by default — the markdown body becomes guidance the LLM sees when the node runs. Add an `mcp:` block in the frontmatter above to wire in tool execution via an MCP server.");
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("Concrete things to document here:");
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push("- Required environment variables (also declare in `config:` above so `sweny check` validates them).");
|
|
73
|
+
lines.push("- The exact commands or tool calls the node should make.");
|
|
74
|
+
lines.push("- Common failure modes and how to recover.");
|
|
75
|
+
lines.push("- Any preconditions the workflow must satisfy before invoking this skill.");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("## Example invocation");
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push("Replace this section with a concrete usage example, including the exact bash, MCP tool call, or LLM prompt the node should produce.");
|
|
80
|
+
lines.push("");
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
function runSkillNew(idArg, options) {
|
|
84
|
+
const id = idArg.toLowerCase();
|
|
85
|
+
if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
|
|
86
|
+
console.error(chalk.red(` Invalid skill id "${idArg}".\n Skill ids must be lowercase alphanumeric + hyphens (no consecutive hyphens), max 64 chars.`));
|
|
87
|
+
process.exit(2);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const description = (options.description ?? `Custom ${id} skill`).trim();
|
|
91
|
+
const category = (options.category ?? "general");
|
|
92
|
+
if (!VALID_CATEGORIES.includes(category)) {
|
|
93
|
+
console.error(chalk.red(` Invalid category "${category}".\n Allowed: ${VALID_CATEGORIES.join(", ")}`));
|
|
94
|
+
process.exit(2);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const harness = options.harness ?? "claude";
|
|
98
|
+
const baseDir = HARNESS_DIRS[harness];
|
|
99
|
+
if (!baseDir) {
|
|
100
|
+
console.error(chalk.red(` Unknown harness "${harness}". Allowed: ${Object.keys(HARNESS_DIRS).join(", ")}`));
|
|
101
|
+
process.exit(2);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const cwd = process.cwd();
|
|
105
|
+
const skillDir = path.join(cwd, baseDir, id);
|
|
106
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
107
|
+
if (fs.existsSync(skillFile) && !options.force) {
|
|
108
|
+
console.error(chalk.red(` ${path.relative(cwd, skillFile)} already exists. Pass --force to overwrite.`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
113
|
+
fs.writeFileSync(skillFile, renderSkillTemplate({ id, description, category }), "utf-8");
|
|
114
|
+
const rel = path.relative(cwd, skillFile);
|
|
115
|
+
console.log(chalk.green(`\n Created ${rel}\n`));
|
|
116
|
+
console.log(chalk.dim(" Next steps:"));
|
|
117
|
+
console.log(chalk.dim(` 1. Open ${rel} and replace the placeholder sections.`));
|
|
118
|
+
console.log(chalk.dim(` 2. If your skill needs env vars, uncomment the 'config:' block in the frontmatter.`));
|
|
119
|
+
console.log(chalk.dim(` 3. If your skill executes tools via MCP, uncomment the 'mcp:' block.`));
|
|
120
|
+
console.log(chalk.dim(` 4. Reference the skill in a workflow node: skills: [${id}]`));
|
|
121
|
+
console.log(chalk.dim(` 5. Run \`sweny skill list\` to confirm it discovers correctly.`));
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
function runSkillList(options) {
|
|
125
|
+
const cwd = process.cwd();
|
|
126
|
+
// We want the FULL list — built-in + custom — even when env vars
|
|
127
|
+
// aren't set. configuredSkills filters by env, which hides authoring-
|
|
128
|
+
// time skills the user just scaffolded. So combine sources directly.
|
|
129
|
+
const { skills: customSkills, warnings: customWarnings } = discoverSkillsWithDiagnostics(cwd);
|
|
130
|
+
const customIds = new Set(customSkills.map((s) => s.id));
|
|
131
|
+
const builtinList = builtinSkills.filter((s) => !customIds.has(s.id));
|
|
132
|
+
// Configured map drives the "configured?" badge — required env vars present.
|
|
133
|
+
const configuredIds = new Set(configuredSkillsWithDiagnostics(process.env, cwd).skills.map((s) => s.id));
|
|
134
|
+
if (options.json) {
|
|
135
|
+
const data = [
|
|
136
|
+
...builtinList.map((s) => ({
|
|
137
|
+
id: s.id,
|
|
138
|
+
kind: "builtin",
|
|
139
|
+
category: s.category,
|
|
140
|
+
description: s.description,
|
|
141
|
+
configured: configuredIds.has(s.id),
|
|
142
|
+
})),
|
|
143
|
+
...customSkills.map((s) => ({
|
|
144
|
+
id: s.id,
|
|
145
|
+
kind: "custom",
|
|
146
|
+
category: s.category,
|
|
147
|
+
description: s.description,
|
|
148
|
+
configured: configuredIds.has(s.id),
|
|
149
|
+
})),
|
|
150
|
+
];
|
|
151
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const renderRow = (s, kind) => {
|
|
155
|
+
const tag = kind === "custom" ? chalk.magenta("custom") : chalk.dim("builtin");
|
|
156
|
+
const ok = configuredIds.has(s.id) ? chalk.green("✓") : chalk.yellow("·");
|
|
157
|
+
console.log(` ${ok} ${chalk.cyan(s.id)} ${chalk.dim(`(${s.category})`)} ${tag}`);
|
|
158
|
+
console.log(chalk.dim(` ${s.description}`));
|
|
159
|
+
};
|
|
160
|
+
console.log(chalk.bold("\nAvailable skills:\n"));
|
|
161
|
+
console.log(chalk.dim(" ✓ = configured (required env vars present)"));
|
|
162
|
+
console.log(chalk.dim(" · = present but not configured"));
|
|
163
|
+
console.log();
|
|
164
|
+
if (builtinList.length === 0 && customSkills.length === 0) {
|
|
165
|
+
console.log(chalk.dim(" (no skills found)\n"));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
for (const s of builtinList)
|
|
169
|
+
renderRow(s, "builtin");
|
|
170
|
+
for (const s of customSkills)
|
|
171
|
+
renderRow(s, "custom");
|
|
172
|
+
}
|
|
173
|
+
if (customWarnings.length > 0) {
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(chalk.yellow(` ${customWarnings.length} skill discovery warning(s):`));
|
|
176
|
+
for (const w of customWarnings) {
|
|
177
|
+
console.log(chalk.yellow(` ${w.kind}: ${w.message}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(chalk.dim(` Scaffold a new skill: sweny skill new <id>`));
|
|
182
|
+
console.log();
|
|
183
|
+
}
|
|
184
|
+
/** Register the `sweny skill` subcommand group on the parent program. */
|
|
185
|
+
export function registerSkillCommand(program) {
|
|
186
|
+
const skillCmd = program.command("skill").description("Author and inspect skills");
|
|
187
|
+
skillCmd
|
|
188
|
+
.command("new <id>")
|
|
189
|
+
.description("Scaffold a new SKILL.md in .claude/skills/<id>/")
|
|
190
|
+
.option("-d, --description <text>", "One-line description (required field in frontmatter)")
|
|
191
|
+
.option("-c, --category <category>", `Skill category (${VALID_CATEGORIES.join("|")})`, "general")
|
|
192
|
+
.option("--harness <harness>", `Where to place the skill (${Object.keys(HARNESS_DIRS).join("|")})`, "claude")
|
|
193
|
+
.option("--force", "Overwrite an existing SKILL.md")
|
|
194
|
+
.action((id, options) => runSkillNew(id, options));
|
|
195
|
+
skillCmd
|
|
196
|
+
.command("list")
|
|
197
|
+
.description("List built-in and custom skills together with configuration status")
|
|
198
|
+
.option("--json", "Output as JSON array")
|
|
199
|
+
.action((options) => runSkillList(options));
|
|
200
|
+
}
|
|
@@ -99,6 +99,10 @@ function parseSkillMd(content, path) {
|
|
|
99
99
|
}
|
|
100
100
|
// Narrow the loosely-typed YAML output into the fields we expect
|
|
101
101
|
const fm = raw;
|
|
102
|
+
const VALID_CATEGORIES = ["general", "git", "tasks", "notification", "observability", "data"];
|
|
103
|
+
const category = typeof fm.category === "string" && VALID_CATEGORIES.includes(fm.category)
|
|
104
|
+
? fm.category
|
|
105
|
+
: "general";
|
|
102
106
|
const id = String(fm.name);
|
|
103
107
|
if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
|
|
104
108
|
return {
|
|
@@ -145,7 +149,7 @@ function parseSkillMd(content, path) {
|
|
|
145
149
|
id,
|
|
146
150
|
name: id,
|
|
147
151
|
description: typeof fm.description === "string" ? fm.description : `Custom skill: ${id}`,
|
|
148
|
-
category
|
|
152
|
+
category,
|
|
149
153
|
config,
|
|
150
154
|
tools: [],
|
|
151
155
|
instruction: instruction || undefined,
|
package/dist/types.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface ConfigField {
|
|
|
23
23
|
env?: string;
|
|
24
24
|
}
|
|
25
25
|
/** Skill categories — used for validation and grouping */
|
|
26
|
-
export type SkillCategory = "git" | "observability" | "tasks" | "notification" | "general";
|
|
26
|
+
export type SkillCategory = "git" | "observability" | "tasks" | "notification" | "data" | "general";
|
|
27
27
|
/** A skill groups related tools with shared config requirements */
|
|
28
28
|
export interface Skill {
|
|
29
29
|
id: string;
|