@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.
@@ -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 { 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/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, 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,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, configuredSkills } from "../skills/index.js";
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, 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
@@ -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.37",
3
+ "version": "0.1.39",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"