@sweny-ai/core 0.1.51 → 0.1.53

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.
Files changed (40) hide show
  1. package/dist/__tests__/publish.test.js +146 -0
  2. package/dist/__tests__/schema.test.js +156 -1
  3. package/dist/__tests__/skills.test.js +135 -4
  4. package/dist/__tests__/templates.test.d.ts +1 -0
  5. package/dist/__tests__/templates.test.js +49 -0
  6. package/dist/__tests__/version-consistency.test.js +7 -7
  7. package/dist/cli/config-file.d.ts +1 -1
  8. package/dist/cli/config-file.js +1 -1
  9. package/dist/cli/credentials.d.ts +1 -1
  10. package/dist/cli/credentials.js +1 -1
  11. package/dist/cli/credentials.test.js +1 -1
  12. package/dist/cli/e2e.d.ts +5 -1
  13. package/dist/cli/e2e.js +7 -3
  14. package/dist/cli/e2e.test.js +46 -1
  15. package/dist/cli/main.js +21 -7
  16. package/dist/cli/new.d.ts +101 -0
  17. package/dist/cli/new.js +670 -0
  18. package/dist/cli/new.test.d.ts +1 -0
  19. package/dist/cli/{init.test.js → new.test.js} +216 -78
  20. package/dist/cli/publish.d.ts +34 -0
  21. package/dist/cli/publish.js +368 -0
  22. package/dist/cli/templates.d.ts +13 -0
  23. package/dist/cli/templates.js +181 -0
  24. package/dist/executor.js +63 -20
  25. package/dist/executor.test.js +233 -0
  26. package/dist/index.d.ts +5 -5
  27. package/dist/index.js +3 -3
  28. package/dist/mcp.d.ts +2 -0
  29. package/dist/mcp.js +8 -0
  30. package/dist/mcp.test.js +67 -0
  31. package/dist/schema.d.ts +428 -18
  32. package/dist/schema.js +71 -4
  33. package/dist/skills/custom-loader.d.ts +12 -12
  34. package/dist/skills/custom-loader.js +105 -50
  35. package/dist/types.d.ts +18 -1
  36. package/dist/workflows/browser.js +3 -3
  37. package/package.json +10 -2
  38. package/dist/cli/init.d.ts +0 -53
  39. package/dist/cli/init.js +0 -521
  40. /package/dist/{cli/init.test.d.ts → __tests__/publish.test.d.ts} +0 -0
package/dist/executor.js CHANGED
@@ -21,7 +21,11 @@ import { consoleLogger } from "./types.js";
21
21
  * - `trace`: full ordered execution trace including loops and routing decisions
22
22
  */
23
23
  export async function execute(workflow, input, options) {
24
- const { skills, claude, observer } = options;
24
+ const { claude, observer } = options;
25
+ // Merge inline workflow skills into the skill map so they resolve at runtime.
26
+ // Inline skills (instruction/mcp only) become Skill objects with empty tools/config.
27
+ // The caller's skill map takes precedence — inline skills only fill gaps.
28
+ const skills = mergeInlineSkills(options.skills, workflow.skills);
25
29
  const config = resolveConfig(skills, options.config);
26
30
  const logger = options.logger ?? consoleLogger;
27
31
  const results = new Map();
@@ -41,11 +45,13 @@ export async function execute(workflow, input, options) {
41
45
  logger.info(`→ ${node.name}`, { node: currentId });
42
46
  // Gather tools from the node's skills
43
47
  const tools = resolveTools(node.skills, skills);
48
+ const skillInstructions = resolveSkillInstructions(node.skills, skills);
44
49
  // Runtime guard: if this node declares skills but none resolved, the node
45
50
  // cannot do its job (e.g. "create a Linear issue" with no linear skill).
46
51
  // The startup validate() warns about this possibility, but only throw when
47
52
  // the node is actually reached — unreachable nodes with missing skills are fine.
48
- if (node.skills.length > 0 && tools.length === 0) {
53
+ // Instruction-only skills (no tools) are valid — they inject context into the prompt.
54
+ if (node.skills.length > 0 && tools.length === 0 && skillInstructions.length === 0) {
49
55
  throw new Error(`Node "${currentId}" requires skills [${node.skills.join(", ")}] but none are configured. ` +
50
56
  `Set the required environment variables and try again.`);
51
57
  }
@@ -66,7 +72,7 @@ export async function execute(workflow, input, options) {
66
72
  },
67
73
  }));
68
74
  // Prepend rules and context to instruction if provided
69
- const instruction = buildNodeInstruction(node.instruction, input);
75
+ const instruction = buildNodeInstruction(node.instruction, input, skillInstructions);
70
76
  // Run Claude on this node
