@sweny-ai/core 0.1.0

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 (93) hide show
  1. package/dist/__tests__/claude.test.d.ts +1 -0
  2. package/dist/__tests__/claude.test.js +328 -0
  3. package/dist/__tests__/executor.test.d.ts +1 -0
  4. package/dist/__tests__/executor.test.js +296 -0
  5. package/dist/__tests__/integration/datadog.integration.test.d.ts +1 -0
  6. package/dist/__tests__/integration/datadog.integration.test.js +23 -0
  7. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +1 -0
  8. package/dist/__tests__/integration/e2e-workflow.integration.test.js +75 -0
  9. package/dist/__tests__/integration/github.integration.test.d.ts +1 -0
  10. package/dist/__tests__/integration/github.integration.test.js +37 -0
  11. package/dist/__tests__/integration/harness.d.ts +24 -0
  12. package/dist/__tests__/integration/harness.js +34 -0
  13. package/dist/__tests__/integration/linear.integration.test.d.ts +1 -0
  14. package/dist/__tests__/integration/linear.integration.test.js +15 -0
  15. package/dist/__tests__/integration/sentry.integration.test.d.ts +1 -0
  16. package/dist/__tests__/integration/sentry.integration.test.js +20 -0
  17. package/dist/__tests__/integration/slack.integration.test.d.ts +1 -0
  18. package/dist/__tests__/integration/slack.integration.test.js +22 -0
  19. package/dist/__tests__/schema.test.d.ts +1 -0
  20. package/dist/__tests__/schema.test.js +239 -0
  21. package/dist/__tests__/skills-index.test.d.ts +1 -0
  22. package/dist/__tests__/skills-index.test.js +122 -0
  23. package/dist/__tests__/skills.test.d.ts +1 -0
  24. package/dist/__tests__/skills.test.js +296 -0
  25. package/dist/__tests__/studio.test.d.ts +1 -0
  26. package/dist/__tests__/studio.test.js +172 -0
  27. package/dist/__tests__/testing.test.d.ts +1 -0
  28. package/dist/__tests__/testing.test.js +224 -0
  29. package/dist/browser.d.ts +17 -0
  30. package/dist/browser.js +22 -0
  31. package/dist/claude.d.ts +48 -0
  32. package/dist/claude.js +293 -0
  33. package/dist/cli/check.d.ts +11 -0
  34. package/dist/cli/check.js +237 -0
  35. package/dist/cli/config-file.d.ts +12 -0
  36. package/dist/cli/config-file.js +208 -0
  37. package/dist/cli/config.d.ts +77 -0
  38. package/dist/cli/config.js +565 -0
  39. package/dist/cli/main.d.ts +10 -0
  40. package/dist/cli/main.js +744 -0
  41. package/dist/cli/output.d.ts +26 -0
  42. package/dist/cli/output.js +357 -0
  43. package/dist/cli/renderer.d.ts +33 -0
  44. package/dist/cli/renderer.js +423 -0
  45. package/dist/cli/renderer.test.d.ts +1 -0
  46. package/dist/cli/renderer.test.js +302 -0
  47. package/dist/cli/setup.d.ts +11 -0
  48. package/dist/cli/setup.js +310 -0
  49. package/dist/executor.d.ts +29 -0
  50. package/dist/executor.js +173 -0
  51. package/dist/executor.test.d.ts +1 -0
  52. package/dist/executor.test.js +314 -0
  53. package/dist/index.d.ts +37 -0
  54. package/dist/index.js +36 -0
  55. package/dist/mcp.d.ts +11 -0
  56. package/dist/mcp.js +183 -0
  57. package/dist/mcp.test.d.ts +1 -0
  58. package/dist/mcp.test.js +334 -0
  59. package/dist/schema.d.ts +318 -0
  60. package/dist/schema.js +207 -0
  61. package/dist/skills/betterstack.d.ts +7 -0
  62. package/dist/skills/betterstack.js +114 -0
  63. package/dist/skills/datadog.d.ts +7 -0
  64. package/dist/skills/datadog.js +107 -0
  65. package/dist/skills/github.d.ts +8 -0
  66. package/dist/skills/github.js +155 -0
  67. package/dist/skills/index.d.ts +68 -0
  68. package/dist/skills/index.js +134 -0
  69. package/dist/skills/linear.d.ts +7 -0
  70. package/dist/skills/linear.js +89 -0
  71. package/dist/skills/notification.d.ts +11 -0
  72. package/dist/skills/notification.js +142 -0
  73. package/dist/skills/sentry.d.ts +7 -0
  74. package/dist/skills/sentry.js +105 -0
  75. package/dist/skills/slack.d.ts +8 -0
  76. package/dist/skills/slack.js +115 -0
  77. package/dist/studio.d.ts +124 -0
  78. package/dist/studio.js +174 -0
  79. package/dist/testing.d.ts +88 -0
  80. package/dist/testing.js +253 -0
  81. package/dist/types.d.ts +144 -0
  82. package/dist/types.js +11 -0
  83. package/dist/workflow-builder.d.ts +45 -0
  84. package/dist/workflow-builder.js +120 -0
  85. package/dist/workflow-builder.test.d.ts +1 -0
  86. package/dist/workflow-builder.test.js +117 -0
  87. package/dist/workflows/implement.d.ts +11 -0
  88. package/dist/workflows/implement.js +83 -0
  89. package/dist/workflows/index.d.ts +2 -0
  90. package/dist/workflows/index.js +2 -0
  91. package/dist/workflows/triage.d.ts +18 -0
  92. package/dist/workflows/triage.js +108 -0
  93. package/package.json +83 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Notification Skill — generic webhook / multi-channel
