@sweny-ai/core 0.1.37 → 0.1.39
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/__tests__/claude.test.js +99 -0
- package/dist/__tests__/skills.test.js +2 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +2 -2
- package/dist/claude.js +13 -0
- package/dist/cli/e2e.js +2 -1
- package/dist/cli/main.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/skills/custom-loader.d.ts +24 -0
- package/dist/skills/custom-loader.js +88 -0
- package/dist/skills/index.d.ts +10 -12
- package/dist/skills/index.js +12 -77
- package/package.json +1 -1
|
@@ -250,6 +250,105 @@ describe("ClaudeClient", () => {
|
|
|
250
250
|
expect(result.status).toBe("failed");
|
|
251
251
|
expect(result.data.error).toContain("Too many turns");
|
|
252
252
|
});
|
|
253
|
+
// terminal_reason was added in @anthropic-ai/claude-agent-sdk v0.2.91.
|
|
254
|
+
// When the turn budget is exhausted the SDK now emits subtype='success'
|
|
255
|
+
// alongside terminal_reason='max_turns' instead of the old error subtype,
|
|
256
|
+
// so the claude.ts handler has to look at both fields or silently accept
|
|
257
|
+
// truncated output as a success.
|
|
258
|
+
describe("terminal_reason handling (claude-agent-sdk v0.2.91+)", () => {
|
|
259
|
+
it("returns failed when terminal_reason is max_turns", async () => {
|
|
260
|
+
mockQuery.mockReturnValueOnce(makeStream([
|
|
261
|
+
{
|
|
262
|
+
type: "result",
|
|
263
|
+
subtype: "success",
|
|
264
|
+
result: "partial output",
|
|
265
|
+
terminal_reason: "max_turns",
|
|
266
|
+
},
|
|
267
|
+
]));
|
|
268
|
+
const client = new ClaudeClient();
|
|
269
|
+
const result = await client.run({ instruction: "x", context: {}, tools: [] });
|
|
270
|
+
expect(result.status).toBe("failed");
|
|
271
|
+
expect(result.data.error).toContain("max_turns");
|
|
272
|
+
expect(result.data.error).toContain("terminated early");
|
|
273
|
+
});
|
|
274
|
+
it("returns failed for any non-completed terminal_reason", async () => {
|
|
275
|
+
// Future SDK versions may add new terminal reasons (aborted_tools,
|
|
276
|
+
// blocking_limit, etc.) — treat anything that isn't 'completed' as a
|
|
277
|
+
// failure rather than assume it's recoverable.
|
|
278
|
+
mockQuery.mockReturnValueOnce(makeStream([
|
|
279
|
+
{
|
|
280
|
+
type: "result",
|
|
281
|
+
subtype: "success",
|
|
282
|
+
result: "partial",
|
|
283
|
+
terminal_reason: "aborted_tools",
|
|
284
|
+
},
|
|
285
|
+
]));
|
|
286
|
+
const client = new ClaudeClient();
|
|
287
|
+
const result = await client.run({ instruction: "x", context: {}, tools: [] });
|
|
288
|
+
expect(result.status).toBe("failed");
|
|
289
|
+
expect(result.data.error).toContain("aborted_tools");
|
|
290
|
+
});
|
|
291
|
+
it("returns success when terminal_reason is 'completed'", async () => {
|
|
292
|
+
mockQuery.mockReturnValueOnce(makeStream([
|
|
293
|
+
{
|
|
294
|
+
type: "result",
|
|
295
|
+
subtype: "success",
|
|
296
|
+
result: '{"ok": true}',
|
|
297
|
+
terminal_reason: "completed",
|
|
298
|
+
},
|
|
299
|
+
]));
|
|
300
|
+
const client = new ClaudeClient();
|
|
301
|
+
const result = await client.run({ instruction: "x", context: {}, tools: [] });
|
|
302
|
+
expect(result.status).toBe("success");
|
|
303
|
+
expect(result.data.ok).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
it("returns success when terminal_reason is absent (pre-v0.2.91 SDK)", async () => {
|
|
306
|
+
// Old SDKs never set terminal_reason. The guard must not treat an
|
|
307
|
+
// absent field as a failure or every caller would break on downgrade.
|
|
308
|
+
mockQuery.mockReturnValueOnce(makeStream([
|
|
309
|
+
{
|
|
310
|
+
type: "result",
|
|
311
|
+
subtype: "success",
|
|
312
|
+
result: '{"done": true}',
|
|
313
|
+
// terminal_reason deliberately omitted
|
|
314
|
+
},
|
|
315
|
+
]));
|
|
316
|
+
const client = new ClaudeClient();
|
|
317
|
+
const result = await client.run({ instruction: "x", context: {}, tools: [] });
|
|
318
|
+
expect(result.status).toBe("success");
|
|
319
|
+
expect(result.data.done).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
it("preserves toolCalls when failing on terminal_reason", async () => {
|
|
322
|
+
// A max_turns failure should still surface any tool calls that
|
|
323
|
+
// happened before the budget ran out, so debuggers can see what
|
|
324
|
+
// Claude was trying to do.
|
|
325
|
+
mockQuery.mockReturnValueOnce(makeStream([
|
|
326
|
+
{
|
|
327
|
+
type: "assistant",
|
|
328
|
+
message: {
|
|
329
|
+
content: [
|
|
330
|
+
{
|
|
331
|
+
type: "tool_use",
|
|
332
|
+
name: "search_code",
|
|
333
|
+
input: { query: "foo" },
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: "result",
|
|
340
|
+
subtype: "success",
|
|
341
|
+
result: "partial",
|
|
342
|
+
terminal_reason: "max_turns",
|
|
343
|
+
},
|
|
344
|
+
]));
|
|
345
|
+
const client = new ClaudeClient();
|
|
346
|
+
const result = await client.run({ instruction: "x", context: {}, tools: [] });
|
|
347
|
+
expect(result.status).toBe("failed");
|
|
348
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
349
|
+
expect(result.toolCalls[0].tool).toBe("search_code");
|
|
350
|
+
});
|
|
351
|
+
});
|
|
253
352
|
it("passes model option to query", async () => {
|
|
254
353
|
mockQuery.mockReturnValueOnce(makeStream([{ type: "result", subtype: "success", result: "ok" }]));
|
|
255
354
|
const client = new ClaudeClient({ model: "claude-sonnet-4-20250514" });
|
|
@@ -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 {
|
|
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,
|
|
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,
|
|
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/claude.js
CHANGED
|
@@ -112,6 +112,19 @@ export class ClaudeClient {
|
|
|
112
112
|
else if (message.type === "result") {
|
|
113
113
|
const resultMsg = message;
|
|
114
114
|
if (resultMsg.subtype === "success" && "result" in resultMsg) {
|
|
115
|
+
// terminal_reason was added in @anthropic-ai/claude-agent-sdk v0.2.91.
|
|
116
|
+
// When the turn budget is exhausted the SDK still emits subtype='success'
|
|
117
|
+
// but sets terminal_reason='max_turns'. Treat anything other than
|
|
118
|
+
// 'completed' (or absent) as a failure so callers are not silently fed
|
|
119
|
+
// incomplete output.
|
|
120
|
+
const terminalReason = resultMsg.terminal_reason;
|
|
121
|
+
if (terminalReason && terminalReason !== "completed") {
|
|
122
|
+
return {
|
|
123
|
+
status: "failed",
|
|
124
|
+
data: { error: `Claude query terminated early: ${terminalReason}` },
|
|
125
|
+
toolCalls,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
115
128
|
response = resultMsg.result;
|
|
116
129
|
}
|
|
117
130
|
else if ("errors" in resultMsg) {
|
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
|
|
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,7 +10,8 @@ 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
|
|
13
|
+
import { createSkillMap } from "../skills/index.js";
|
|
14
|
+
import { configuredSkills } from "../skills/custom-loader.js";
|
|
14
15
|
import { buildAutoMcpServers, buildProviderContext } from "../mcp.js";
|
|
15
16
|
import { loadAdditionalContext } from "../templates.js";
|
|
16
17
|
import { validateWorkflow as validateWorkflowSchema } from "../schema.js";
|
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,
|
|
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,
|
|
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
|
|
@@ -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
|
+
}
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/skills/index.js
CHANGED
|
@@ -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 =
|
|
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
|
-
*
|
|
122
|
-
*
|
|
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
|
|
125
|
-
|
|
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 = [];
|