71
77
  const result = await claude.run({
72
78
  instruction,
@@ -116,30 +122,35 @@ export async function execute(workflow, input, options) {
116
122
  * Rules get "You MUST follow" framing; context gets "Background" framing.
117
123
  * Falls back to legacy `additionalContext` if rules/context aren't set.
118
124
  */
119
- function buildNodeInstruction(baseInstruction, input) {
125
+ function buildNodeInstruction(baseInstruction, input, skillInstructions) {
120
126
  const inp = input;
121
- if (!inp)
122
- return baseInstruction;
123
127
  const sections = [];
124
- // New structured format
125
- const rules = typeof inp.rules === "string" && inp.rules ? inp.rules : "";
126
- const context = typeof inp.context === "string" && inp.context ? inp.context : "";
127
- if (rules) {
128
- sections.push(`## Rules — You MUST Follow These\n\n${rules}`);
129
- }
130
- if (context) {
131
- sections.push(`## Background Context\n\n${context}`);
128
+ if (inp) {
129
+ const rules = typeof inp.rules === "string" && inp.rules ? inp.rules : "";
130
+ const context = typeof inp.context === "string" && inp.context ? inp.context : "";
131
+ if (rules) {
132
+ sections.push(`## Rules — You MUST Follow These\n\n${rules}`);
133
+ }
134
+ if (context) {
135
+ sections.push(`## Background Context\n\n${context}`);
136
+ }
137
+ // Legacy fallback
138
+ if (sections.length === 0) {
139
+ const legacy = typeof inp.additionalContext === "string" ? inp.additionalContext : "";
140
+ if (legacy) {
141
+ sections.push(`## Additional Context & Rules\n\n${legacy}`);
142
+ }
143
+ }
132
144
  }
133
- // Legacy fallback
134
- if (sections.length === 0) {
135
- const legacy = typeof inp.additionalContext === "string" ? inp.additionalContext : "";
136
- if (legacy) {
137
- sections.push(`## Additional Context & Rules\n\n${legacy}`);
145
+ // Skill instructions — injected between context and base instruction
146
+ if (skillInstructions && skillInstructions.length > 0) {
147
+ for (const { name, instruction } of skillInstructions) {
148
+ sections.push(`## Skill: ${name}\n\n${instruction}`);
138
149
  }
139
150
  }
140
151
  if (sections.length === 0)
141
152
  return baseInstruction;
142
- return `${sections.join("\n\n")}\n\n---\n\n${baseInstruction}`;
153
+ return `${sections.join("\n\n---\n\n")}\n\n---\n\n${baseInstruction}`;
143
154
  }
144
155
  /**
145
156
  * Call observer without letting exceptions crash the workflow.
@@ -157,12 +168,44 @@ function safeObserve(observer, event, logger) {
157
168
  (logger ?? consoleLogger).warn(`Observer error (non-fatal): ${err.message}`);
158
169
  }
159
170
  }
171
+ /**
172
+ * Merge inline workflow skill definitions into the skill map.
173
+ * Inline skills (from workflow.skills) become Skill objects with empty tools/config.
174
+ * The caller's skill map takes precedence — inline skills only fill gaps.
175
+ */
176
+ function mergeInlineSkills(skills, inlineSkills) {
177
+ if (!inlineSkills || Object.keys(inlineSkills).length === 0)
178
+ return skills;
179
+ const merged = new Map(skills);
180
+ for (const [id, def] of Object.entries(inlineSkills)) {
181
+ if (merged.has(id))
182
+ continue; // caller-provided skill takes precedence
183
+ merged.set(id, {
184
+ id,
185
+ name: def.name ?? id,
186
+ description: def.description ?? `Inline skill: ${id}`,
187
+ category: "general",
188
+ config: {},
189
+ tools: [],
190
+ instruction: def.instruction,
191
+ mcp: def.mcp,
192
+ });
193
+ }
194
+ return merged;
195
+ }
160
196
  function resolveTools(skillIds, skills) {
161
197
  return skillIds
162
198
  .map((id) => skills.get(id))
163
199
  .filter((s) => s != null)
164
200
  .flatMap((s) => s.tools);
165
201
  }
202
+ /** Collect instruction strings from skills that have them, in array order. */
203
+ function resolveSkillInstructions(skillIds, skills) {
204
+ return skillIds
205
+ .map((id) => skills.get(id))
206
+ .filter((s) => s != null && s.instruction != null)
207
+ .map((s) => ({ name: s.name, instruction: s.instruction }));
208
+ }
166
209
  /**
167
210
  * Resolve config values: check explicit overrides first, then env vars.
168
211
  * Throws if a required field is missing.
@@ -313,3 +313,236 @@ describe("file skill", () => {
313
313
  expect(existsSync(path.join(outputDir, "deep/nested/file.json"))).toBe(true);
314
314
  });
315
315
  });
316
+ // ─── Skill instruction injection tests ──────────────────────────
317
+ describe("skill instruction injection", () => {
318
+ let outputDir;
319
+ beforeEach(() => {
320
+ outputDir = freshDir("skill-inject");
321
+ });
322
+ it("injects skill instructions into the prompt", async () => {
323
+ const instructionSkill = {
324
+ id: "code-standards",
325
+ name: "Code Standards",
326
+ description: "Team conventions",
327
+ category: "general",
328
+ config: {},
329
+ tools: [],
330
+ instruction: "Always use camelCase for variable names.",
331
+ };
332
+ const workflow = {
333
+ id: "test-inject",
334
+ name: "Inject Test",
335
+ description: "",
336
+ entry: "step",
337
+ nodes: {
338
+ step: {
339
+ name: "Do Work",
340
+ instruction: "Write some code.",
341
+ skills: ["code-standards"],
342
+ },
343
+ },
344
+ edges: [],
345
+ };
346
+ let capturedInstruction = "";
347
+ const claude = {
348
+ async run(opts) {
349
+ capturedInstruction = opts.instruction;
350
+ return { status: "success", data: {}, toolCalls: [] };
351
+ },
352
+ async evaluate() {
353
+ return "";
354
+ },
355
+ };
356
+ await execute(workflow, {}, {
357
+ skills: createSkillMap([instructionSkill]),
358
+ claude,
359
+ config: {},
360
+ });
361
+ expect(capturedInstruction).toContain("## Skill: Code Standards");
362
+ expect(capturedInstruction).toContain("Always use camelCase for variable names.");
363
+ expect(capturedInstruction).toContain("Write some code.");
364
+ });
365
+ it("injects multiple skill instructions in array order", async () => {
366
+ const skillA = {
367
+ id: "skill-a",
368
+ name: "Skill A",
369
+ description: "First",
370
+ category: "general",
371
+ config: {},
372
+ tools: [],
373
+ instruction: "AAA instruction",
374
+ };
375
+ const skillB = {
376
+ id: "skill-b",
377
+ name: "Skill B",
378
+ description: "Second",
379
+ category: "general",
380
+ config: {},
381
+ tools: [],
382
+ instruction: "BBB instruction",
383
+ };
384
+ const workflow = {
385
+ id: "test-multi",
386
+ name: "Multi Inject",
387
+ description: "",
388
+ entry: "step",
389
+ nodes: {
390
+ step: {
391
+ name: "Work",
392
+ instruction: "Do the work.",
393
+ skills: ["skill-a", "skill-b"],
394
+ },
395
+ },
396
+ edges: [],
397
+ };
398
+ let capturedInstruction = "";
399
+ const claude = {
400
+ async run(opts) {
401
+ capturedInstruction = opts.instruction;
402
+ return { status: "success", data: {}, toolCalls: [] };
403
+ },
404
+ async evaluate() {
405
+ return "";
406
+ },
407
+ };
408
+ await execute(workflow, {}, {
409
+ skills: createSkillMap([skillA, skillB]),
410
+ claude,
411
+ config: {},
412
+ });
413
+ const posA = capturedInstruction.indexOf("AAA instruction");
414
+ const posB = capturedInstruction.indexOf("BBB instruction");
415
+ const posBase = capturedInstruction.indexOf("Do the work.");
416
+ expect(posA).toBeLessThan(posB);
417
+ expect(posB).toBeLessThan(posBase);
418
+ });
419
+ it("allows instruction-only nodes (no tools, no error)", async () => {
420
+ const instructionSkill = {
421
+ id: "rubric",
422
+ name: "Evaluation Rubric",
423
+ description: "Scoring criteria",
424
+ category: "general",
425
+ config: {},
426
+ tools: [],
427
+ instruction: "Score quality 1-5.",
428
+ };
429
+ const workflow = {
430
+ id: "test-notools",
431
+ name: "No Tools",
432
+ description: "",
433
+ entry: "judge",
434
+ nodes: {
435
+ judge: {
436
+ name: "Judge",
437
+ instruction: "Evaluate the output.",
438
+ skills: ["rubric"],
439
+ },
440
+ },
441
+ edges: [],
442
+ };
443
+ const claude = {
444
+ async run() {
445
+ return { status: "success", data: { score: 4 }, toolCalls: [] };
446
+ },
447
+ async evaluate() {
448
+ return "";
449
+ },
450
+ };
451
+ // Should NOT throw "none are configured"
452
+ const { results } = await execute(workflow, {}, {
453
+ skills: createSkillMap([instructionSkill]),
454
+ claude,
455
+ config: {},
456
+ });
457
+ expect(results.get("judge")?.status).toBe("success");
458
+ });
459
+ it("wires inline workflow.skills into the executor automatically", async () => {
460
+ const workflow = {
461
+ id: "test-inline",
462
+ name: "Inline Skills",
463
+ description: "",
464
+ entry: "review",
465
+ nodes: {
466
+ review: {
467
+ name: "Review",
468
+ instruction: "Review the code.",
469
+ skills: ["my-rubric"],
470
+ },
471
+ },
472
+ edges: [],
473
+ skills: {
474
+ "my-rubric": {
475
+ name: "My Rubric",
476
+ instruction: "Check for security issues first, then correctness.",
477
+ },
478
+ },
479
+ };
480
+ let capturedInstruction = "";
481
+ const claude = {
482
+ async run(opts) {
483
+ capturedInstruction = opts.instruction;
484
+ return { status: "success", data: {}, toolCalls: [] };
485
+ },
486
+ async evaluate() {
487
+ return "";
488
+ },
489
+ };
490
+ // No "my-rubric" in the skill map — executor must pull it from workflow.skills
491
+ const { results } = await execute(workflow, {}, {
492
+ skills: createSkillMap([]),
493
+ claude,
494
+ config: {},
495
+ });
496
+ expect(results.get("review")?.status).toBe("success");
497
+ expect(capturedInstruction).toContain("## Skill: My Rubric");
498
+ expect(capturedInstruction).toContain("Check for security issues first");
499
+ });
500
+ it("caller-provided skills take precedence over inline workflow.skills", async () => {
501
+ const callerSkill = {
502
+ id: "shared",
503
+ name: "Caller Version",
504
+ description: "From caller",
505
+ category: "general",
506
+ config: {},
507
+ tools: [],
508
+ instruction: "Caller instruction wins.",
509
+ };
510
+ const workflow = {
511
+ id: "test-precedence",
512
+ name: "Precedence",
513
+ description: "",
514
+ entry: "step",
515
+ nodes: {
516
+ step: {
517
+ name: "Step",
518
+ instruction: "Do work.",
519
+ skills: ["shared"],
520
+ },
521
+ },
522
+ edges: [],
523
+ skills: {
524
+ shared: {
525
+ name: "Inline Version",
526
+ instruction: "Inline instruction should NOT appear.",
527
+ },
528
+ },
529
+ };
530
+ let capturedInstruction = "";
531
+ const claude = {
532
+ async run(opts) {
533
+ capturedInstruction = opts.instruction;
534
+ return { status: "success", data: {}, toolCalls: [] };
535
+ },
536
+ async evaluate() {
537
+ return "";
538
+ },
539
+ };
540
+ await execute(workflow, {}, {
541
+ skills: createSkillMap([callerSkill]),
542
+ claude,
543
+ config: {},
544
+ });
545
+ expect(capturedInstruction).toContain("Caller instruction wins.");
546
+ expect(capturedInstruction).not.toContain("Inline instruction should NOT appear.");
547
+ });
548
+ });
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, } from "./types.js";
24
+ export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, } from "./types.js";
25
25
  export { consoleLogger } from "./types.js";
26
26
  export { execute } from "./executor.js";
27
27
  export type { ExecuteOptions } from "./executor.js";
@@ -29,11 +29,11 @@ export { ClaudeClient } from "./claude.js";
29
29
  export type { ClaudeClientOptions } from "./claude.js";
30
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";
33
- export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
32
+ export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
33
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
34
34
  export type { WorkflowError } from "./schema.js";
35
- export { buildAutoMcpServers, buildProviderContext } from "./mcp.js";
36
- export type { ProviderContextOptions } from "./mcp.js";
35
+ export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
36
+ export type { ProviderContextOptions, SkillMcpOptions } from "./mcp.js";
37
37
  export type { McpServerConfig, McpAutoConfig } from "./types.js";
38
38
  export { buildWorkflow, refineWorkflow } from "./workflow-builder.js";
39
39
  export type { BuildWorkflowOptions } from "./workflow-builder.js";
package/dist/index.js CHANGED
@@ -29,11 +29,11 @@ export { ClaudeClient } from "./claude.js";
29
29
  // Skills
30
30
  export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
31
31
  // Node-only skill discovery (filesystem-based)
32
- export { loadCustomSkills, configuredSkills } from "./skills/custom-loader.js";
32
+ export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
33
33
  // Schema & validation
34
- export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
34
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
35
35
  // MCP auto-injection
36
- export { buildAutoMcpServers, buildProviderContext } from "./mcp.js";
36
+ export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
37
37
  // Workflow builder
38
38
  export { buildWorkflow, refineWorkflow } from "./workflow-builder.js";
39
39
  // Templates
package/dist/mcp.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface SkillMcpOptions {
6
6
  credentials: Record<string, string>;
7
7
  /** User-supplied MCP servers — always win on key conflict. */
8
8
  userMcpServers?: Record<string, McpServerConfig>;
9
+ /** MCP servers declared by custom skills. Only wired when the skill is referenced. */
10
+ skillMcpServers?: Record<string, McpServerConfig>;
9
11
  }
10
12
  /**
11
13
  * Build MCP server configs for the skills a workflow references.
package/dist/mcp.js CHANGED
@@ -155,6 +155,14 @@ export function buildSkillMcpServers(opts) {
155
155
  env: { ASANA_ACCESS_TOKEN: creds.ASANA_ACCESS_TOKEN },
156
156
  };
157
157
  }
158
+ // Skill-declared MCP servers — only include for referenced skills.
159
+ if (opts.skillMcpServers) {
160
+ for (const [skillId, mcpConfig] of Object.entries(opts.skillMcpServers)) {
161
+ if (refs.has(skillId) && !auto[skillId]) {
162
+ auto[skillId] = mcpConfig;
163
+ }
164
+ }
165
+ }
158
166
  // User-supplied servers always win on key conflict.
159
167
  return { ...auto, ...(opts.userMcpServers ?? {}) };
160
168
  }
package/dist/mcp.test.js CHANGED
@@ -665,4 +665,71 @@ describe("buildSkillMcpServers", () => {
665
665
  });
666
666
  expect(result["betterstack"]?.headers?.Authorization).toBe("Bearer primary");
667
667
  });
668
+ // ── Skill-declared MCP servers ──────────────────────────────────────
669
+ it("includes skill-declared MCP servers", () => {
670
+ const result = buildSkillMcpServers({
671
+ referencedSkills: new Set(["our-crm"]),
672
+ credentials: {},
673
+ skillMcpServers: {
674
+ "our-crm": {
675
+ type: "stdio",
676
+ command: "npx",
677
+ args: ["-y", "@company/crm-server"],
678
+ env: { API_KEY: "test-key" },
679
+ },
680
+ },
681
+ });
682
+ expect(result["our-crm"]).toBeDefined();
683
+ expect(result["our-crm"].command).toBe("npx");
684
+ });
685
+ it("skill-declared MCP is only included when skill is referenced", () => {
686
+ const result = buildSkillMcpServers({
687
+ referencedSkills: new Set(["github"]),
688
+ credentials: { GITHUB_TOKEN: "ghp_test" },
689
+ skillMcpServers: {
690
+ "our-crm": {
691
+ type: "stdio",
692
+ command: "npx",
693
+ args: ["-y", "@company/crm-server"],
694
+ },
695
+ },
696
+ });
697
+ expect(result["github"]).toBeDefined();
698
+ expect(result["our-crm"]).toBeUndefined();
699
+ });
700
+ it("user-supplied servers win over skill-declared", () => {
701
+ const result = buildSkillMcpServers({
702
+ referencedSkills: new Set(["our-crm"]),
703
+ credentials: {},
704
+ skillMcpServers: {
705
+ "our-crm": {
706
+ type: "stdio",
707
+ command: "npx",
708
+ args: ["-y", "@company/crm-server"],
709
+ },
710
+ },
711
+ userMcpServers: {
712
+ "our-crm": {
713
+ type: "http",
714
+ url: "https://override.example.com",
715
+ },
716
+ },
717
+ });
718
+ expect(result["our-crm"].url).toBe("https://override.example.com");
719
+ });
720
+ it("skill-declared and auto-wired MCPs coexist", () => {
721
+ const result = buildSkillMcpServers({
722
+ referencedSkills: new Set(["github", "our-crm"]),
723
+ credentials: { GITHUB_TOKEN: "ghp_test" },
724
+ skillMcpServers: {
725
+ "our-crm": {
726
+ type: "stdio",
727
+ command: "npx",
728
+ args: ["-y", "@company/crm-server"],
729
+ },
730
+ },
731
+ });
732
+ expect(result["github"]).toBeDefined();
733
+ expect(result["our-crm"]).toBeDefined();
734
+ });
668
735
  });