3
+ *
4
+ * Replaces: notification/webhook.ts + notification/discord-webhook.ts +
5
+ * notification/email.ts + notification/teams-webhook.ts
6
+ *
7
+ * For Slack-specific notifications, use the Slack skill instead.
8
+ * This skill handles generic webhooks, Discord, Teams, and email.
9
+ */
10
+ export const notification = {
11
+ id: "notification",
12
+ name: "Notification",
13
+ description: "Send notifications via webhook, Discord, Teams, or email",
14
+ category: "notification",
15
+ config: {
16
+ NOTIFICATION_WEBHOOK_URL: {
17
+ description: "Generic webhook URL for notifications",
18
+ required: false,
19
+ env: "NOTIFICATION_WEBHOOK_URL",
20
+ },
21
+ DISCORD_WEBHOOK_URL: {
22
+ description: "Discord webhook URL",
23
+ required: false,
24
+ env: "DISCORD_WEBHOOK_URL",
25
+ },
26
+ TEAMS_WEBHOOK_URL: {
27
+ description: "Microsoft Teams webhook URL",
28
+ required: false,
29
+ env: "TEAMS_WEBHOOK_URL",
30
+ },
31
+ SMTP_URL: {
32
+ description: "SMTP connection URL for email notifications",
33
+ required: false,
34
+ env: "SMTP_URL",
35
+ },
36
+ },
37
+ tools: [
38
+ {
39
+ name: "notify_webhook",
40
+ description: "Send a JSON payload to a webhook URL",
41
+ input_schema: {
42
+ type: "object",
43
+ properties: {
44
+ url: { type: "string", description: "Webhook URL (overrides NOTIFICATION_WEBHOOK_URL)" },
45
+ payload: { type: "object", description: "JSON payload to send" },
46
+ },
47
+ required: ["payload"],
48
+ },
49
+ handler: async (input, ctx) => {
50
+ const url = input.url ?? ctx.config.NOTIFICATION_WEBHOOK_URL;
51
+ if (!url)
52
+ throw new Error("No webhook URL provided or configured");
53
+ const res = await fetch(url, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify(input.payload),
57
+ signal: AbortSignal.timeout(30_000),
58
+ });
59
+ if (!res.ok)
60
+ throw new Error(`[Notification] webhook failed (HTTP ${res.status}): ${await res.text()}`);
61
+ return { ok: true, status: res.status };
62
+ },
63
+ },
64
+ {
65
+ name: "notify_discord",
66
+ description: "Send a message to Discord via webhook",
67
+ input_schema: {
68
+ type: "object",
69
+ properties: {
70
+ content: { type: "string", description: "Message text" },
71
+ embeds: {
72
+ type: "array",
73
+ description: "Discord embed objects for rich formatting",
74
+ items: {
75
+ type: "object",
76
+ properties: {
77
+ title: { type: "string" },
78
+ description: { type: "string" },
79
+ color: { type: "number", description: "Embed color (decimal)" },
80
+ fields: {
81
+ type: "array",
82
+ items: {
83
+ type: "object",
84
+ properties: {
85
+ name: { type: "string" },
86
+ value: { type: "string" },
87
+ inline: { type: "boolean" },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ },
95
+ },
96
+ handler: async (input, ctx) => {
97
+ if (!ctx.config.DISCORD_WEBHOOK_URL)
98
+ throw new Error("[Notification] DISCORD_WEBHOOK_URL not configured");
99
+ const res = await fetch(ctx.config.DISCORD_WEBHOOK_URL, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify({ content: input.content, embeds: input.embeds }),
103
+ signal: AbortSignal.timeout(30_000),
104
+ });
105
+ if (!res.ok)
106
+ throw new Error(`[Notification] Discord webhook failed (HTTP ${res.status}): ${await res.text()}`);
107
+ return { ok: true };
108
+ },
109
+ },
110
+ {
111
+ name: "notify_teams",
112
+ description: "Send a message to Microsoft Teams via webhook",
113
+ input_schema: {
114
+ type: "object",
115
+ properties: {
116
+ title: { type: "string", description: "Card title" },
117
+ text: { type: "string", description: "Card body (markdown)" },
118
+ themeColor: { type: "string", description: "Hex color for the card accent" },
119
+ },
120
+ required: ["text"],
121
+ },
122
+ handler: async (input, ctx) => {
123
+ if (!ctx.config.TEAMS_WEBHOOK_URL)
124
+ throw new Error("[Notification] TEAMS_WEBHOOK_URL not configured");
125
+ const res = await fetch(ctx.config.TEAMS_WEBHOOK_URL, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({
129
+ "@type": "MessageCard",
130
+ themeColor: input.themeColor ?? "0076D7",
131
+ title: input.title,
132
+ text: input.text,
133
+ }),
134
+ signal: AbortSignal.timeout(30_000),
135
+ });
136
+ if (!res.ok)
137
+ throw new Error(`[Notification] Teams webhook failed (HTTP ${res.status}): ${await res.text()}`);
138
+ return { ok: true };
139
+ },
140
+ },
141
+ ],
142
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Sentry Skill
3
+ *
4
+ * Replaces: observability/sentry.ts
5
+ */
6
+ import type { Skill } from "../types.js";
7
+ export declare const sentry: Skill;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Sentry Skill
3
+ *
4
+ * Replaces: observability/sentry.ts
5
+ */
6
+ async function sentryApi(path, ctx) {
7
+ const base = ctx.config.SENTRY_BASE_URL || "https://sentry.io";
8
+ const res = await fetch(`${base}/api/0${path}`, {
9
+ headers: { Authorization: `Bearer ${ctx.config.SENTRY_AUTH_TOKEN}` },
10
+ signal: AbortSignal.timeout(30_000),
11
+ });
12
+ if (!res.ok)
13
+ throw new Error(`[Sentry] API request failed (HTTP ${res.status}): ${await res.text()}`);
14
+ return res.json();
15
+ }
16
+ export const sentry = {
17
+ id: "sentry",
18
+ name: "Sentry",
19
+ description: "Query errors, issues, and performance data from Sentry",
20
+ category: "observability",
21
+ config: {
22
+ SENTRY_AUTH_TOKEN: {
23
+ description: "Sentry authentication token",
24
+ required: true,
25
+ env: "SENTRY_AUTH_TOKEN",
26
+ },
27
+ SENTRY_ORG: {
28
+ description: "Sentry organization slug",
29
+ required: true,
30
+ env: "SENTRY_ORG",
31
+ },
32
+ SENTRY_BASE_URL: {
33
+ description: "Sentry base URL (default: https://sentry.io)",
34
+ required: false,
35
+ env: "SENTRY_BASE_URL",
36
+ },
37
+ },
38
+ tools: [
39
+ {
40
+ name: "sentry_list_issues",
41
+ description: "List recent issues for a Sentry project",
42
+ input_schema: {
43
+ type: "object",
44
+ properties: {
45
+ project: { type: "string", description: "Sentry project slug" },
46
+ query: { type: "string", description: "Search query (e.g., 'is:unresolved level:error')" },
47
+ },
48
+ required: ["project"],
49
+ },
50
+ handler: async (input, ctx) => {
51
+ const q = encodeURIComponent(input.query ?? "is:unresolved");
52
+ return sentryApi(`/projects/${ctx.config.SENTRY_ORG}/${input.project}/issues/?query=${q}`, ctx);
53
+ },
54
+ },
55
+ {
56
+ name: "sentry_get_issue",
57
+ description: "Get detailed information about a Sentry issue",
58
+ input_schema: {
59
+ type: "object",
60
+ properties: {
61
+ issueId: { type: "string", description: "Sentry issue ID" },
62
+ },
63
+ required: ["issueId"],
64
+ },
65
+ handler: async (input, ctx) => sentryApi(`/issues/${input.issueId}/`, ctx),
66
+ },
67
+ {
68
+ name: "sentry_get_issue_events",
69
+ description: "Get recent events (occurrences) for a Sentry issue",
70
+ input_schema: {
71
+ type: "object",
72
+ properties: {
73
+ issueId: { type: "string", description: "Sentry issue ID" },
74
+ },
75
+ required: ["issueId"],
76
+ },
77
+ handler: async (input, ctx) => sentryApi(`/issues/${input.issueId}/events/?full=true`, ctx),
78
+ },
79
+ {
80
+ name: "sentry_search_events",
81
+ description: "Search events across a project using Discover query syntax",
82
+ input_schema: {
83
+ type: "object",
84
+ properties: {
85
+ project: { type: "string", description: "Sentry project slug" },
86
+ query: { type: "string", description: "Discover query" },
87
+ fields: {
88
+ type: "array",
89
+ items: { type: "string" },
90
+ description: "Fields to return (e.g., ['title', 'count()', 'last_seen()'])",
91
+ },
92
+ },
93
+ required: ["project", "fields"],
94
+ },
95
+ handler: async (input, ctx) => {
96
+ const params = new URLSearchParams();
97
+ if (input.query)
98
+ params.set("query", input.query);
99
+ for (const f of input.fields)
100
+ params.append("field", f);
101
+ return sentryApi(`/organizations/${ctx.config.SENTRY_ORG}/events/?project=${input.project}&${params}`, ctx);
102
+ },
103
+ },
104
+ ],
105
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Slack Skill
3
+ *
4
+ * Replaces: notification/slack-webhook.ts + messaging/slack.ts
5
+ * Two separate provider modules → one skill with simple tools
6
+ */
7
+ import type { Skill } from "../types.js";
8
+ export declare const slack: Skill;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Slack Skill
3
+ *
4
+ * Replaces: notification/slack-webhook.ts + messaging/slack.ts
5
+ * Two separate provider modules → one skill with simple tools
6
+ */
7
+ export const slack = {
8
+ id: "slack",
9
+ name: "Slack",
10
+ description: "Send messages and notifications to Slack channels",
11
+ category: "notification",
12
+ config: {
13
+ SLACK_WEBHOOK_URL: {
14
+ description: "Slack incoming webhook URL",
15
+ required: false,
16
+ env: "SLACK_WEBHOOK_URL",
17
+ },
18
+ SLACK_BOT_TOKEN: {
19
+ description: "Slack bot token (for API calls)",
20
+ required: false,
21
+ env: "SLACK_BOT_TOKEN",
22
+ },
23
+ },
24
+ tools: [
25
+ {
26
+ name: "slack_send_message",
27
+ description: "Send a message to a Slack channel via webhook or API",
28
+ input_schema: {
29
+ type: "object",
30
+ properties: {
31
+ text: { type: "string", description: "Message text (Slack mrkdwn supported)" },
32
+ channel: { type: "string", description: "Channel ID or name (required for API, ignored for webhook)" },
33
+ blocks: {
34
+ type: "array",
35
+ description: "Optional Block Kit blocks for rich formatting",
36
+ items: { type: "object" },
37
+ },
38
+ },
39
+ required: ["text"],
40
+ },
41
+ handler: async (input, ctx) => {
42
+ // Prefer bot token API for channel targeting; fall back to webhook
43
+ if (ctx.config.SLACK_BOT_TOKEN && input.channel) {
44
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
45
+ method: "POST",
46
+ headers: {
47
+ Authorization: `Bearer ${ctx.config.SLACK_BOT_TOKEN}`,
48
+ "Content-Type": "application/json",
49
+ },
50
+ body: JSON.stringify({
51
+ channel: input.channel,
52
+ text: input.text,
53
+ blocks: input.blocks,
54
+ }),
55
+ signal: AbortSignal.timeout(30_000),
56
+ });
57
+ if (!res.ok)
58
+ throw new Error(`[Slack] send_message failed (HTTP ${res.status}): ${await res.text()}`);
59
+ const data = await res.json();
60
+ if (!data.ok)
61
+ throw new Error(`[Slack] send_message failed: ${data.error ?? "unknown error"}`);
62
+ return data;
63
+ }
64
+ if (ctx.config.SLACK_WEBHOOK_URL) {
65
+ const res = await fetch(ctx.config.SLACK_WEBHOOK_URL, {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({ text: input.text, blocks: input.blocks }),
69
+ signal: AbortSignal.timeout(30_000),
70
+ });
71
+ if (!res.ok)
72
+ throw new Error(`[Slack] webhook failed (HTTP ${res.status}): ${await res.text()}`);
73
+ return { ok: true };
74
+ }
75
+ throw new Error("No Slack credentials configured (need SLACK_WEBHOOK_URL or SLACK_BOT_TOKEN)");
76
+ },
77
+ },
78
+ {
79
+ name: "slack_send_thread_reply",
80
+ description: "Reply to an existing Slack message thread",
81
+ input_schema: {
82
+ type: "object",
83
+ properties: {
84
+ channel: { type: "string", description: "Channel ID" },
85
+ thread_ts: { type: "string", description: "Parent message timestamp" },
86
+ text: { type: "string" },
87
+ },
88
+ required: ["channel", "thread_ts", "text"],
89
+ },
90
+ handler: async (input, ctx) => {
91
+ if (!ctx.config.SLACK_BOT_TOKEN)
92
+ throw new Error("[Slack] thread replies require SLACK_BOT_TOKEN");
93
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
94
+ method: "POST",
95
+ headers: {
96
+ Authorization: `Bearer ${ctx.config.SLACK_BOT_TOKEN}`,
97
+ "Content-Type": "application/json",
98
+ },
99
+ body: JSON.stringify({
100
+ channel: input.channel,
101
+ thread_ts: input.thread_ts,
102
+ text: input.text,
103
+ }),
104
+ signal: AbortSignal.timeout(30_000),
105
+ });
106
+ if (!res.ok)
107
+ throw new Error(`[Slack] thread_reply failed (HTTP ${res.status}): ${await res.text()}`);
108
+ const data = await res.json();
109
+ if (!data.ok)
110
+ throw new Error(`[Slack] thread_reply failed: ${data.error ?? "unknown error"}`);
111
+ return data;
112
+ },
113
+ },
114
+ ],
115
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Studio Adapter
3
+ *
4
+ * Converts between @sweny-ai/core Workflow types and React Flow
5
+ * graph structures. This replaces definition-to-flow.ts in the
6
+ * old Studio — and it's dramatically simpler because our Workflow
7
+ * type already uses explicit nodes + edges (not next/on routing).
8
+ *
9
+ * Old Studio flow:
10
+ * WorkflowDefinition → extractTransitions() → build nodes → build edges → ELK
11
+ * (94 lines of conversion code)
12
+ *
13
+ * New Studio flow:
14
+ * Workflow → workflowToFlow()
15
+ * (direct mapping — edges are already explicit)
16
+ *
17
+ * The big wins for Studio in the new model:
18
+ *
19
+ * 1. NO CONVERSION LAYER — Workflow.nodes → React Flow nodes,
20
+ * Workflow.edges → React Flow edges. It's a 1:1 map.
21
+ *
22
+ * 2. SKILL CATALOG in the properties panel — instead of picking
23
+ * from "phase" and "uses" (provider roles), users browse a
24
+ * skill catalog and toggle which skills are available at each node.
25
+ *
26
+ * 3. INSTRUCTION EDITOR — each node has a rich-text instruction
27
+ * instead of a one-line description. This is what Claude actually
28
+ * reads, so WYSIWYG.
29
+ *
30
+ * 4. NATURAL LANGUAGE EDGE CONDITIONS — instead of "outcome → target"
31
+ * with magic string outcomes, edges have human-readable `when`
32
+ * clauses like "severity is high or critical".
33
+ *
34
+ * 5. OUTPUT SCHEMA EDITOR — optional JSON schema per node for
35
+ * structured output. Studio could render this as a form builder.
36
+ *
37
+ * 6. LIVE EXECUTION OVERLAY — the event stream from execute() maps
38
+ * directly to node highlights. node:enter → glow blue,
39
+ * tool:call → show tool name, node:exit → green/red.
40
+ */
41
+ import type { Workflow, Node, ExecutionEvent, NodeResult, Skill } from "./types.js";
42
+ export interface FlowNode {
43
+ id: string;
44
+ type: "skillNode";
45
+ position: {
46
+ x: number;
47
+ y: number;
48
+ };
49
+ data: SkillNodeData;
50
+ }
51
+ export interface SkillNodeData {
52
+ nodeId: string;
53
+ node: Node;
54
+ isEntry: boolean;
55
+ isTerminal: boolean;
56
+ /** Resolved skill metadata for the node's skill IDs */
57
+ skills: {
58
+ id: string;
59
+ name: string;
60
+ toolCount: number;
61
+ }[];
62
+ /** Execution state (for live/simulate mode) */
63
+ exec: {
64
+ status: "pending" | "running" | "success" | "failed" | "skipped";
65
+ result?: NodeResult;
66
+ currentTool?: string;
67
+ };
68
+ }
69
+ export interface FlowEdge {
70
+ id: string;
71
+ source: string;
72
+ target: string;
73
+ type: "conditionEdge";
74
+ data: {
75
+ when?: string;
76
+ isConditional: boolean;
77
+ };
78
+ }
79
+ /**
80
+ * Convert a Workflow to React Flow nodes + edges.
81
+ *
82
+ * This is the entire conversion layer — compare to the old
83
+ * definitionToFlow() which had to parse next/on routing into edges.
84
+ */
85
+ export declare function workflowToFlow(workflow: Workflow, skillCatalog?: Skill[]): {
86
+ nodes: FlowNode[];
87
+ edges: FlowEdge[];
88
+ };
89
+ /**
90
+ * Apply an execution event to the flow state.
91
+ * Returns a function that mutates SkillNodeData in place (for Zustand/immer).
92
+ */
93
+ export declare function applyExecutionEvent(event: ExecutionEvent, nodeDataMap: Map<string, SkillNodeData>): void;
94
+ /**
95
+ * Convert React Flow state back to a Workflow.
96
+ * This is what "Export" / "Save" does in Studio.
97
+ */
98
+ export declare function flowToWorkflow(meta: {
99
+ id: string;
100
+ name: string;
101
+ description: string;
102
+ entry: string;
103
+ }, nodes: FlowNode[], edges: FlowEdge[]): Workflow;
104
+ /**
105
+ * Generate a TypeScript file from a Workflow definition.
106
+ *
107
+ * Compare to the old exportAsTypescript() which generated step
108
+ * implementation stubs. The new export is just the workflow
109
+ * definition — Claude does the implementation.
110
+ */
111
+ export declare function exportAsTypescript(workflow: Workflow): string;
112
+ /**
113
+ * Skill catalog for the Studio properties panel.
114
+ * Returns all available skills with their tool lists.
115
+ */
116
+ export declare function getSkillCatalog(extraSkills?: Skill[]): {
117
+ id: string;
118
+ name: string;
119
+ description: string;
120
+ tools: {
121
+ name: string;
122
+ description: string;
123
+ }[];
124
+ }[];
package/dist/studio.js ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Studio Adapter
3
+ *
4
+ * Converts between @sweny-ai/core Workflow types and React Flow
5
+ * graph structures. This replaces definition-to-flow.ts in the
6
+ * old Studio — and it's dramatically simpler because our Workflow
7
+ * type already uses explicit nodes + edges (not next/on routing).
8
+ *
9
+ * Old Studio flow:
10
+ * WorkflowDefinition → extractTransitions() → build nodes → build edges → ELK
11
+ * (94 lines of conversion code)
12
+ *
13
+ * New Studio flow:
14
+ * Workflow → workflowToFlow()
15
+ * (direct mapping — edges are already explicit)
16
+ *
17
+ * The big wins for Studio in the new model:
18
+ *
19
+ * 1. NO CONVERSION LAYER — Workflow.nodes → React Flow nodes,
20
+ * Workflow.edges → React Flow edges. It's a 1:1 map.
21
+ *
22
+ * 2. SKILL CATALOG in the properties panel — instead of picking
23
+ * from "phase" and "uses" (provider roles), users browse a
24
+ * skill catalog and toggle which skills are available at each node.
25
+ *
26
+ * 3. INSTRUCTION EDITOR — each node has a rich-text instruction
27
+ * instead of a one-line description. This is what Claude actually
28
+ * reads, so WYSIWYG.
29
+ *
30
+ * 4. NATURAL LANGUAGE EDGE CONDITIONS — instead of "outcome → target"
31
+ * with magic string outcomes, edges have human-readable `when`
32
+ * clauses like "severity is high or critical".
33
+ *
34
+ * 5. OUTPUT SCHEMA EDITOR — optional JSON schema per node for
35
+ * structured output. Studio could render this as a form builder.
36
+ *
37
+ * 6. LIVE EXECUTION OVERLAY — the event stream from execute() maps
38
+ * directly to node highlights. node:enter → glow blue,
39
+ * tool:call → show tool name, node:exit → green/red.
40
+ */
41
+ import { builtinSkills } from "./skills/index.js";
42
+ // ─── Conversion ──────────────────────────────────────────────────
43
+ /**
44
+ * Convert a Workflow to React Flow nodes + edges.
45
+ *
46
+ * This is the entire conversion layer — compare to the old
47
+ * definitionToFlow() which had to parse next/on routing into edges.
48
+ */
49
+ export function workflowToFlow(workflow, skillCatalog = builtinSkills) {
50
+ const skillMap = new Map(skillCatalog.map((s) => [s.id, s]));
51
+ const terminalIds = findTerminals(workflow);
52
+ const nodes = Object.entries(workflow.nodes).map(([id, node]) => ({
53
+ id,
54
+ type: "skillNode",
55
+ position: { x: 0, y: 0 }, // ELK will compute real positions
56
+ data: {
57
+ nodeId: id,
58
+ node,
59
+ isEntry: id === workflow.entry,
60
+ isTerminal: terminalIds.has(id),
61
+ skills: node.skills.map((sid) => {
62
+ const skill = skillMap.get(sid);
63
+ return {
64
+ id: sid,
65
+ name: skill?.name ?? sid,
66
+ toolCount: skill?.tools.length ?? 0,
67
+ };
68
+ }),
69
+ exec: { status: "pending" },
70
+ },
71
+ }));
72
+ const edges = workflow.edges.map((edge) => ({
73
+ id: `${edge.from}--${edge.to}`,
74
+ source: edge.from,
75
+ target: edge.to,
76
+ type: "conditionEdge",
77
+ data: {
78
+ when: edge.when,
79
+ isConditional: !!edge.when,
80
+ },
81
+ }));
82
+ return { nodes, edges };
83
+ }
84
+ /**
85
+ * Apply an execution event to the flow state.
86
+ * Returns a function that mutates SkillNodeData in place (for Zustand/immer).
87
+ */
88
+ export function applyExecutionEvent(event, nodeDataMap) {
89
+ switch (event.type) {
90
+ case "workflow:start":
91
+ for (const data of nodeDataMap.values()) {
92
+ data.exec = { status: "pending" };
93
+ }
94
+ break;
95
+ case "node:enter": {
96
+ const data = nodeDataMap.get(event.node);
97
+ if (data)
98
+ data.exec = { status: "running" };
99
+ break;
100
+ }
101
+ case "tool:call": {
102
+ const data = nodeDataMap.get(event.node);
103
+ if (data)
104
+ data.exec.currentTool = event.tool;
105
+ break;
106
+ }
107
+ case "tool:result": {
108
+ const data = nodeDataMap.get(event.node);
109
+ if (data)
110
+ data.exec.currentTool = undefined;
111
+ break;
112
+ }
113
+ case "node:exit": {
114
+ const data = nodeDataMap.get(event.node);
115
+ if (data) {
116
+ data.exec = {
117
+ status: event.result.status === "success" ? "success" : "failed",
118
+ result: event.result,
119
+ };
120
+ }
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ // ─── Reverse conversion (Studio → Workflow) ──────────────────────
126
+ /**
127
+ * Convert React Flow state back to a Workflow.
128
+ * This is what "Export" / "Save" does in Studio.
129
+ */
130
+ export function flowToWorkflow(meta, nodes, edges) {
131
+ return {
132
+ id: meta.id,
133
+ name: meta.name,
134
+ description: meta.description,
135
+ entry: meta.entry,
136
+ nodes: Object.fromEntries(nodes.map((n) => [n.id, n.data.node])),
137
+ edges: edges.map((e) => ({
138
+ from: e.source,
139
+ to: e.target,
140
+ ...(e.data.when ? { when: e.data.when } : {}),
141
+ })),
142
+ };
143
+ }
144
+ // ─── Helpers ─────────────────────────────────────────────────────
145
+ function findTerminals(workflow) {
146
+ const hasOutgoing = new Set(workflow.edges.map((e) => e.from));
147
+ return new Set(Object.keys(workflow.nodes).filter((id) => !hasOutgoing.has(id)));
148
+ }
149
+ /**
150
+ * Generate a TypeScript file from a Workflow definition.
151
+ *
152
+ * Compare to the old exportAsTypescript() which generated step
153
+ * implementation stubs. The new export is just the workflow
154
+ * definition — Claude does the implementation.
155
+ */
156
+ export function exportAsTypescript(workflow) {
157
+ const varName = workflow.id.replace(/[^a-zA-Z0-9]/g, "_");
158
+ return `import type { Workflow } from "@sweny-ai/core";
159
+
160
+ export const ${varName}: Workflow = ${JSON.stringify(workflow, null, 2)};
161
+ `;
162
+ }
163
+ /**
164
+ * Skill catalog for the Studio properties panel.
165
+ * Returns all available skills with their tool lists.
166
+ */
167
+ export function getSkillCatalog(extraSkills = []) {
168
+ return [...builtinSkills, ...extraSkills].map((s) => ({
169
+ id: s.id,
170
+ name: s.name,
171
+ description: s.description,
172
+ tools: s.tools.map((t) => ({ name: t.name, description: t.description })),
173
+ }));
174
+ }