claude-cwc 0.2.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.
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Claude Workflow Composer</title>
7
+ <meta name="description" content="Visually compose multi-agent coding workflows for Claude Code. Drag agents, attach skills, wire handoffs, and export." />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Spline+Sans:wght@400;500;600;700&family=Barlow:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
+ <script type="module" crossorigin src="/assets/index-BygZUlo1.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-BtCVmYEx.css">
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,42 @@
1
+ export function bfsTraversal(nodes, edges) {
2
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
3
+ // Build adjacency: from → edges
4
+ const adj = new Map();
5
+ for (const e of edges) {
6
+ if (!adj.has(e.from))
7
+ adj.set(e.from, []);
8
+ adj.get(e.from).push(e);
9
+ }
10
+ // Entry nodes: nodes with no incoming edges (ignoring terminal edges where to === null)
11
+ const hasIncoming = new Set(edges.filter(e => e.to !== null).map(e => e.to));
12
+ let entryIds = nodes.filter(n => !hasIncoming.has(n.id)).map(n => n.id);
13
+ // If every node has incoming edges (pure cycle), seed with the first node
14
+ if (entryIds.length === 0 && nodes.length > 0) {
15
+ entryIds = [nodes[0].id];
16
+ }
17
+ const visited = new Set();
18
+ const steps = [];
19
+ // Queue of [nodeId, level]
20
+ const queue = entryIds.map(id => [id, 0]);
21
+ while (queue.length > 0) {
22
+ const [id, level] = queue.shift();
23
+ if (visited.has(id))
24
+ continue;
25
+ visited.add(id);
26
+ const n = nodeMap.get(id);
27
+ if (!n)
28
+ continue;
29
+ const rawEdges = adj.get(id) ?? [];
30
+ const annotated = rawEdges.map(e => ({
31
+ edge: e,
32
+ isBackEdge: e.to !== null && visited.has(e.to),
33
+ }));
34
+ steps.push({ node: n, level, outgoingEdges: annotated });
35
+ for (const ae of annotated) {
36
+ if (!ae.isBackEdge && ae.edge.to !== null) {
37
+ queue.push([ae.edge.to, level + 1]);
38
+ }
39
+ }
40
+ }
41
+ return steps;
42
+ }
@@ -0,0 +1,24 @@
1
+ export function detectConflict(fileContent, ownershipRegex, currentWorkflowId) {
2
+ const lines = fileContent.split('\n');
3
+ // Scan upward for first non-blank line
4
+ let lastNonBlank = null;
5
+ for (let i = lines.length - 1; i >= 0; i--) {
6
+ const trimmed = lines[i].trim();
7
+ if (trimmed.length > 0) {
8
+ lastNonBlank = trimmed;
9
+ break;
10
+ }
11
+ }
12
+ if (lastNonBlank === null)
13
+ return 'absent';
14
+ if (!lastNonBlank.startsWith('<!-- cwc:'))
15
+ return 'absent';
16
+ if (!ownershipRegex.test(lastNonBlank))
17
+ return 'malformed';
18
+ // Extract UUID — last token before ' -->'
19
+ const uuidMatch = lastNonBlank.match(/([^\s:>]+) -->$/);
20
+ if (!uuidMatch)
21
+ return 'malformed';
22
+ const foundId = uuidMatch[1];
23
+ return foundId === currentWorkflowId ? 'owned' : 'foreign';
24
+ }
@@ -0,0 +1,139 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { slugify } from './slugify.js';
5
+ import { generateOrchestratorBody } from './prose-generator.js';
6
+ import { resolveSkill } from './skill-resolver.js';
7
+ import { buildAgentFileContent, buildWorkflowSkillContent } from './file-writer.js';
8
+ import { detectConflict } from './conflict-detector.js';
9
+ const AGENT_OWNERSHIP_REGEX = /^<!-- cwc:node:[^:\s]+:workflow:[^:\s>]+ -->$/;
10
+ const WORKFLOW_OWNERSHIP_REGEX = /^<!-- cwc:workflow:[^:\s>]+ -->$/;
11
+ async function safeReadFile(p) {
12
+ try {
13
+ return await fs.readFile(p, 'utf-8');
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ async function ensureDir(p) {
20
+ await fs.mkdir(p, { recursive: true });
21
+ }
22
+ async function resolveSkillWithOverride(slug, userSkillsDir) {
23
+ if (!slug.includes(':') && userSkillsDir) {
24
+ const skillMdPath = path.join(userSkillsDir, slug, 'SKILL.md');
25
+ try {
26
+ const content = await fs.readFile(skillMdPath, 'utf-8');
27
+ const { data } = matter(content);
28
+ return { slug, description: typeof data.description === 'string' ? data.description : null, found: true };
29
+ }
30
+ catch {
31
+ // Fall through to normal resolution
32
+ }
33
+ }
34
+ return resolveSkill(slug);
35
+ }
36
+ export async function exportWorkflow(cwc, target, opts) {
37
+ const warnings = [];
38
+ const workflowId = cwc.meta.id;
39
+ const workflowSlug = 'cwc-' + slugify(cwc.meta.name);
40
+ const agentsDir = target.type === 'project'
41
+ ? path.join(target.projectDir, '.claude', 'agents')
42
+ : path.join(target.userDir ?? (process.env.HOME ?? ''), '.claude', 'agents');
43
+ await ensureDir(agentsDir);
44
+ const updatedNodes = [];
45
+ const nodeOverrides = {};
46
+ for (const node of cwc.nodes) {
47
+ if (node.agentRef) {
48
+ // Ref node — points to an existing agent; don't write a new file
49
+ const refSlug = node.agentRef;
50
+ if (node.exportedSlug && node.exportedSlug !== refSlug) {
51
+ const oldPath = path.join(agentsDir, `${node.exportedSlug}.md`);
52
+ const oldContent = await safeReadFile(oldPath);
53
+ if (oldContent !== null) {
54
+ const status = detectConflict(oldContent, AGENT_OWNERSHIP_REGEX, workflowId);
55
+ if (status === 'owned') {
56
+ await fs.unlink(oldPath);
57
+ }
58
+ }
59
+ }
60
+ // Resolve ref node's skills for warnings
61
+ for (const skillSlug of node.agent.skills ?? []) {
62
+ const resolved = await resolveSkillWithOverride(skillSlug, opts.userSkillsDir);
63
+ if (!resolved.found) {
64
+ warnings.push(`Skill not found: ${skillSlug} — install it on the target machine`);
65
+ }
66
+ }
67
+ // Collect overrides for orchestrator annotation
68
+ const hasOverrides = (node.agent.skills ?? []).length > 0
69
+ || (node.agent.tools ?? []).length > 0
70
+ || (node.agent.systemPrompt ?? '').trim().length > 0
71
+ || (node.agent.completionCriteria ?? '').trim().length > 0;
72
+ if (hasOverrides) {
73
+ nodeOverrides[node.id] = {
74
+ skills: node.agent.skills,
75
+ tools: node.agent.tools,
76
+ systemPrompt: node.agent.systemPrompt,
77
+ completionCriteria: node.agent.completionCriteria,
78
+ };
79
+ }
80
+ // Warn if the referenced agent file doesn't exist on the target machine
81
+ const refPath = path.join(agentsDir, `${refSlug}.md`);
82
+ const refContent = await safeReadFile(refPath);
83
+ if (refContent === null) {
84
+ warnings.push(`Referenced agent not found: ${refSlug} — install it on the target machine`);
85
+ }
86
+ updatedNodes.push({ ...node, exportedSlug: refSlug });
87
+ continue;
88
+ }
89
+ const newSlug = slugify(node.agent.name);
90
+ const agentPath = path.join(agentsDir, `${newSlug}.md`);
91
+ // Rename: old file cleanup
92
+ if (node.exportedSlug && node.exportedSlug !== newSlug) {
93
+ const oldPath = path.join(agentsDir, `${node.exportedSlug}.md`);
94
+ const oldContent = await safeReadFile(oldPath);
95
+ if (oldContent !== null) {
96
+ const status = detectConflict(oldContent, AGENT_OWNERSHIP_REGEX, workflowId);
97
+ if (status === 'owned') {
98
+ await fs.unlink(oldPath);
99
+ }
100
+ }
101
+ // If file missing (null): skip delete, proceed to write new file
102
+ }
103
+ // Resolve skills
104
+ const resolvedSkills = [];
105
+ for (const skillSlug of node.agent.skills ?? []) {
106
+ const resolved = await resolveSkillWithOverride(skillSlug, opts.userSkillsDir);
107
+ if (!resolved.found) {
108
+ warnings.push(`Skill not found: ${skillSlug} — install it on the target machine`);
109
+ }
110
+ resolvedSkills.push(resolved);
111
+ }
112
+ const content = buildAgentFileContent(node, resolvedSkills, workflowId);
113
+ await fs.writeFile(agentPath, content, 'utf-8');
114
+ updatedNodes.push({ ...node, exportedSlug: newSlug });
115
+ }
116
+ // Generate workflow skill
117
+ const orchestratorBody = generateOrchestratorBody(cwc.nodes, cwc.edges, cwc.meta.name, nodeOverrides);
118
+ const skillContent = buildWorkflowSkillContent(cwc.meta.name, cwc.meta.description, orchestratorBody, workflowId);
119
+ const skillDir = path.join(opts.skillsDir, workflowSlug);
120
+ await ensureDir(skillDir);
121
+ const skillFilePath = path.join(skillDir, 'SKILL.md');
122
+ const existingSkill = await safeReadFile(skillFilePath);
123
+ if (existingSkill !== null) {
124
+ const status = detectConflict(existingSkill, WORKFLOW_OWNERSHIP_REGEX, workflowId);
125
+ if (status === 'foreign') {
126
+ warnings.push(`Workflow skill at ${skillFilePath} belongs to a different workflow — overwriting`);
127
+ }
128
+ else if (status === 'absent') {
129
+ warnings.push(`Workflow skill at ${skillFilePath} was not created by this workflow — overwriting`);
130
+ }
131
+ }
132
+ await fs.writeFile(skillFilePath, skillContent, 'utf-8');
133
+ const updatedCwc = {
134
+ ...cwc,
135
+ nodes: updatedNodes,
136
+ meta: { ...cwc.meta, updated: new Date().toISOString() },
137
+ };
138
+ return { updatedCwc, warnings };
139
+ }
@@ -0,0 +1,53 @@
1
+ function buildFrontmatter(node) {
2
+ const { name, description, color, model, tools } = node.agent;
3
+ const lines = ['---'];
4
+ lines.push(`name: ${name}`);
5
+ lines.push(`description: ${description}`);
6
+ if (color)
7
+ lines.push(`color: ${color}`);
8
+ if (model)
9
+ lines.push(`model: ${model}`);
10
+ if (tools && tools.length > 0)
11
+ lines.push(`tools: ${tools.join(', ')}`);
12
+ lines.push('---');
13
+ return lines.join('\n');
14
+ }
15
+ function buildSkillsBlock(skills) {
16
+ const lines = skills.map(s => s.description
17
+ ? `Use the \`${s.slug}\` skill. (${s.description})`
18
+ : `Use the \`${s.slug}\` skill.`);
19
+ return `## Workflow Skills\n\n${lines.join('\n')}`;
20
+ }
21
+ export function buildAgentFileContent(node, resolvedSkills, workflowId) {
22
+ const parts = [];
23
+ parts.push(buildFrontmatter(node));
24
+ const { systemPrompt, completionCriteria } = node.agent;
25
+ if (systemPrompt && systemPrompt.trim().length > 0) {
26
+ parts.push('\n' + systemPrompt);
27
+ }
28
+ if (completionCriteria && completionCriteria.trim().length > 0) {
29
+ parts.push(`\n\n## Completion Criteria\n\nBefore returning, verify: ${completionCriteria}`);
30
+ }
31
+ const ownershipComment = `<!-- cwc:node:${node.id}:workflow:${workflowId} -->`;
32
+ const hasContent = (systemPrompt && systemPrompt.trim().length > 0) ||
33
+ (completionCriteria && completionCriteria.trim().length > 0);
34
+ if (resolvedSkills.length > 0) {
35
+ const separator = hasContent ? '\n\n---\n' : '\n';
36
+ parts.push(separator + buildSkillsBlock(resolvedSkills));
37
+ parts.push('\n' + ownershipComment);
38
+ }
39
+ else {
40
+ parts.push('\n' + ownershipComment);
41
+ }
42
+ return parts.join('');
43
+ }
44
+ export function buildWorkflowSkillContent(name, description, orchestratorBody, workflowId) {
45
+ const frontmatter = [
46
+ '---',
47
+ `name: ${name}`,
48
+ `description: ${description}`,
49
+ 'disable-model-invocation: true',
50
+ '---',
51
+ ].join('\n');
52
+ return `${frontmatter}\n\n${orchestratorBody}\n<!-- cwc:workflow:${workflowId} -->`;
53
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ // Entry point - will be populated in next tasks
@@ -0,0 +1,128 @@
1
+ import { bfsTraversal } from './bfs.js';
2
+ import { slugify } from './slugify.js';
3
+ function oxfordJoin(items) {
4
+ if (items.length === 0)
5
+ return '';
6
+ if (items.length === 1)
7
+ return items[0];
8
+ if (items.length === 2)
9
+ return `${items[0]} and ${items[1]}`;
10
+ return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`;
11
+ }
12
+ function formatArtifactLabel(a) {
13
+ return a.type === 'file' && a.path ? `${a.name} (\`${a.path}\`)` : a.name;
14
+ }
15
+ function boldWrapAgentNames(text, agentNames) {
16
+ let result = text;
17
+ for (const name of [...agentNames].sort((a, b) => b.length - a.length)) {
18
+ result = result.replaceAll(name, `**${name}**`);
19
+ }
20
+ return result;
21
+ }
22
+ function formatContextClause(context) {
23
+ if (!context || context.length === 0)
24
+ return '';
25
+ return ` Pass the ${oxfordJoin(context.map(formatArtifactLabel))} forward.`;
26
+ }
27
+ function nodeSlug(node) {
28
+ return node.agentRef ?? node.exportedSlug ?? slugify(node.agent.name);
29
+ }
30
+ function formatOverrideAnnotation(nodeId, overrides) {
31
+ const o = overrides[nodeId];
32
+ if (!o)
33
+ return '';
34
+ const parts = [];
35
+ if (o.skills && o.skills.length > 0)
36
+ parts.push(`additional skills (${o.skills.join(', ')})`);
37
+ if (o.tools && o.tools.length > 0)
38
+ parts.push(`tools (${o.tools.join(', ')})`);
39
+ if (o.systemPrompt && o.systemPrompt.trim()) {
40
+ const snippet = o.systemPrompt.trim().slice(0, 80);
41
+ parts.push(`prompt "${snippet}${o.systemPrompt.length > 80 ? '...' : ''}"`);
42
+ }
43
+ if (o.completionCriteria && o.completionCriteria.trim()) {
44
+ const snippet = o.completionCriteria.trim().slice(0, 80);
45
+ parts.push(`completion "${snippet}${o.completionCriteria.length > 80 ? '...' : ''}"`);
46
+ }
47
+ if (parts.length === 0)
48
+ return '';
49
+ return ` Workflow-specific configuration: ${parts.join(', ')}.`;
50
+ }
51
+ export function generateOrchestratorBody(nodes, edges, workflowName, nodeOverrides = {}) {
52
+ const agentNames = nodes.map(n => n.agent.name);
53
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
54
+ const steps = bfsTraversal(nodes, edges);
55
+ const lines = [];
56
+ lines.push(`I am the orchestrator for the **${workflowName}** workflow. I coordinate this pipeline exclusively through the Agent tool — I do not read, write, or edit files myself. All implementation work is delegated to subagents.`, '', '## Pipeline', '');
57
+ let stepNum = 1;
58
+ const level0 = steps.filter(s => s.level === 0);
59
+ if (level0.length > 1) {
60
+ const nameList = level0.map(s => `**${s.node.agent.name}**`).join(' and ');
61
+ lines.push(`${stepNum++}. Invoke ${nameList} in parallel:`);
62
+ for (const s of level0) {
63
+ const trigger = s.node.startTrigger ? ` ${s.node.startTrigger}` : '';
64
+ const overrides = formatOverrideAnnotation(s.node.id, nodeOverrides);
65
+ lines.push(` - **${s.node.agent.name}** (\`subagent_type: "${nodeSlug(s.node)}"\`)${trigger}.${overrides}`);
66
+ }
67
+ }
68
+ else if (level0.length === 1) {
69
+ const s = level0[0];
70
+ const trigger = s.node.startTrigger ? ` ${s.node.startTrigger}` : '';
71
+ const overrides = formatOverrideAnnotation(s.node.id, nodeOverrides);
72
+ lines.push(`${stepNum++}. Invoke **${s.node.agent.name}** (\`subagent_type: "${nodeSlug(s.node)}"\`)${trigger}.${overrides}`);
73
+ }
74
+ const emitted = new Set();
75
+ for (const step of steps) {
76
+ const forwardEdges = step.outgoingEdges.filter(ae => !ae.isBackEdge && ae.edge.to !== null);
77
+ const terminalEdges = step.outgoingEdges.filter(ae => ae.edge.to === null);
78
+ const backEdges = step.outgoingEdges.filter(ae => ae.isBackEdge);
79
+ if (forwardEdges.length > 1) {
80
+ const targets = forwardEdges.map(ae => nodeMap.get(ae.edge.to)).filter(Boolean);
81
+ const nameList = targets.map(n => `**${n.agent.name}**`).join(' and ');
82
+ lines.push(`${stepNum++}. When **${step.node.agent.name}** completes, invoke ${nameList} in parallel:`);
83
+ for (const ae of forwardEdges) {
84
+ const target = nodeMap.get(ae.edge.to);
85
+ if (!target)
86
+ continue;
87
+ const trigger = ae.edge.trigger.trim() || `Activate **${target.agent.name}**.`;
88
+ const ctx = formatContextClause(ae.edge.context);
89
+ const overrides = formatOverrideAnnotation(ae.edge.to, nodeOverrides);
90
+ lines.push(` - **${target.agent.name}** (\`subagent_type: "${nodeSlug(target)}"\`): ${trigger}${ctx}${overrides}`);
91
+ emitted.add(ae.edge.id);
92
+ }
93
+ }
94
+ else if (forwardEdges.length === 1) {
95
+ const ae = forwardEdges[0];
96
+ if (!emitted.has(ae.edge.id)) {
97
+ const target = nodeMap.get(ae.edge.to);
98
+ if (target) {
99
+ const raw = ae.edge.trigger.trim() || `Invoke **${target.agent.name}**.`;
100
+ const trigger = boldWrapAgentNames(raw, agentNames);
101
+ const ctx = formatContextClause(ae.edge.context);
102
+ const overrides = formatOverrideAnnotation(ae.edge.to, nodeOverrides);
103
+ lines.push(`${stepNum++}. ${trigger} Use the Agent tool with \`subagent_type: "${nodeSlug(target)}"\`.${ctx}${overrides}`);
104
+ }
105
+ emitted.add(ae.edge.id);
106
+ }
107
+ }
108
+ for (const ae of terminalEdges) {
109
+ if (!emitted.has(ae.edge.id)) {
110
+ const raw = ae.edge.trigger.trim() || `**${step.node.agent.name}** completes the workflow.`;
111
+ lines.push(`${stepNum++}. ${boldWrapAgentNames(raw, agentNames)}`);
112
+ emitted.add(ae.edge.id);
113
+ }
114
+ }
115
+ for (const ae of backEdges) {
116
+ if (!emitted.has(ae.edge.id)) {
117
+ const target = nodeMap.get(ae.edge.to);
118
+ const raw = ae.edge.trigger.trim() || (target ? `Return to **${target.agent.name}**.` : '');
119
+ const trigger = boldWrapAgentNames(raw, agentNames);
120
+ const ctx = formatContextClause(ae.edge.context);
121
+ lines.push(`${stepNum++}. ${trigger}${ctx}`);
122
+ emitted.add(ae.edge.id);
123
+ }
124
+ }
125
+ }
126
+ lines.push('', '## Scope Boundary', '', 'Append the following to every subagent prompt:', '', '> Operate within the scope defined in this prompt. Escalate if the required work falls outside that scope.', '', '## Escalation', '', 'After every subagent returns, check its response. If `status` is `blocked` or `escalation_needed`, stop immediately and present the issue to the user — do not attempt to work around it.', '', '## Completion', '', 'When all steps finish, present a summary to the user: which agents ran, what each produced, and any escalations or skipped steps.');
127
+ return lines.join('\n');
128
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { Router as createRouter } from 'express';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import matter from 'gray-matter';
5
+ async function scanAgentsDir(dir, source) {
6
+ try {
7
+ const files = await fs.readdir(dir);
8
+ const results = await Promise.all(files
9
+ .filter((f) => f.endsWith('.md'))
10
+ .map(async (f) => {
11
+ const fullPath = path.join(dir, f);
12
+ try {
13
+ const raw = await fs.readFile(fullPath, 'utf-8');
14
+ const { data } = matter(raw);
15
+ if (!data['name'])
16
+ return null;
17
+ return {
18
+ name: String(data['name']),
19
+ description: String(data['description'] ?? ''),
20
+ slug: f.replace(/\.md$/, ''),
21
+ source,
22
+ filePath: fullPath,
23
+ };
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }));
29
+ return results.filter((r) => r !== null);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ export function agentsRouter(userHomeDir) {
36
+ const router = createRouter();
37
+ router.get('/', async (req, res) => {
38
+ const projectDir = req.query['projectDir'];
39
+ const userAgentsDir = path.join(userHomeDir, '.claude', 'agents');
40
+ const agents = [
41
+ ...await scanAgentsDir(userAgentsDir, 'user'),
42
+ ...(projectDir ? await scanAgentsDir(path.join(projectDir, '.claude', 'agents'), 'project') : []),
43
+ ];
44
+ res.json(agents);
45
+ });
46
+ return router;
47
+ }
@@ -0,0 +1,18 @@
1
+ import { Router as createRouter } from 'express';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ export function claudeCheckRouter() {
6
+ const router = createRouter();
7
+ router.get('/', async (_req, res) => {
8
+ const claudeDir = path.join(os.homedir(), '.claude');
9
+ try {
10
+ await fs.access(claudeDir);
11
+ res.json({ installed: true, claudeDir });
12
+ }
13
+ catch {
14
+ res.json({ installed: false, claudeDir });
15
+ }
16
+ });
17
+ return router;
18
+ }
@@ -0,0 +1,87 @@
1
+ import { Router as createRouter } from 'express';
2
+ import { detectConflict } from '../../conflict-detector.js';
3
+ import { slugify } from '../../slugify.js';
4
+ import * as fs from 'node:fs/promises';
5
+ import * as path from 'node:path';
6
+ import * as os from 'node:os';
7
+ const AGENT_OWNERSHIP_REGEX = /^<!-- cwc:node:[^:\s]+:workflow:[^:\s>]+ -->$/;
8
+ const WORKFLOW_OWNERSHIP_REGEX = /^<!-- cwc:workflow:[^:\s>]+ -->$/;
9
+ async function safeReadFile(p) {
10
+ try {
11
+ return await fs.readFile(p, 'utf-8');
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export async function deleteExport(cwc, target) {
18
+ const workflowId = cwc.meta.id;
19
+ const workflowSlug = 'cwc-' + slugify(cwc.meta.name);
20
+ const legacyWorkflowSlug = slugify(cwc.meta.name);
21
+ const homeDir = os.homedir();
22
+ const agentsDir = target.type === 'project'
23
+ ? path.join(target.projectDir, '.claude', 'agents')
24
+ : path.join(homeDir, '.claude', 'agents');
25
+ const skillsDir = target.type === 'project'
26
+ ? path.join(target.projectDir, '.claude', 'skills')
27
+ : path.join(homeDir, '.claude', 'skills');
28
+ const deleted = [];
29
+ const skipped = [];
30
+ const notFound = [];
31
+ for (const node of cwc.nodes) {
32
+ if (node.agentRef) {
33
+ // Ref nodes point to pre-existing agent files — never delete them
34
+ continue;
35
+ }
36
+ const slug = node.exportedSlug ?? slugify(node.agent.name);
37
+ const agentPath = path.join(agentsDir, `${slug}.md`);
38
+ const content = await safeReadFile(agentPath);
39
+ if (content === null) {
40
+ notFound.push(agentPath);
41
+ continue;
42
+ }
43
+ const status = detectConflict(content, AGENT_OWNERSHIP_REGEX, workflowId);
44
+ if (status === 'owned') {
45
+ await fs.unlink(agentPath);
46
+ deleted.push(agentPath);
47
+ }
48
+ else {
49
+ skipped.push(agentPath);
50
+ }
51
+ }
52
+ for (const slug of [workflowSlug, legacyWorkflowSlug]) {
53
+ const skillDir = path.join(skillsDir, slug);
54
+ const skillFilePath = path.join(skillDir, 'SKILL.md');
55
+ const skillContent = await safeReadFile(skillFilePath);
56
+ if (skillContent !== null) {
57
+ const status = detectConflict(skillContent, WORKFLOW_OWNERSHIP_REGEX, workflowId);
58
+ if (status === 'owned') {
59
+ await fs.rm(skillDir, { recursive: true, force: true });
60
+ deleted.push(skillDir);
61
+ }
62
+ else {
63
+ skipped.push(skillFilePath);
64
+ }
65
+ break; // found the skill dir (new or legacy), no need to check the other
66
+ }
67
+ }
68
+ return { deleted, skipped, notFound };
69
+ }
70
+ export function exportDeleteRouter() {
71
+ const router = createRouter();
72
+ router.post('/', async (req, res) => {
73
+ const { cwcFile, target } = req.body;
74
+ if (!cwcFile || !target)
75
+ return void res.status(400).json({ error: 'cwcFile and target required' });
76
+ if (target.type === 'project' && !target.projectDir)
77
+ return void res.status(400).json({ error: 'projectDir required for project target' });
78
+ try {
79
+ const result = await deleteExport(cwcFile, target);
80
+ res.json(result);
81
+ }
82
+ catch (err) {
83
+ res.status(500).json({ error: String(err) });
84
+ }
85
+ });
86
+ return router;
87
+ }
@@ -0,0 +1,71 @@
1
+ import { Router as createRouter } from 'express';
2
+ import { slugify } from '../../slugify.js';
3
+ import { buildAgentFileContent, buildWorkflowSkillContent } from '../../file-writer.js';
4
+ import { generateOrchestratorBody } from '../../prose-generator.js';
5
+ import { resolveSkill } from '../../skill-resolver.js';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ export function exportPreviewRouter() {
9
+ const router = createRouter();
10
+ router.post('/', async (req, res) => {
11
+ const { cwcFile, target } = req.body;
12
+ if (!cwcFile || !target)
13
+ return void res.status(400).json({ error: 'cwcFile and target required' });
14
+ if (target.type === 'project' && !path.isAbsolute(target.projectDir)) {
15
+ return void res.status(400).json({ error: 'projectDir must be an absolute path' });
16
+ }
17
+ try {
18
+ const warnings = [];
19
+ const workflowId = cwcFile.meta.id;
20
+ const agentsDir = target.type === 'project'
21
+ ? path.join(target.projectDir, '.claude', 'agents')
22
+ : path.join(target.userDir ?? os.homedir(), '.claude', 'agents');
23
+ const workflowSlug = 'cwc-' + slugify(cwcFile.meta.name);
24
+ const skillsDir = target.type === 'project'
25
+ ? path.join(target.projectDir, '.claude', 'skills')
26
+ : path.join(target.userDir ?? os.homedir(), '.claude', 'skills');
27
+ const files = [];
28
+ const nodeOverrides = {};
29
+ for (const node of cwcFile.nodes) {
30
+ if (node.agentRef) {
31
+ // Ref node — don't generate an agent file; collect overrides for orchestrator
32
+ for (const skillSlug of node.agent.skills ?? []) {
33
+ const r = await resolveSkill(skillSlug);
34
+ if (!r.found)
35
+ warnings.push(`Skill not found: ${skillSlug} — install it on the target machine`);
36
+ }
37
+ const hasOverrides = (node.agent.skills ?? []).length > 0
38
+ || (node.agent.tools ?? []).length > 0
39
+ || (node.agent.systemPrompt ?? '').trim().length > 0
40
+ || (node.agent.completionCriteria ?? '').trim().length > 0;
41
+ if (hasOverrides) {
42
+ nodeOverrides[node.id] = {
43
+ skills: node.agent.skills,
44
+ tools: node.agent.tools,
45
+ systemPrompt: node.agent.systemPrompt,
46
+ completionCriteria: node.agent.completionCriteria,
47
+ };
48
+ }
49
+ continue;
50
+ }
51
+ const slug = slugify(node.agent.name);
52
+ const resolvedSkills = await Promise.all((node.agent.skills ?? []).map(async (s) => {
53
+ const r = await resolveSkill(s);
54
+ if (!r.found)
55
+ warnings.push(`Skill not found: ${s} — install it on the target machine`);
56
+ return r;
57
+ }));
58
+ const content = buildAgentFileContent(node, resolvedSkills, workflowId);
59
+ files.push({ path: path.join(agentsDir, `${slug}.md`), content });
60
+ }
61
+ const orchestratorBody = generateOrchestratorBody(cwcFile.nodes, cwcFile.edges, cwcFile.meta.name, nodeOverrides);
62
+ const skillContent = buildWorkflowSkillContent(cwcFile.meta.name, cwcFile.meta.description, orchestratorBody, workflowId);
63
+ files.push({ path: path.join(skillsDir, workflowSlug, 'SKILL.md'), content: skillContent });
64
+ res.json({ files, warnings });
65
+ }
66
+ catch (err) {
67
+ res.status(500).json({ error: String(err) });
68
+ }
69
+ });
70
+ return router;
71
+ }
@@ -0,0 +1,23 @@
1
+ import { Router as createRouter } from 'express';
2
+ import { exportWorkflow } from '../../exporter.js';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ export function exportRouter() {
6
+ const router = createRouter();
7
+ router.post('/', async (req, res) => {
8
+ const { cwcFile, target, skillsDir } = req.body;
9
+ if (!cwcFile || !target)
10
+ return void res.status(400).json({ error: 'cwcFile and target required' });
11
+ const opts = {
12
+ skillsDir: skillsDir ?? path.join(os.homedir(), '.claude', 'skills'),
13
+ };
14
+ try {
15
+ const result = await exportWorkflow(cwcFile, target, opts);
16
+ res.json(result);
17
+ }
18
+ catch (err) {
19
+ res.status(500).json({ error: String(err) });
20
+ }
21
+ });
22
+ return router;
23
+ }