@crewhaus/target-claude-plugin 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.
- package/package.json +42 -0
- package/src/index.test.ts +188 -0
- package/src/index.ts +348 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/target-claude-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Track H / §59 — emits an Anthropic-compatible Claude Code plugin directory from any CrewHaus IR variant. Source: claude-plugins-official (Anthropic).",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@crewhaus/errors": "0.0.0",
|
|
16
|
+
"@crewhaus/ir": "0.0.0"
|
|
17
|
+
},
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Max Meier",
|
|
21
|
+
"email": "max@studiomax.io",
|
|
22
|
+
"url": "https://studiomax.io"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
27
|
+
"directory": "packages/target-claude-plugin"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/target-claude-plugin#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "restricted"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"NOTICE"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { IrCrewV0, IrGraphV0, IrV0, IrWorkflowV0 } from "@crewhaus/ir";
|
|
3
|
+
import { TargetClaudePluginError, emitClaudePlugin } from "./index";
|
|
4
|
+
|
|
5
|
+
const baseCli: IrV0 = {
|
|
6
|
+
version: 0,
|
|
7
|
+
name: "hello-plugin",
|
|
8
|
+
target: "cli",
|
|
9
|
+
agent: { model: "claude-sonnet-4-6", instructions: "Be helpful." },
|
|
10
|
+
tools: [],
|
|
11
|
+
toolConfigs: {},
|
|
12
|
+
mcp_servers: {},
|
|
13
|
+
permissions: { rules: [] },
|
|
14
|
+
subAgents: [],
|
|
15
|
+
compaction: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const baseWorkflow: IrWorkflowV0 = {
|
|
19
|
+
version: 0,
|
|
20
|
+
name: "summarize-then-translate",
|
|
21
|
+
target: "workflow",
|
|
22
|
+
steps: [
|
|
23
|
+
{
|
|
24
|
+
name: "summarize",
|
|
25
|
+
instructions: "Summarize the input.",
|
|
26
|
+
model: "m",
|
|
27
|
+
tools: [],
|
|
28
|
+
toolConfigs: {},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "translate",
|
|
32
|
+
instructions: "Translate the summary to French.",
|
|
33
|
+
model: "m",
|
|
34
|
+
tools: [],
|
|
35
|
+
toolConfigs: {},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
mcp_servers: {},
|
|
39
|
+
permissions: { rules: [] },
|
|
40
|
+
compaction: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const baseGraph: IrGraphV0 = {
|
|
44
|
+
version: 0,
|
|
45
|
+
name: "plan-then-execute",
|
|
46
|
+
target: "graph",
|
|
47
|
+
entry: "planner",
|
|
48
|
+
nodes: [
|
|
49
|
+
{ name: "planner", instructions: "Make a plan", model: "m", tools: [], toolConfigs: {} },
|
|
50
|
+
{ name: "executor", instructions: "Execute", model: "m", tools: [], toolConfigs: {} },
|
|
51
|
+
],
|
|
52
|
+
edges: [{ from: "planner", to: "executor" }],
|
|
53
|
+
permissions: { rules: [] },
|
|
54
|
+
compaction: {},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const baseCrew: IrCrewV0 = {
|
|
58
|
+
version: 0,
|
|
59
|
+
name: "research-crew",
|
|
60
|
+
target: "crew",
|
|
61
|
+
entry: "researcher",
|
|
62
|
+
roles: [
|
|
63
|
+
{
|
|
64
|
+
name: "researcher",
|
|
65
|
+
model: "m",
|
|
66
|
+
instructions: "Do research.",
|
|
67
|
+
tools: [],
|
|
68
|
+
toolConfigs: {},
|
|
69
|
+
subAgents: [],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "writer",
|
|
73
|
+
model: "m",
|
|
74
|
+
instructions: "Write the report.",
|
|
75
|
+
tools: [],
|
|
76
|
+
toolConfigs: {},
|
|
77
|
+
subAgents: [],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
mcp_servers: {},
|
|
81
|
+
permissions: { rules: [] },
|
|
82
|
+
compaction: {},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
describe("emitClaudePlugin — universal files", () => {
|
|
86
|
+
test("always emits plugin.json and README.md", () => {
|
|
87
|
+
const b = emitClaudePlugin(baseCli, { author: { name: "Test" } });
|
|
88
|
+
const paths = b.files.map((f) => f.path);
|
|
89
|
+
expect(paths).toContain(".claude-plugin/plugin.json");
|
|
90
|
+
expect(paths).toContain("README.md");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("plugin.json has the minimal Anthropic schema", () => {
|
|
94
|
+
const b = emitClaudePlugin(baseCli, {
|
|
95
|
+
author: { name: "Test Author", email: "x@y.z" },
|
|
96
|
+
description: "test description",
|
|
97
|
+
});
|
|
98
|
+
const file = b.files.find((f) => f.path === ".claude-plugin/plugin.json");
|
|
99
|
+
expect(file).toBeDefined();
|
|
100
|
+
const parsed = JSON.parse(file?.content ?? "{}");
|
|
101
|
+
expect(parsed.name).toBe("hello-plugin");
|
|
102
|
+
expect(parsed.description).toBe("test description");
|
|
103
|
+
expect(parsed.author.name).toBe("Test Author");
|
|
104
|
+
expect(parsed.author.email).toBe("x@y.z");
|
|
105
|
+
// No extra fields beyond what Anthropic requires.
|
|
106
|
+
expect(Object.keys(parsed).sort()).toEqual(["author", "description", "name"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("omits .mcp.json when mcp_servers is empty", () => {
|
|
110
|
+
const b = emitClaudePlugin(baseCli, { author: { name: "x" } });
|
|
111
|
+
expect(b.files.find((f) => f.path === ".mcp.json")).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("emits .mcp.json when mcp_servers is populated", () => {
|
|
115
|
+
const ir: IrV0 = {
|
|
116
|
+
...baseCli,
|
|
117
|
+
mcp_servers: {
|
|
118
|
+
fs: { transport: "stdio", command: "npx", args: ["-y", "fs"] },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const b = emitClaudePlugin(ir, { author: { name: "x" } });
|
|
122
|
+
const mcp = b.files.find((f) => f.path === ".mcp.json");
|
|
123
|
+
expect(mcp).toBeDefined();
|
|
124
|
+
expect(JSON.parse(mcp?.content ?? "{}").fs.transport).toBe("stdio");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("emitClaudePlugin — per-shape emission", () => {
|
|
129
|
+
test("cli emits one SKILL.md and one agent per sub-agent", () => {
|
|
130
|
+
const ir: IrV0 = {
|
|
131
|
+
...baseCli,
|
|
132
|
+
subAgents: [
|
|
133
|
+
{
|
|
134
|
+
name: "reviewer",
|
|
135
|
+
description: "Reviews code",
|
|
136
|
+
instructions: "Find bugs",
|
|
137
|
+
tools: [],
|
|
138
|
+
permissions: "inherit",
|
|
139
|
+
inheritBypass: false,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
const b = emitClaudePlugin(ir, { author: { name: "x" } });
|
|
144
|
+
expect(b.files.some((f) => f.path === "skills/hello-plugin/SKILL.md")).toBe(true);
|
|
145
|
+
expect(b.files.some((f) => f.path === "agents/reviewer.md")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("workflow emits one SKILL.md per step", () => {
|
|
149
|
+
const b = emitClaudePlugin(baseWorkflow, { author: { name: "x" } });
|
|
150
|
+
const skillPaths = b.files.map((f) => f.path).filter((p) => p.startsWith("skills/"));
|
|
151
|
+
expect(skillPaths).toContain("skills/summarize-then-translate-summarize/SKILL.md");
|
|
152
|
+
expect(skillPaths).toContain("skills/summarize-then-translate-translate/SKILL.md");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("graph emits one top-level SKILL.md plus one per node", () => {
|
|
156
|
+
const b = emitClaudePlugin(baseGraph, { author: { name: "x" } });
|
|
157
|
+
const skillPaths = b.files.map((f) => f.path).filter((p) => p.startsWith("skills/"));
|
|
158
|
+
expect(skillPaths).toContain("skills/plan-then-execute/SKILL.md");
|
|
159
|
+
expect(skillPaths).toContain("skills/plan-then-execute-planner/SKILL.md");
|
|
160
|
+
expect(skillPaths).toContain("skills/plan-then-execute-executor/SKILL.md");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("crew emits SKILL.md for entry role and one agent per role", () => {
|
|
164
|
+
const b = emitClaudePlugin(baseCrew, { author: { name: "x" } });
|
|
165
|
+
expect(b.files.some((f) => f.path === "skills/research-crew/SKILL.md")).toBe(true);
|
|
166
|
+
expect(b.files.some((f) => f.path === "agents/researcher.md")).toBe(true);
|
|
167
|
+
expect(b.files.some((f) => f.path === "agents/writer.md")).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("SKILL.md frontmatter", () => {
|
|
172
|
+
test("uses minimal name + description frontmatter", () => {
|
|
173
|
+
const b = emitClaudePlugin(baseCli, { author: { name: "x" } });
|
|
174
|
+
const skill = b.files.find((f) => f.path === "skills/hello-plugin/SKILL.md");
|
|
175
|
+
expect(skill?.content).toMatch(/^---\nname: hello-plugin\ndescription: /);
|
|
176
|
+
expect(skill?.content).toContain("Be helpful.");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("emitClaudePlugin — error handling", () => {
|
|
181
|
+
test("throws on unsupported target shape", () => {
|
|
182
|
+
expect(() =>
|
|
183
|
+
emitClaudePlugin({ target: "unknown-shape" } as never, {
|
|
184
|
+
author: { name: "x" },
|
|
185
|
+
}),
|
|
186
|
+
).toThrow(TargetClaudePluginError);
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track H (§59) — `target-claude-plugin`. Emits an Anthropic-compatible
|
|
3
|
+
* Claude Code plugin directory from any CrewHaus IR variant.
|
|
4
|
+
*
|
|
5
|
+
* Source: claude-plugins-official (Anthropic's reference repo at
|
|
6
|
+
* github.com/anthropics/claude-code-plugins). Anthropic's reference
|
|
7
|
+
* plugin format is intentionally minimal:
|
|
8
|
+
*
|
|
9
|
+
* plugin-name/
|
|
10
|
+
* ├── .claude-plugin/
|
|
11
|
+
* │ └── plugin.json # required: name, description, author
|
|
12
|
+
* ├── .mcp.json # optional, MCP server config
|
|
13
|
+
* ├── skills/<name>/SKILL.md
|
|
14
|
+
* ├── agents/<name>.md # optional sub-agent definitions
|
|
15
|
+
* ├── commands/<name>.md # optional legacy slash entries
|
|
16
|
+
* └── README.md # documentation
|
|
17
|
+
*
|
|
18
|
+
* The emitter is shape-aware:
|
|
19
|
+
*
|
|
20
|
+
* - `cli` / `channel` / `managed` / `pipeline` / `research` / `batch` /
|
|
21
|
+
* `voice` / `browser` / `eval` / `onchain` / `onchain-game` →
|
|
22
|
+
* produce a single SKILL.md derived from agent.instructions, plus
|
|
23
|
+
* sub-agent .md files for each declared sub-agent.
|
|
24
|
+
* - `workflow` → one SKILL.md per step.
|
|
25
|
+
* - `graph` → one SKILL.md per node, plus a top-level SKILL.md naming
|
|
26
|
+
* the entry node.
|
|
27
|
+
* - `crew` → one agent .md per role; the entry role becomes the
|
|
28
|
+
* primary SKILL.md.
|
|
29
|
+
*
|
|
30
|
+
* This is a strict-emission package: no I/O, returns a `Bundle` of
|
|
31
|
+
* files for the caller (the CLI) to write to disk. Pure functions.
|
|
32
|
+
*
|
|
33
|
+
* See Track H of the §55–§59 batch in factory/CHANGELOG.md.
|
|
34
|
+
*/
|
|
35
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
36
|
+
import type {
|
|
37
|
+
IrChannelV0,
|
|
38
|
+
IrCrewV0,
|
|
39
|
+
IrEvalV0,
|
|
40
|
+
IrGraphV0,
|
|
41
|
+
IrManagedV0,
|
|
42
|
+
IrNode,
|
|
43
|
+
IrPipelineV0,
|
|
44
|
+
IrResearchV0,
|
|
45
|
+
IrV0,
|
|
46
|
+
IrWorkflowV0,
|
|
47
|
+
} from "@crewhaus/ir";
|
|
48
|
+
|
|
49
|
+
export class TargetClaudePluginError extends CrewhausError {
|
|
50
|
+
override readonly name = "TargetClaudePluginError";
|
|
51
|
+
constructor(message: string, cause?: unknown) {
|
|
52
|
+
super("compiler", message, cause);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type PluginFile = {
|
|
57
|
+
readonly path: string;
|
|
58
|
+
readonly content: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type PluginBundle = {
|
|
62
|
+
readonly files: ReadonlyArray<PluginFile>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type EmitClaudePluginOptions = {
|
|
66
|
+
/** Plugin author (required by Anthropic's minimal schema). */
|
|
67
|
+
readonly author: {
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly email?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Optional human-readable description; defaults to the IR's name + target.
|
|
73
|
+
* Anthropic recommends 1-2 sentences describing trigger conditions.
|
|
74
|
+
*/
|
|
75
|
+
readonly description?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Main entry. Returns a `PluginBundle` of files; the caller writes
|
|
80
|
+
* them to disk under the chosen plugin directory.
|
|
81
|
+
*/
|
|
82
|
+
export function emitClaudePlugin(ir: IrNode, opts: EmitClaudePluginOptions): PluginBundle {
|
|
83
|
+
const description = opts.description ?? defaultDescription(ir);
|
|
84
|
+
const pluginJson = renderPluginJson(ir.name, description, opts.author);
|
|
85
|
+
const files: PluginFile[] = [
|
|
86
|
+
{ path: ".claude-plugin/plugin.json", content: pluginJson },
|
|
87
|
+
{ path: "README.md", content: renderReadme(ir, description) },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// MCP servers — only emitted for variants that carry them in IR.
|
|
91
|
+
const mcp = renderMcpJson(ir);
|
|
92
|
+
if (mcp !== undefined) {
|
|
93
|
+
files.push({ path: ".mcp.json", content: mcp });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Per-variant skill + agent emission.
|
|
97
|
+
switch (ir.target) {
|
|
98
|
+
case "cli":
|
|
99
|
+
files.push(...emitCliShape(ir as IrV0));
|
|
100
|
+
break;
|
|
101
|
+
case "workflow":
|
|
102
|
+
files.push(...emitWorkflowShape(ir as IrWorkflowV0));
|
|
103
|
+
break;
|
|
104
|
+
case "channel":
|
|
105
|
+
files.push(...emitChannelShape(ir as IrChannelV0));
|
|
106
|
+
break;
|
|
107
|
+
case "graph":
|
|
108
|
+
files.push(...emitGraphShape(ir as IrGraphV0));
|
|
109
|
+
break;
|
|
110
|
+
case "crew":
|
|
111
|
+
files.push(...emitCrewShape(ir as IrCrewV0));
|
|
112
|
+
break;
|
|
113
|
+
case "managed":
|
|
114
|
+
files.push(...emitGenericAgentShape(ir as IrManagedV0, ir.target));
|
|
115
|
+
break;
|
|
116
|
+
case "pipeline":
|
|
117
|
+
files.push(...emitGenericAgentShape(ir as IrPipelineV0, ir.target));
|
|
118
|
+
break;
|
|
119
|
+
case "research":
|
|
120
|
+
files.push(...emitGenericAgentShape(ir as IrResearchV0, ir.target));
|
|
121
|
+
break;
|
|
122
|
+
case "eval":
|
|
123
|
+
files.push(...emitEvalShape(ir as IrEvalV0));
|
|
124
|
+
break;
|
|
125
|
+
case "batch":
|
|
126
|
+
case "voice":
|
|
127
|
+
case "browser":
|
|
128
|
+
case "onchain":
|
|
129
|
+
case "onchain-game":
|
|
130
|
+
files.push(...emitGenericAgentShape(ir, ir.target));
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
throw new TargetClaudePluginError(
|
|
134
|
+
`unsupported IR target for claude-plugin emission: ${(ir as { target: string }).target}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return { files };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function defaultDescription(ir: IrNode): string {
|
|
141
|
+
return `${ir.name} — CrewHaus ${ir.target} agent compiled to Claude Code plugin format.`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderPluginJson(
|
|
145
|
+
name: string,
|
|
146
|
+
description: string,
|
|
147
|
+
author: EmitClaudePluginOptions["author"],
|
|
148
|
+
): string {
|
|
149
|
+
const obj = {
|
|
150
|
+
name,
|
|
151
|
+
description,
|
|
152
|
+
author: {
|
|
153
|
+
name: author.name,
|
|
154
|
+
...(author.email !== undefined ? { email: author.email } : {}),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderReadme(ir: IrNode, description: string): string {
|
|
161
|
+
return [
|
|
162
|
+
`# ${ir.name}`,
|
|
163
|
+
"",
|
|
164
|
+
description,
|
|
165
|
+
"",
|
|
166
|
+
"## Origin",
|
|
167
|
+
"",
|
|
168
|
+
`Generated by CrewHaus \`target-claude-plugin\` from a \`target: ${ir.target}\` spec.`,
|
|
169
|
+
"Format reference: [claude-plugins-official](https://github.com/anthropics/claude-code-plugins).",
|
|
170
|
+
"",
|
|
171
|
+
"## Install",
|
|
172
|
+
"",
|
|
173
|
+
"Drop this directory under `~/.claude/plugins/` or your project's `.claude/plugins/`.",
|
|
174
|
+
"",
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderMcpJson(ir: IrNode): string | undefined {
|
|
179
|
+
// Only emit when the IR variant has mcp_servers and it's non-empty.
|
|
180
|
+
const v = ir as { mcp_servers?: Record<string, unknown> };
|
|
181
|
+
if (v.mcp_servers === undefined) return undefined;
|
|
182
|
+
const keys = Object.keys(v.mcp_servers);
|
|
183
|
+
if (keys.length === 0) return undefined;
|
|
184
|
+
return `${JSON.stringify(v.mcp_servers, null, 2)}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Render a SKILL.md frontmatter block. Anthropic's minimal schema:
|
|
189
|
+
* `name` + `description` required. Optional `argument-hint` makes
|
|
190
|
+
* the skill user-invokable as a slash command.
|
|
191
|
+
*/
|
|
192
|
+
function renderSkill(opts: {
|
|
193
|
+
readonly name: string;
|
|
194
|
+
readonly description: string;
|
|
195
|
+
readonly body: string;
|
|
196
|
+
readonly argumentHint?: string;
|
|
197
|
+
}): string {
|
|
198
|
+
const frontmatter: string[] = ["---", `name: ${opts.name}`, `description: ${opts.description}`];
|
|
199
|
+
if (opts.argumentHint !== undefined) {
|
|
200
|
+
frontmatter.push(`argument-hint: ${opts.argumentHint}`);
|
|
201
|
+
}
|
|
202
|
+
frontmatter.push("---");
|
|
203
|
+
return `${frontmatter.join("\n")}\n\n${opts.body}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function emitCliShape(ir: IrV0): PluginFile[] {
|
|
207
|
+
const files: PluginFile[] = [
|
|
208
|
+
{
|
|
209
|
+
path: `skills/${ir.name}/SKILL.md`,
|
|
210
|
+
content: renderSkill({
|
|
211
|
+
name: ir.name,
|
|
212
|
+
description: firstSentence(ir.agent.instructions) || defaultDescription(ir),
|
|
213
|
+
body: ir.agent.instructions,
|
|
214
|
+
}),
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
for (const sa of ir.subAgents ?? []) {
|
|
218
|
+
files.push({
|
|
219
|
+
path: `agents/${sa.name}.md`,
|
|
220
|
+
content: renderAgent(sa.name, sa.description, sa.instructions),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return files;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function emitWorkflowShape(ir: IrWorkflowV0): PluginFile[] {
|
|
227
|
+
const files: PluginFile[] = [];
|
|
228
|
+
for (const step of ir.steps) {
|
|
229
|
+
files.push({
|
|
230
|
+
path: `skills/${ir.name}-${step.name}/SKILL.md`,
|
|
231
|
+
content: renderSkill({
|
|
232
|
+
name: `${ir.name}-${step.name}`,
|
|
233
|
+
description: `Step ${step.name} of workflow ${ir.name}`,
|
|
234
|
+
body: step.instructions,
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return files;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function emitChannelShape(ir: IrChannelV0): PluginFile[] {
|
|
242
|
+
const files = emitCliShape({
|
|
243
|
+
...(ir as unknown as IrV0),
|
|
244
|
+
target: "cli",
|
|
245
|
+
} as IrV0);
|
|
246
|
+
// Channel daemons aren't natively claude-plugin-shaped; we emit the
|
|
247
|
+
// skill content but flag the channel context in the README.
|
|
248
|
+
files.push({
|
|
249
|
+
path: "CLAUDE_PLUGIN_NOTES.md",
|
|
250
|
+
content: [
|
|
251
|
+
"# Channel daemon notes",
|
|
252
|
+
"",
|
|
253
|
+
"This plugin was emitted from a `target: channel` spec. The channel " +
|
|
254
|
+
"daemon (Slack/Telegram/Discord/etc.) lifecycle is NOT part of the " +
|
|
255
|
+
"Claude Code plugin runtime — re-deploy the channel separately and " +
|
|
256
|
+
"use this plugin's skills inside Claude Code for design-time work.",
|
|
257
|
+
"",
|
|
258
|
+
].join("\n"),
|
|
259
|
+
});
|
|
260
|
+
return files;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function emitGraphShape(ir: IrGraphV0): PluginFile[] {
|
|
264
|
+
const files: PluginFile[] = [
|
|
265
|
+
{
|
|
266
|
+
path: `skills/${ir.name}/SKILL.md`,
|
|
267
|
+
content: renderSkill({
|
|
268
|
+
name: ir.name,
|
|
269
|
+
description: `Stateful graph; entry node = "${ir.entry}".`,
|
|
270
|
+
body: `Graph nodes: ${ir.nodes.map((n) => n.name).join(", ")}\nEdges: ${ir.edges
|
|
271
|
+
.map((e) => `${e.from}→${e.to}`)
|
|
272
|
+
.join(", ")}`,
|
|
273
|
+
}),
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
for (const node of ir.nodes) {
|
|
277
|
+
files.push({
|
|
278
|
+
path: `skills/${ir.name}-${node.name}/SKILL.md`,
|
|
279
|
+
content: renderSkill({
|
|
280
|
+
name: `${ir.name}-${node.name}`,
|
|
281
|
+
description: `Graph node "${node.name}" (entry=${node.name === ir.entry})`,
|
|
282
|
+
body: node.instructions,
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return files;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function emitCrewShape(ir: IrCrewV0): PluginFile[] {
|
|
290
|
+
const files: PluginFile[] = [];
|
|
291
|
+
for (const role of ir.roles) {
|
|
292
|
+
if (role.name === ir.entry) {
|
|
293
|
+
files.push({
|
|
294
|
+
path: `skills/${ir.name}/SKILL.md`,
|
|
295
|
+
content: renderSkill({
|
|
296
|
+
name: ir.name,
|
|
297
|
+
description: `Entry role "${role.name}" of crew ${ir.name}`,
|
|
298
|
+
body: role.instructions,
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
files.push({
|
|
303
|
+
path: `agents/${role.name}.md`,
|
|
304
|
+
content: renderAgent(role.name, `Role: ${role.name}`, role.instructions),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return files;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function emitEvalShape(ir: IrEvalV0): PluginFile[] {
|
|
311
|
+
return [
|
|
312
|
+
{
|
|
313
|
+
path: `skills/${ir.name}/SKILL.md`,
|
|
314
|
+
content: renderSkill({
|
|
315
|
+
name: ir.name,
|
|
316
|
+
description: `Eval harness over dataset ${ir.dataset.name} (${ir.dataset.split})`,
|
|
317
|
+
body: ir.agent.instructions,
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function emitGenericAgentShape(
|
|
324
|
+
ir: { name: string; target: string; agent: { instructions: string } },
|
|
325
|
+
target: string,
|
|
326
|
+
): PluginFile[] {
|
|
327
|
+
return [
|
|
328
|
+
{
|
|
329
|
+
path: `skills/${ir.name}/SKILL.md`,
|
|
330
|
+
content: renderSkill({
|
|
331
|
+
name: ir.name,
|
|
332
|
+
description: `${target} agent — see body for instructions.`,
|
|
333
|
+
body: ir.agent.instructions,
|
|
334
|
+
}),
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function renderAgent(name: string, description: string, instructions: string): string {
|
|
340
|
+
return ["---", `name: ${name}`, `description: ${description}`, "---", "", instructions, ""].join(
|
|
341
|
+
"\n",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function firstSentence(text: string): string {
|
|
346
|
+
const m = text.match(/^[^.\n]+[.!?]/);
|
|
347
|
+
return m !== null ? m[0] : (text.split("\n")[0] ?? "");
|
|
348
|
+
}
|