@sweny-ai/core 0.1.38 → 0.1.40

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.
@@ -8,7 +8,8 @@ import { slack } from "../skills/slack.js";
8
8
  import { sentry } from "../skills/sentry.js";
9
9
  import { datadog } from "../skills/datadog.js";
10
10
  import { notification } from "../skills/notification.js";
11
- import { loadCustomSkills, configuredSkills, createSkillMap } from "../skills/index.js";
11
+ import { createSkillMap } from "../skills/index.js";
12
+ import { loadCustomSkills, configuredSkills } from "../skills/custom-loader.js";
12
13
  // ─── Helpers ─────────────────────────────────────────────────────
13
14
  const mockCtx = (config = {}) => ({
14
15
  config,
package/dist/browser.d.ts CHANGED
@@ -9,7 +9,7 @@ export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema,
9
9
  export { consoleLogger } from "./types.js";
10
10
  export { execute } from "./executor.js";
11
11
  export type { ExecuteOptions } from "./executor.js";
12
- export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
12
+ export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
13
13
  export type { SkillValidationResult } from "./skills/index.js";
14
14
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
15
15
  export type { WorkflowError } from "./schema.js";
package/dist/browser.js CHANGED
@@ -8,8 +8,8 @@
8
8
  export { consoleLogger } from "./types.js";
9
9
  // Executor
10
10
  export { execute } from "./executor.js";
11
- // Skills
12
- export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
11
+ // Skills (browser-safe — no filesystem access, no `process.env` reads)
12
+ export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
13
13
  // Schema & validation
14
14
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
15
15
  // Studio adapter
package/dist/cli/e2e.js CHANGED
@@ -11,7 +11,8 @@ import chalk from "chalk";
11
11
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
12
12
  import { execute } from "../executor.js";
13
13
  import { ClaudeClient } from "../claude.js";
14
- import { createSkillMap, configuredSkills } from "../skills/index.js";
14
+ import { createSkillMap } from "../skills/index.js";
15
+ import { configuredSkills } from "../skills/custom-loader.js";
15
16
  import { consoleLogger } from "../types.js";
16
17
  import { validateWorkflow } from "../schema.js";
17
18
  // ── Template variable resolution ───────────────────────────────────────
package/dist/cli/main.js CHANGED
@@ -10,8 +10,9 @@ import { execute } from "../executor.js";
10
10
  import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
11
11
  import { consoleLogger } from "../types.js";
12
12
  import { ClaudeClient } from "../claude.js";
13
- import { createSkillMap, configuredSkills } from "../skills/index.js";
14
- import { buildAutoMcpServers, buildProviderContext } from "../mcp.js";
13
+ import { createSkillMap, validateWorkflowSkills } from "../skills/index.js";
14
+ import { configuredSkills } from "../skills/custom-loader.js";
15
+ import { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "../mcp.js";
15
16
  import { loadAdditionalContext } from "../templates.js";
16
17
  import { validateWorkflow as validateWorkflowSchema } from "../schema.js";
17
18
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
@@ -560,8 +561,40 @@ export async function workflowRunAction(file, options) {
560
561
  const isJson = Boolean(options.json);
561
562
  const isTTY = !isJson && (process.stderr.isTTY ?? false);
562
563
  const skills = createSkillMap(configuredSkills(process.env, process.cwd()));
563
- const mcpAutoConfig = buildMcpAutoConfig(config);
564
- const mcpServers = buildAutoMcpServers(mcpAutoConfig);
564
+ // Hard-fail at startup if the workflow references skills that aren't available.
565
+ // Each node lists alternatives by category (e.g. [sentry, datadog, betterstack]);
566
+ // we require at least one configured skill per category.
567
+ const validation = validateWorkflowSkills(workflow, skills);
568
+ if (validation.errors.length > 0) {
569
+ console.error(chalk.red(`\n Workflow cannot run — missing required skills:\n`));
570
+ for (const err of validation.errors)
571
+ console.error(chalk.red(` \u2717 ${err}`));
572
+ if (validation.missing.length > 0) {
573
+ console.error(chalk.dim(`\n Set the missing env vars and try again:`));
574
+ for (const m of validation.missing) {
575
+ if (m.missingEnv.length > 0) {
576
+ console.error(chalk.dim(` - ${m.id}: ${m.missingEnv.join(", ")}`));
577
+ }
578
+ }
579
+ }
580
+ console.error("");
581
+ process.exit(1);
582
+ return;
583
+ }
584
+ for (const warn of validation.warnings)
585
+ console.error(chalk.yellow(` \u26A0 ${warn}`));
586
+ // Engine-driven MCP wiring: only inject MCPs for skills that the workflow
587
+ // actually references AND whose env vars are present.
588
+ const referencedSkillIds = new Set();
589
+ for (const node of Object.values(workflow.nodes)) {
590
+ for (const id of node.skills)
591
+ referencedSkillIds.add(id);
592
+ }
593
+ const mcpServers = buildSkillMcpServers({
594
+ referencedSkills: referencedSkillIds,
595
+ credentials: buildCredentialMap(),
596
+ userMcpServers: Object.keys(config.mcpServers).length > 0 ? config.mcpServers : undefined,
597
+ });
565
598
  const claude = new ClaudeClient({
566
599
  maxTurns: config.maxInvestigateTurns || 50,
567
600
  cwd: process.cwd(),
package/dist/index.d.ts CHANGED
@@ -27,8 +27,9 @@ export { execute } from "./executor.js";
27
27
  export type { ExecuteOptions } from "./executor.js";
28
28
  export { ClaudeClient } from "./claude.js";
29
29
  export type { ClaudeClientOptions } from "./claude.js";
30
- export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
30
+ export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
31
31
  export type { SkillValidationResult } from "./skills/index.js";
32
+ export { loadCustomSkills, configuredSkills } from "./skills/custom-loader.js";
32
33
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
33
34
  export type { WorkflowError } from "./schema.js";
34
35
  export { buildAutoMcpServers, buildProviderContext } from "./mcp.js";
package/dist/index.js CHANGED
@@ -27,7 +27,9 @@ export { execute } from "./executor.js";
27
27
  // Claude client
28
28
  export { ClaudeClient } from "./claude.js";
29
29
  // Skills
30
- export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
30
+ export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
31
+ // Node-only skill discovery (filesystem-based)
32
+ export { loadCustomSkills, configuredSkills } from "./skills/custom-loader.js";
31
33
  // Schema & validation
32
34
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
33
35
  // MCP auto-injection
package/dist/mcp.d.ts CHANGED
@@ -1,4 +1,27 @@
1
1
  import type { McpAutoConfig, McpServerConfig } from "./types.js";
2
+ export interface SkillMcpOptions {
3
+ /** Skill IDs referenced by the workflow being executed. Only these get MCPs. */
4
+ referencedSkills: Set<string>;
5
+ /** Flat credential map (env vars). MCPs are only wired when their creds are set. */
6
+ credentials: Record<string, string>;
7
+ /** User-supplied MCP servers — always win on key conflict. */
8
+ userMcpServers?: Record<string, McpServerConfig>;
9
+ }
10
+ /**
11
+ * Build MCP server configs for the skills a workflow references.
12
+ *
13
+ * For each skill ID in `referencedSkills`, look up its MCP wiring (if any)
14
+ * and include it only when the required credentials are present in `credentials`.
15
+ *
16
+ * Skills with no MCP variant (e.g. `notification`, `supabase`) are silently
17
+ * skipped — their in-process tools are wired separately by the executor.
18
+ *
19
+ * Unknown skill IDs (not built-in, not in this MCP catalog) are silently
20
+ * skipped here; the engine's hard-fail validation handles them upstream.
21
+ *
22
+ * User-supplied servers always win on key conflict.
23
+ */
24
+ export declare function buildSkillMcpServers(opts: SkillMcpOptions): Record<string, McpServerConfig>;
2
25
  /**
3
26
  * Auto-configure well-known MCP servers based on which providers
4
27
  * and workspace tools the user has enabled.
package/dist/mcp.js CHANGED
@@ -1,3 +1,163 @@
1
+ /**
2
+ * Build MCP server configs for the skills a workflow references.
3
+ *
4
+ * For each skill ID in `referencedSkills`, look up its MCP wiring (if any)
5
+ * and include it only when the required credentials are present in `credentials`.
6
+ *
7
+ * Skills with no MCP variant (e.g. `notification`, `supabase`) are silently
8
+ * skipped — their in-process tools are wired separately by the executor.
9
+ *
10
+ * Unknown skill IDs (not built-in, not in this MCP catalog) are silently
11
+ * skipped here; the engine's hard-fail validation handles them upstream.
12
+ *
13
+ * User-supplied servers always win on key conflict.
14
+ */
15
+ export function buildSkillMcpServers(opts) {
16
+ const auto = {};
17
+ const refs = opts.referencedSkills;
18
+ const creds = opts.credentials;
19
+ // GitHub MCP — wired when workflow uses `github` skill and a token is set.
20
+ if (refs.has("github") && creds.GITHUB_TOKEN) {
21
+ auto["github"] = {
22
+ type: "stdio",
23
+ command: "npx",
24
+ args: ["-y", "@modelcontextprotocol/server-github@latest"],
25
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: creds.GITHUB_TOKEN },
26
+ };
27
+ }
28
+ // GitLab MCP.
29
+ if (refs.has("gitlab") && creds.GITLAB_TOKEN) {
30
+ const env = { GITLAB_PERSONAL_ACCESS_TOKEN: creds.GITLAB_TOKEN };
31
+ const baseUrl = creds.GITLAB_URL || "https://gitlab.com";
32
+ if (baseUrl !== "https://gitlab.com")
33
+ env.GITLAB_API_URL = `${baseUrl}/api/v4`;
34
+ auto["gitlab"] = {
35
+ type: "stdio",
36
+ command: "npx",
37
+ args: ["-y", "@modelcontextprotocol/server-gitlab@latest"],
38
+ env,
39
+ };
40
+ }
41
+ // Linear MCP — official remote HTTP endpoint.
42
+ if (refs.has("linear") && creds.LINEAR_API_KEY) {
43
+ auto["linear"] = {
44
+ type: "http",
45
+ url: "https://mcp.linear.app/mcp",
46
+ headers: { Authorization: `Bearer ${creds.LINEAR_API_KEY}` },
47
+ };
48
+ }
49
+ // Jira / Confluence — needs all 3 creds.
50
+ if (refs.has("jira") && creds.JIRA_URL && creds.JIRA_EMAIL && creds.JIRA_API_TOKEN) {
51
+ auto["jira"] = {
52
+ type: "stdio",
53
+ command: "npx",
54
+ args: ["-y", "@sooperset/mcp-atlassian@latest"],
55
+ env: {
56
+ JIRA_URL: creds.JIRA_URL,
57
+ JIRA_EMAIL: creds.JIRA_EMAIL,
58
+ JIRA_API_TOKEN: creds.JIRA_API_TOKEN,
59
+ },
60
+ };
61
+ }
62
+ // Datadog MCP.
63
+ if (refs.has("datadog") && creds.DD_API_KEY && creds.DD_APP_KEY) {
64
+ auto["datadog"] = {
65
+ type: "http",
66
+ url: "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp",
67
+ headers: { DD_API_KEY: creds.DD_API_KEY, DD_APPLICATION_KEY: creds.DD_APP_KEY },
68
+ };
69
+ }
70
+ // Sentry MCP — server reads SENTRY_ACCESS_TOKEN, not SENTRY_AUTH_TOKEN.
71
+ if (refs.has("sentry") && creds.SENTRY_AUTH_TOKEN) {
72
+ const env = { SENTRY_ACCESS_TOKEN: creds.SENTRY_AUTH_TOKEN };
73
+ const sentryUrl = creds.SENTRY_URL;
74
+ if (sentryUrl && sentryUrl !== "https://sentry.io") {
75
+ try {
76
+ env.SENTRY_HOST = new URL(sentryUrl).hostname;
77
+ }
78
+ catch {
79
+ // malformed URL — leave SENTRY_HOST unset
80
+ }
81
+ }
82
+ auto["sentry"] = {
83
+ type: "stdio",
84
+ command: "npx",
85
+ args: ["-y", "@sentry/mcp-server@latest"],
86
+ env,
87
+ };
88
+ }
89
+ // New Relic MCP — region-aware HTTP endpoint, header is "Api-Key".
90
+ if (refs.has("newrelic") && creds.NEW_RELIC_API_KEY) {
91
+ const region = creds.NEW_RELIC_REGION;
92
+ const url = region === "eu" ? "https://mcp.eu.newrelic.com/mcp/" : "https://mcp.newrelic.com/mcp/";
93
+ auto["newrelic"] = {
94
+ type: "http",
95
+ url,
96
+ headers: { "Api-Key": creds.NEW_RELIC_API_KEY },
97
+ };
98
+ }
99
+ // BetterStack MCP — accept any of the legacy/telemetry/uptime tokens.
100
+ const bsToken = creds.BETTERSTACK_API_TOKEN || creds.BETTERSTACK_TELEMETRY_TOKEN || creds.BETTERSTACK_UPTIME_TOKEN;
101
+ if (refs.has("betterstack") && bsToken) {
102
+ auto["betterstack"] = {
103
+ type: "http",
104
+ url: "https://mcp.betterstack.com",
105
+ headers: { Authorization: `Bearer ${bsToken}` },
106
+ };
107
+ }
108
+ // Slack MCP — wired when workflow uses `slack` skill and bot token is set.
109
+ if (refs.has("slack") && creds.SLACK_BOT_TOKEN) {
110
+ const env = { SLACK_BOT_TOKEN: creds.SLACK_BOT_TOKEN };
111
+ if (creds.SLACK_TEAM_ID)
112
+ env.SLACK_TEAM_ID = creds.SLACK_TEAM_ID;
113
+ auto["slack"] = {
114
+ type: "stdio",
115
+ command: "npx",
116
+ args: ["-y", "@modelcontextprotocol/server-slack@latest"],
117
+ env,
118
+ };
119
+ }
120
+ // Notion MCP — accept either NOTION_TOKEN or NOTION_API_KEY.
121
+ if (refs.has("notion")) {
122
+ const token = creds.NOTION_TOKEN || creds.NOTION_API_KEY;
123
+ if (token) {
124
+ auto["notion"] = {
125
+ type: "stdio",
126
+ command: "npx",
127
+ args: ["-y", "@notionhq/notion-mcp-server@latest"],
128
+ env: { NOTION_TOKEN: token },
129
+ };
130
+ }
131
+ }
132
+ // PagerDuty MCP — auth scheme is `Token token=<key>`.
133
+ if (refs.has("pagerduty") && creds.PAGERDUTY_API_TOKEN) {
134
+ auto["pagerduty"] = {
135
+ type: "http",
136
+ url: "https://mcp.pagerduty.com/mcp",
137
+ headers: { Authorization: `Token token=${creds.PAGERDUTY_API_TOKEN}` },
138
+ };
139
+ }
140
+ // Monday.com MCP.
141
+ if (refs.has("monday") && creds.MONDAY_TOKEN) {
142
+ auto["monday"] = {
143
+ type: "stdio",
144
+ command: "npx",
145
+ args: ["-y", "@mondaydotcomorg/monday-api-mcp@latest"],
146
+ env: { MONDAY_TOKEN: creds.MONDAY_TOKEN },
147
+ };
148
+ }
149
+ // Asana MCP.
150
+ if (refs.has("asana") && creds.ASANA_ACCESS_TOKEN) {
151
+ auto["asana"] = {
152
+ type: "stdio",
153
+ command: "npx",
154
+ args: ["-y", "asana-mcp@latest"],
155
+ env: { ASANA_ACCESS_TOKEN: creds.ASANA_ACCESS_TOKEN },
156
+ };
157
+ }
158
+ // User-supplied servers always win on key conflict.
159
+ return { ...auto, ...(opts.userMcpServers ?? {}) };
160
+ }
1
161
  /**
2
162
  * Auto-configure well-known MCP servers based on which providers
3
163
  * and workspace tools the user has enabled.
package/dist/mcp.test.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { buildAutoMcpServers } from "./mcp.js";
2
+ import { buildAutoMcpServers, buildSkillMcpServers } from "./mcp.js";
3
3
  // ─── Helpers ─────────────────────────────────────────────────────
4
4
  function cfg(overrides = {}) {
5
5
  return { credentials: {}, ...overrides };
@@ -345,3 +345,154 @@ describe("buildAutoMcpServers", () => {
345
345
  expect(result["slack"]).toBeUndefined();
346
346
  });
347
347
  });
348
+ // ─── buildSkillMcpServers (engine-driven) ────────────────────────────
349
+ describe("buildSkillMcpServers", () => {
350
+ it("returns empty when no skills referenced and no creds present", () => {
351
+ const result = buildSkillMcpServers({
352
+ referencedSkills: new Set(),
353
+ credentials: {},
354
+ });
355
+ expect(result).toEqual({});
356
+ });
357
+ it("does not wire MCP for a skill not referenced by the workflow", () => {
358
+ // Token is set, but the workflow doesn't ask for github → no MCP.
359
+ const result = buildSkillMcpServers({
360
+ referencedSkills: new Set(["linear"]),
361
+ credentials: { GITHUB_TOKEN: "ghp_abc", LINEAR_API_KEY: "lin_abc" },
362
+ });
363
+ expect(result["github"]).toBeUndefined();
364
+ expect(result["linear"]).toBeDefined();
365
+ });
366
+ it("does not wire MCP for a referenced skill whose creds are missing", () => {
367
+ const result = buildSkillMcpServers({
368
+ referencedSkills: new Set(["github", "linear"]),
369
+ credentials: { LINEAR_API_KEY: "lin_abc" }, // no GITHUB_TOKEN
370
+ });
371
+ expect(result["github"]).toBeUndefined();
372
+ expect(result["linear"]).toBeDefined();
373
+ });
374
+ it("wires github MCP when referenced and GITHUB_TOKEN present", () => {
375
+ const result = buildSkillMcpServers({
376
+ referencedSkills: new Set(["github"]),
377
+ credentials: { GITHUB_TOKEN: "ghp_abc" },
378
+ });
379
+ expect(result["github"]).toEqual({
380
+ type: "stdio",
381
+ command: "npx",
382
+ args: ["-y", "@modelcontextprotocol/server-github@latest"],
383
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_abc" },
384
+ });
385
+ });
386
+ it("wires linear MCP when referenced and LINEAR_API_KEY present", () => {
387
+ const result = buildSkillMcpServers({
388
+ referencedSkills: new Set(["linear"]),
389
+ credentials: { LINEAR_API_KEY: "lin_abc" },
390
+ });
391
+ expect(result["linear"]).toEqual({
392
+ type: "http",
393
+ url: "https://mcp.linear.app/mcp",
394
+ headers: { Authorization: "Bearer lin_abc" },
395
+ });
396
+ });
397
+ it("wires sentry MCP with SENTRY_HOST for self-hosted SENTRY_URL", () => {
398
+ const result = buildSkillMcpServers({
399
+ referencedSkills: new Set(["sentry"]),
400
+ credentials: { SENTRY_AUTH_TOKEN: "sntryu_abc", SENTRY_URL: "https://sentry.mycompany.com" },
401
+ });
402
+ expect(result["sentry"]?.env).toMatchObject({
403
+ SENTRY_ACCESS_TOKEN: "sntryu_abc",
404
+ SENTRY_HOST: "sentry.mycompany.com",
405
+ });
406
+ });
407
+ it("wires datadog MCP when both keys present", () => {
408
+ const result = buildSkillMcpServers({
409
+ referencedSkills: new Set(["datadog"]),
410
+ credentials: { DD_API_KEY: "dd_api", DD_APP_KEY: "dd_app" },
411
+ });
412
+ expect(result["datadog"]).toEqual({
413
+ type: "http",
414
+ url: "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp",
415
+ headers: { DD_API_KEY: "dd_api", DD_APPLICATION_KEY: "dd_app" },
416
+ });
417
+ });
418
+ it("does not wire datadog MCP when DD_APP_KEY is missing", () => {
419
+ const result = buildSkillMcpServers({
420
+ referencedSkills: new Set(["datadog"]),
421
+ credentials: { DD_API_KEY: "dd_api" },
422
+ });
423
+ expect(result["datadog"]).toBeUndefined();
424
+ });
425
+ it("wires slack MCP without an opt-in flag — workflow reference is the opt-in", () => {
426
+ const result = buildSkillMcpServers({
427
+ referencedSkills: new Set(["slack"]),
428
+ credentials: { SLACK_BOT_TOKEN: "xoxb-abc" },
429
+ });
430
+ expect(result["slack"]).toEqual({
431
+ type: "stdio",
432
+ command: "npx",
433
+ args: ["-y", "@modelcontextprotocol/server-slack@latest"],
434
+ env: { SLACK_BOT_TOKEN: "xoxb-abc" },
435
+ });
436
+ });
437
+ it("wires betterstack MCP only when the workflow references betterstack", () => {
438
+ // Token alone is not enough — must be referenced.
439
+ const noRef = buildSkillMcpServers({
440
+ referencedSkills: new Set(["sentry"]),
441
+ credentials: { BETTERSTACK_API_TOKEN: "bs_abc", SENTRY_AUTH_TOKEN: "sntry_x" },
442
+ });
443
+ expect(noRef["betterstack"]).toBeUndefined();
444
+ const withRef = buildSkillMcpServers({
445
+ referencedSkills: new Set(["betterstack"]),
446
+ credentials: { BETTERSTACK_API_TOKEN: "bs_abc" },
447
+ });
448
+ expect(withRef["betterstack"]).toEqual({
449
+ type: "http",
450
+ url: "https://mcp.betterstack.com",
451
+ headers: { Authorization: "Bearer bs_abc" },
452
+ });
453
+ });
454
+ it("user-supplied MCP servers win on key conflict", () => {
455
+ const custom = { type: "stdio", command: "my-github", args: [] };
456
+ const result = buildSkillMcpServers({
457
+ referencedSkills: new Set(["github"]),
458
+ credentials: { GITHUB_TOKEN: "ghp_abc" },
459
+ userMcpServers: { github: custom },
460
+ });
461
+ expect(result["github"]).toEqual(custom);
462
+ });
463
+ it("user-supplied servers can add MCPs alongside auto-wired ones", () => {
464
+ const my = { type: "http", url: "https://my.example.com" };
465
+ const result = buildSkillMcpServers({
466
+ referencedSkills: new Set(["github"]),
467
+ credentials: { GITHUB_TOKEN: "ghp_abc" },
468
+ userMcpServers: { "my-tool": my },
469
+ });
470
+ expect(result["github"]).toBeDefined();
471
+ expect(result["my-tool"]).toEqual(my);
472
+ });
473
+ it("silently skips skill IDs with no MCP catalog entry (e.g. notification)", () => {
474
+ const result = buildSkillMcpServers({
475
+ referencedSkills: new Set(["notification", "supabase", "github"]),
476
+ credentials: { GITHUB_TOKEN: "ghp_abc" },
477
+ });
478
+ expect(result["notification"]).toBeUndefined();
479
+ expect(result["supabase"]).toBeUndefined();
480
+ expect(result["github"]).toBeDefined();
481
+ });
482
+ it("wires multiple skills simultaneously", () => {
483
+ const result = buildSkillMcpServers({
484
+ referencedSkills: new Set(["github", "linear", "datadog", "slack"]),
485
+ credentials: {
486
+ GITHUB_TOKEN: "ghp_abc",
487
+ LINEAR_API_KEY: "lin_abc",
488
+ DD_API_KEY: "dd_api",
489
+ DD_APP_KEY: "dd_app",
490
+ SLACK_BOT_TOKEN: "xoxb-abc",
491
+ },
492
+ });
493
+ expect(result["github"]).toBeDefined();
494
+ expect(result["linear"]).toBeDefined();
495
+ expect(result["datadog"]).toBeDefined();
496
+ expect(result["slack"]).toBeDefined();
497
+ });
498
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Custom skill discovery — Node-only.
3
+ *
4
+ * Lives outside `skills/index.ts` so the registry stays browser-safe. Anything
5
+ * that needs `node:fs` / `node:path` / `process.cwd()` belongs here.
6
+ */
7
+ import type { Skill } from "../types.js";
8
+ /**
9
+ * Load custom skills from a `.claude/skills/` directory.
10
+ *
11
+ * Custom skills are instruction-only (no tools or config) — they provide
12
+ * guidance documents that the coding agent reads from the filesystem.
13
+ * Registering them here ensures workflow validation recognises the skill IDs
14
+ * so nodes referencing them don't produce spurious warnings.
15
+ */
16
+ export declare function loadCustomSkills(cwd?: string): Skill[];
17
+ /**
18
+ * From all builtins + custom repo skills, return only the skills that are usable.
19
+ * Built-in skills are filtered by env vars; custom skills are always included.
20
+ *
21
+ * Node-only: reads `.claude/skills/` from disk. Browser code should call
22
+ * `configuredBuiltinSkills` from `@sweny-ai/core/browser` instead.
23
+ */
24
+ export declare function configuredSkills(env?: Record<string, string | undefined>, cwd?: string): Skill[];
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Custom skill discovery — Node-only.
3
+ *
4
+ * Lives outside `skills/index.ts` so the registry stays browser-safe. Anything
5
+ * that needs `node:fs` / `node:path` / `process.cwd()` belongs here.
6
+ */
7
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import YAML from "yaml";
10
+ import { builtinSkills, isSkillConfigured } from "./index.js";
11
+ /**
12
+ * Load custom skills from a `.claude/skills/` directory.
13
+ *
14
+ * Custom skills are instruction-only (no tools or config) — they provide
15
+ * guidance documents that the coding agent reads from the filesystem.
16
+ * Registering them here ensures workflow validation recognises the skill IDs
17
+ * so nodes referencing them don't produce spurious warnings.
18
+ */
19
+ export function loadCustomSkills(cwd = process.cwd()) {
20
+ const skillsDir = join(cwd, ".claude", "skills");
21
+ if (!existsSync(skillsDir))
22
+ return [];
23
+ const skills = [];
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(skillsDir, { withFileTypes: true })
27
+ .filter((d) => d.isDirectory())
28
+ .map((d) => d.name);
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ for (const dirName of entries) {
34
+ const skillFile = join(skillsDir, dirName, "SKILL.md");
35
+ if (!existsSync(skillFile))
36
+ continue;
37
+ try {
38
+ const content = readFileSync(skillFile, "utf-8");
39
+ const frontmatter = parseFrontmatter(content);
40
+ if (!frontmatter?.name)
41
+ continue;
42
+ skills.push({
43
+ id: frontmatter.name,
44
+ name: frontmatter.name,
45
+ description: frontmatter.description ?? `Custom skill: ${frontmatter.name}`,
46
+ category: "general",
47
+ config: {},
48
+ tools: [],
49
+ });
50
+ }
51
+ catch {
52
+ // Skip malformed skill files
53
+ }
54
+ }
55
+ return skills;
56
+ }
57
+ /** Extract YAML frontmatter from a markdown file (between --- delimiters). */
58
+ function parseFrontmatter(content) {
59
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
60
+ if (!match)
61
+ return null;
62
+ try {
63
+ return YAML.parse(match[1]);
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ /**
70
+ * From all builtins + custom repo skills, return only the skills that are usable.
71
+ * Built-in skills are filtered by env vars; custom skills are always included.
72
+ *
73
+ * Node-only: reads `.claude/skills/` from disk. Browser code should call
74
+ * `configuredBuiltinSkills` from `@sweny-ai/core/browser` instead.
75
+ */
76
+ export function configuredSkills(env = process.env, cwd) {
77
+ const configured = builtinSkills.filter((s) => isSkillConfigured(s, env));
78
+ const custom = loadCustomSkills(cwd);
79
+ // Deduplicate: built-in skills take precedence over custom skills with the same ID
80
+ const ids = new Set(configured.map((s) => s.id));
81
+ for (const skill of custom) {
82
+ if (!ids.has(skill.id)) {
83
+ configured.push(skill);
84
+ ids.add(skill.id);
85
+ }
86
+ }
87
+ return configured;
88
+ }
@@ -2,6 +2,10 @@
2
2
  * Skill Registry
3
3
  *
4
4
  * All built-in skills + helpers to build a skill map from config.
5
+ *
6
+ * This module is browser-safe — no `node:fs`, no `node:path`, no `process`.
7
+ * Custom skill discovery (filesystem-based) lives in `./custom-loader.js`,
8
+ * which is Node-only and re-exported from `@sweny-ai/core` (the Node entry).
5
9
  */
6
10
  import type { Skill, SkillCategory } from "../types.js";
7
11
  import { github } from "./github.js";
@@ -14,15 +18,6 @@ import { notification } from "./notification.js";
14
18
  import { supabase } from "./supabase.js";
15
19
  export declare const builtinSkills: Skill[];
16
20
  export { github, linear, slack, sentry, datadog, betterstack, notification, supabase };
17
- /**
18
- * Load custom skills from a `.claude/skills/` directory.
19
- *
20
- * Custom skills are instruction-only (no tools or config) — they provide
21
- * guidance documents that the coding agent reads from the filesystem.
22
- * Registering them here ensures workflow validation recognises the skill IDs
23
- * so nodes referencing them don't produce spurious warnings.
24
- */
25
- export declare function loadCustomSkills(cwd?: string): Skill[];
26
21
  /**
27
22
  * Build a skill map from an array of skills.
28
23
  * Pass to `execute()` as the `skills` option.
@@ -47,10 +42,13 @@ export declare function allSkills(): Map<string, Skill>;
47
42
  */
48
43
  export declare function isSkillConfigured(skill: Skill, env?: Record<string, string | undefined>): boolean;
49
44
  /**
50
- * From all builtins + custom repo skills, return only the skills that are usable.
51
- * Built-in skills are filtered by env vars; custom skills are always included.
45
+ * Browser-safe variant of `configuredSkills`: returns only the *built-in* skills
46
+ * whose required env vars are present. Does not touch the filesystem.
47
+ *
48
+ * Node callers should use `configuredSkills` from `@sweny-ai/core`, which also
49
+ * loads custom skills from `.claude/skills/`.
52
50
  */
53
- export declare function configuredSkills(env?: Record<string, string | undefined>, cwd?: string): Skill[];
51
+ export declare function configuredBuiltinSkills(env?: Record<string, string | undefined>): Skill[];
54
52
  /**
55
53
  * Validate that a workflow's skill requirements are satisfiable.
56
54
  *
@@ -2,10 +2,11 @@
2
2
  * Skill Registry
3
3
  *
4
4
  * All built-in skills + helpers to build a skill map from config.
5
+ *
6
+ * This module is browser-safe — no `node:fs`, no `node:path`, no `process`.
7
+ * Custom skill discovery (filesystem-based) lives in `./custom-loader.js`,
8
+ * which is Node-only and re-exported from `@sweny-ai/core` (the Node entry).
5
9
  */
6
- import { readdirSync, readFileSync, existsSync } from "node:fs";
7
- import { join } from "node:path";
8
- import YAML from "yaml";
9
10
  import { github } from "./github.js";
10
11
  import { linear } from "./linear.js";
11
12
  import { slack } from "./slack.js";
@@ -17,65 +18,6 @@ import { supabase } from "./supabase.js";
17
18
  // ─── Built-in skill catalog ─────────────────────────────────────
18
19
  export const builtinSkills = [github, linear, slack, sentry, datadog, betterstack, notification, supabase];
19
20
  export { github, linear, slack, sentry, datadog, betterstack, notification, supabase };
20
- // ─── Custom skill discovery ─────────────────────────────────────
21
- /**
22
- * Load custom skills from a `.claude/skills/` directory.
23
- *
24
- * Custom skills are instruction-only (no tools or config) — they provide
25
- * guidance documents that the coding agent reads from the filesystem.
26
- * Registering them here ensures workflow validation recognises the skill IDs
27
- * so nodes referencing them don't produce spurious warnings.
28
- */
29
- export function loadCustomSkills(cwd = process.cwd()) {
30
- const skillsDir = join(cwd, ".claude", "skills");
31
- if (!existsSync(skillsDir))
32
- return [];
33
- const skills = [];
34
- let entries;
35
- try {
36
- entries = readdirSync(skillsDir, { withFileTypes: true })
37
- .filter((d) => d.isDirectory())
38
- .map((d) => d.name);
39
- }
40
- catch {
41
- return [];
42
- }
43
- for (const dirName of entries) {
44
- const skillFile = join(skillsDir, dirName, "SKILL.md");
45
- if (!existsSync(skillFile))
46
- continue;
47
- try {
48
- const content = readFileSync(skillFile, "utf-8");
49
- const frontmatter = parseFrontmatter(content);
50
- if (!frontmatter?.name)
51
- continue;
52
- skills.push({
53
- id: frontmatter.name,
54
- name: frontmatter.name,
55
- description: frontmatter.description ?? `Custom skill: ${frontmatter.name}`,
56
- category: "general",
57
- config: {},
58
- tools: [],
59
- });
60
- }
61
- catch {
62
- // Skip malformed skill files
63
- }
64
- }
65
- return skills;
66
- }
67
- /** Extract YAML frontmatter from a markdown file (between --- delimiters). */
68
- function parseFrontmatter(content) {
69
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
70
- if (!match)
71
- return null;
72
- try {
73
- return YAML.parse(match[1]);
74
- }
75
- catch {
76
- return null;
77
- }
78
- }
79
21
  // ─── Registry helpers ───────────────────────────────────────────
80
22
  /**
81
23
  * Build a skill map from an array of skills.
@@ -110,7 +52,7 @@ export function allSkills() {
110
52
  * Optional fields are validated by individual tool handlers at call time,
111
53
  * so missing optional config never disqualifies the skill.
112
54
  */
113
- export function isSkillConfigured(skill, env = process.env) {
55
+ export function isSkillConfigured(skill, env = {}) {
114
56
  for (const field of Object.values(skill.config)) {
115
57
  if (field.required && field.env && !env[field.env])
116
58
  return false;
@@ -118,21 +60,14 @@ export function isSkillConfigured(skill, env = process.env) {
118
60
  return true;
119
61
  }
120
62
  /**
121
- * From all builtins + custom repo skills, return only the skills that are usable.
122
- * Built-in skills are filtered by env vars; custom skills are always included.
63
+ * Browser-safe variant of `configuredSkills`: returns only the *built-in* skills
64
+ * whose required env vars are present. Does not touch the filesystem.
65
+ *
66
+ * Node callers should use `configuredSkills` from `@sweny-ai/core`, which also
67
+ * loads custom skills from `.claude/skills/`.
123
68
  */
124
- export function configuredSkills(env = process.env, cwd) {
125
- const configured = builtinSkills.filter((s) => isSkillConfigured(s, env));
126
- const custom = loadCustomSkills(cwd);
127
- // Deduplicate: built-in skills take precedence over custom skills with the same ID
128
- const ids = new Set(configured.map((s) => s.id));
129
- for (const skill of custom) {
130
- if (!ids.has(skill.id)) {
131
- configured.push(skill);
132
- ids.add(skill.id);
133
- }
134
- }
135
- return configured;
69
+ export function configuredBuiltinSkills(env = {}) {
70
+ return builtinSkills.filter((s) => isSkillConfigured(s, env));
136
71
  }
137
72
  export function validateWorkflowSkills(workflow, available) {
138
73
  const configured = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"