@sweny-ai/core 0.1.87 → 0.1.89

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/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
- if (validation.missing.length > 0) {
605
- console.error(chalk.dim(`\n Set the missing env vars and try again:`));
606
- for (const m of validation.missing) {
607
- if (m.missingEnv.length > 0) {
608
- console.error(chalk.dim(` - ${m.id}: ${m.missingEnv.join(", ")}`));
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,54 @@
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
+ import { type SkillCategory } from "../types.js";
18
+ declare const HARNESS_DIRS: {
19
+ readonly claude: ".claude/skills";
20
+ readonly sweny: ".sweny/skills";
21
+ readonly agents: ".agents/skills";
22
+ readonly gemini: ".gemini/skills";
23
+ };
24
+ type HarnessKey = keyof typeof HARNESS_DIRS;
25
+ /** Build the SKILL.md body for a fresh scaffold. Exported for tests. */
26
+ export declare function renderSkillTemplate(opts: {
27
+ id: string;
28
+ description: string;
29
+ category: SkillCategory;
30
+ }): string;
31
+ interface NewOptions {
32
+ description?: string;
33
+ category?: string;
34
+ harness?: HarnessKey;
35
+ force?: boolean;
36
+ }
37
+ /**
38
+ * Action handler for `sweny skill new`. Exported for unit tests; production
39
+ * code goes through `registerSkillCommand`. `cwd` defaults to `process.cwd()`
40
+ * so the CLI works as expected; tests pass a tmp dir for isolation.
41
+ */
42
+ export declare function runSkillNew(idArg: string, options: NewOptions, cwd?: string): void;
43
+ interface ListOptions {
44
+ json?: boolean;
45
+ }
46
+ /**
47
+ * Action handler for `sweny skill list`. Exported for unit tests; production
48
+ * code goes through `registerSkillCommand`. `cwd` and `env` injection lets
49
+ * tests exercise the configured-badge logic without polluting global state.
50
+ */
51
+ export declare function runSkillList(options: ListOptions, cwd?: string, env?: Record<string, string | undefined>): void;
52
+ /** Register the `sweny skill` subcommand group on the parent program. */
53
+ export declare function registerSkillCommand(program: Command): void;
54
+ export {};
@@ -0,0 +1,208 @@
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
+ import { SKILL_CATEGORIES } from "../types.js";
22
+ // Mirror of the loader's regex so authoring-side validation matches discovery.
23
+ const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
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
+ /**
84
+ * Action handler for `sweny skill new`. Exported for unit tests; production
85
+ * code goes through `registerSkillCommand`. `cwd` defaults to `process.cwd()`
86
+ * so the CLI works as expected; tests pass a tmp dir for isolation.
87
+ */
88
+ export function runSkillNew(idArg, options, cwd = process.cwd()) {
89
+ const id = idArg.toLowerCase();
90
+ if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
91
+ console.error(chalk.red(` Invalid skill id "${idArg}".\n Skill ids must be lowercase alphanumeric + hyphens (no consecutive hyphens), max 64 chars.`));
92
+ process.exit(2);
93
+ return;
94
+ }
95
+ const description = (options.description ?? `Custom ${id} skill`).trim();
96
+ const category = (options.category ?? "general");
97
+ if (!SKILL_CATEGORIES.includes(category)) {
98
+ console.error(chalk.red(` Invalid category "${category}".\n Allowed: ${SKILL_CATEGORIES.join(", ")}`));
99
+ process.exit(2);
100
+ return;
101
+ }
102
+ const harness = options.harness ?? "claude";
103
+ const baseDir = HARNESS_DIRS[harness];
104
+ if (!baseDir) {
105
+ console.error(chalk.red(` Unknown harness "${harness}". Allowed: ${Object.keys(HARNESS_DIRS).join(", ")}`));
106
+ process.exit(2);
107
+ return;
108
+ }
109
+ const skillDir = path.join(cwd, baseDir, id);
110
+ const skillFile = path.join(skillDir, "SKILL.md");
111
+ if (fs.existsSync(skillFile) && !options.force) {
112
+ console.error(chalk.red(` ${path.relative(cwd, skillFile)} already exists. Pass --force to overwrite.`));
113
+ process.exit(1);
114
+ return;
115
+ }
116
+ fs.mkdirSync(skillDir, { recursive: true });
117
+ fs.writeFileSync(skillFile, renderSkillTemplate({ id, description, category }), "utf-8");
118
+ const rel = path.relative(cwd, skillFile);
119
+ console.log(chalk.green(`\n Created ${rel}\n`));
120
+ console.log(chalk.dim(" Next steps:"));
121
+ console.log(chalk.dim(` 1. Open ${rel} and replace the placeholder sections.`));
122
+ console.log(chalk.dim(` 2. If your skill needs env vars, uncomment the 'config:' block in the frontmatter.`));
123
+ console.log(chalk.dim(` 3. If your skill executes tools via MCP, uncomment the 'mcp:' block.`));
124
+ console.log(chalk.dim(` 4. Reference the skill in a workflow node: skills: [${id}]`));
125
+ console.log(chalk.dim(` 5. Run \`sweny skill list\` to confirm it discovers correctly.`));
126
+ console.log();
127
+ }
128
+ /**
129
+ * Action handler for `sweny skill list`. Exported for unit tests; production
130
+ * code goes through `registerSkillCommand`. `cwd` and `env` injection lets
131
+ * tests exercise the configured-badge logic without polluting global state.
132
+ */
133
+ export function runSkillList(options, cwd = process.cwd(), env = process.env) {
134
+ // We want the FULL list (built-in + custom) even when env vars aren't set.
135
+ // configuredSkills filters by env, which hides authoring-time skills the
136
+ // user just scaffolded, so combine sources directly.
137
+ const { skills: customSkills, warnings: customWarnings } = discoverSkillsWithDiagnostics(cwd);
138
+ const customIds = new Set(customSkills.map((s) => s.id));
139
+ const builtinList = builtinSkills.filter((s) => !customIds.has(s.id));
140
+ // Configured map drives the "configured?" badge: required env vars present.
141
+ const configuredIds = new Set(configuredSkillsWithDiagnostics(env, cwd).skills.map((s) => s.id));
142
+ if (options.json) {
143
+ const data = [
144
+ ...builtinList.map((s) => ({
145
+ id: s.id,
146
+ kind: "builtin",
147
+ category: s.category,
148
+ description: s.description,
149
+ configured: configuredIds.has(s.id),
150
+ })),
151
+ ...customSkills.map((s) => ({
152
+ id: s.id,
153
+ kind: "custom",
154
+ category: s.category,
155
+ description: s.description,
156
+ configured: configuredIds.has(s.id),
157
+ })),
158
+ ];
159
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
160
+ return;
161
+ }
162
+ const renderRow = (s, kind) => {
163
+ const tag = kind === "custom" ? chalk.magenta("custom") : chalk.dim("builtin");
164
+ const ok = configuredIds.has(s.id) ? chalk.green("✓") : chalk.yellow("·");
165
+ console.log(` ${ok} ${chalk.cyan(s.id)} ${chalk.dim(`(${s.category})`)} ${tag}`);
166
+ console.log(chalk.dim(` ${s.description}`));
167
+ };
168
+ console.log(chalk.bold("\nAvailable skills:\n"));
169
+ console.log(chalk.dim(" ✓ = configured (required env vars present)"));
170
+ console.log(chalk.dim(" · = present but not configured"));
171
+ console.log();
172
+ if (builtinList.length === 0 && customSkills.length === 0) {
173
+ console.log(chalk.dim(" (no skills found)\n"));
174
+ }
175
+ else {
176
+ for (const s of builtinList)
177
+ renderRow(s, "builtin");
178
+ for (const s of customSkills)
179
+ renderRow(s, "custom");
180
+ }
181
+ if (customWarnings.length > 0) {
182
+ console.log();
183
+ console.log(chalk.yellow(` ${customWarnings.length} skill discovery warning(s):`));
184
+ for (const w of customWarnings) {
185
+ console.log(chalk.yellow(` ${w.kind}: ${w.message}`));
186
+ }
187
+ }
188
+ console.log();
189
+ console.log(chalk.dim(` Scaffold a new skill: sweny skill new <id>`));
190
+ console.log();
191
+ }
192
+ /** Register the `sweny skill` subcommand group on the parent program. */
193
+ export function registerSkillCommand(program) {
194
+ const skillCmd = program.command("skill").description("Author and inspect skills");
195
+ skillCmd
196
+ .command("new <id>")
197
+ .description("Scaffold a new SKILL.md in .claude/skills/<id>/")
198
+ .option("-d, --description <text>", "One-line description (required field in frontmatter)")
199
+ .option("-c, --category <category>", `Skill category (${SKILL_CATEGORIES.join("|")})`, "general")
200
+ .option("--harness <harness>", `Where to place the skill (${Object.keys(HARNESS_DIRS).join("|")})`, "claude")
201
+ .option("--force", "Overwrite an existing SKILL.md")
202
+ .action((id, options) => runSkillNew(id, options));
203
+ skillCmd
204
+ .command("list")
205
+ .description("List built-in and custom skills together with configuration status")
206
+ .option("--json", "Output as JSON array")
207
+ .action((options) => runSkillList(options));
208
+ }
package/dist/schema.d.ts CHANGED
@@ -241,7 +241,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
241
241
  id: string;
242
242
  name: string;
243
243
  description: string;
244
- category: "git" | "observability" | "tasks" | "notification" | "general";
244
+ category: "general" | "git" | "tasks" | "notification" | "observability";
245
245
  config: Record<string, {
246
246
  description: string;
247
247
  required?: boolean | undefined;
@@ -266,7 +266,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
266
266
  id: string;
267
267
  name: string;
268
268
  description: string;
269
- category: "git" | "observability" | "tasks" | "notification" | "general";
269
+ category: "general" | "git" | "tasks" | "notification" | "observability";
270
270
  instruction?: string | undefined;
271
271
  mcp?: {
272
272
  url?: string | undefined;
@@ -291,7 +291,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
291
291
  id: string;
292
292
  name: string;
293
293
  description: string;
294
- category: "git" | "observability" | "tasks" | "notification" | "general";
294
+ category: "general" | "git" | "tasks" | "notification" | "observability";
295
295
  config: Record<string, {
296
296
  description: string;
297
297
  required?: boolean | undefined;
@@ -316,7 +316,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
316
316
  id: string;
317
317
  name: string;
318
318
  description: string;
319
- category: "git" | "observability" | "tasks" | "notification" | "general";
319
+ category: "general" | "git" | "tasks" | "notification" | "observability";
320
320
  instruction?: string | undefined;
321
321
  mcp?: {
322
322
  url?: string | undefined;
@@ -11,6 +11,7 @@
11
11
  import { readdirSync, readFileSync, existsSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import YAML from "yaml";
14
+ import { SKILL_CATEGORIES } from "../types.js";
14
15
  import { builtinSkills, isSkillConfigured } from "./index.js";
15
16
  /** Directories to scan, in ascending priority order. Last match wins. */
16
17
  const SKILL_DIRS = [".gemini/skills", ".agents/skills", ".claude/skills", ".sweny/skills"];
@@ -99,6 +100,9 @@ function parseSkillMd(content, path) {
99
100
  }
100
101
  // Narrow the loosely-typed YAML output into the fields we expect
101
102
  const fm = raw;
103
+ const category = typeof fm.category === "string" && SKILL_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: "general",
152
+ category,
149
153
  config,
150
154
  tools: [],
151
155
  instruction: instruction || undefined,
package/dist/types.d.ts CHANGED
@@ -22,8 +22,16 @@ export interface ConfigField {
22
22
  /** Default environment variable to read from */
23
23
  env?: string;
24
24
  }
25
- /** Skill categories — used for validation and grouping */
26
- export type SkillCategory = "git" | "observability" | "tasks" | "notification" | "general";
25
+ /**
26
+ * Skill categories: used for per-node validation and grouping.
27
+ *
28
+ * Single source of truth for runtime + compile-time. The `as const` tuple
29
+ * lets the CLI iterate it for help text and the loader use it for runtime
30
+ * validation; `SkillCategory` is derived so adding a category requires
31
+ * only this line.
32
+ */
33
+ export declare const SKILL_CATEGORIES: readonly ["general", "git", "tasks", "notification", "observability", "data"];
34
+ export type SkillCategory = (typeof SKILL_CATEGORIES)[number];
27
35
  /** A skill groups related tools with shared config requirements */
28
36
  export interface Skill {
29
37
  id: string;
package/dist/types.js CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Skill categories: used for per-node validation and grouping.
3
+ *
4
+ * Single source of truth for runtime + compile-time. The `as const` tuple
5
+ * lets the CLI iterate it for help text and the loader use it for runtime
6
+ * validation; `SkillCategory` is derived so adding a category requires
7
+ * only this line.
8
+ */
9
+ export const SKILL_CATEGORIES = ["general", "git", "tasks", "notification", "observability", "data"];
1
10
  export const consoleLogger = {
2
11
  info: (msg, data) => console.log(`[info] ${msg}`, data ?? ""),
3
12
  warn: (msg, data) => console.warn(`[warn] ${msg}`, data ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"