@crewhaus/target-claude-plugin 0.1.0 → 0.1.2

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 (2) hide show
  1. package/package.json +7 -12
  2. package/src/index.test.ts +141 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/target-claude-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Track H / §59 — emits an Anthropic-compatible Claude Code plugin directory from any CrewHaus IR variant. Source: claude-plugins-official (Anthropic).",
6
6
  "main": "src/index.ts",
@@ -12,14 +12,14 @@
12
12
  "test": "bun test src"
13
13
  },
14
14
  "dependencies": {
15
- "@crewhaus/errors": "0.0.0",
16
- "@crewhaus/ir": "0.0.0"
15
+ "@crewhaus/errors": "0.1.2",
16
+ "@crewhaus/ir": "0.1.2"
17
17
  },
18
18
  "license": "Apache-2.0",
19
19
  "author": {
20
20
  "name": "Max Meier",
21
- "email": "max@studiomax.io",
22
- "url": "https://studiomax.io"
21
+ "email": "max@crewhaus.ai",
22
+ "url": "https://crewhaus.ai"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -31,12 +31,7 @@
31
31
  "url": "https://github.com/crewhaus/factory/issues"
32
32
  },
33
33
  "publishConfig": {
34
- "access": "restricted"
34
+ "access": "public"
35
35
  },
36
- "files": [
37
- "src",
38
- "README.md",
39
- "LICENSE",
40
- "NOTICE"
41
- ]
36
+ "files": ["src", "README.md", "LICENSE", "NOTICE"]
42
37
  }
package/src/index.test.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import type { IrCrewV0, IrGraphV0, IrV0, IrWorkflowV0 } from "@crewhaus/ir";
2
+ import type {
3
+ IrChannelV0,
4
+ IrCrewV0,
5
+ IrEvalV0,
6
+ IrGraphV0,
7
+ IrManagedV0,
8
+ IrNode,
9
+ IrV0,
10
+ IrWorkflowV0,
11
+ } from "@crewhaus/ir";
3
12
  import { TargetClaudePluginError, emitClaudePlugin } from "./index";
4
13
 
5
14
  const baseCli: IrV0 = {
@@ -177,6 +186,137 @@ describe("SKILL.md frontmatter", () => {
177
186
  });
178
187
  });
179
188
 
189
+ describe("emitClaudePlugin — channel shape", () => {
190
+ const baseChannel: IrChannelV0 = {
191
+ version: 0,
192
+ name: "slackbot",
193
+ target: "channel",
194
+ agent: { model: "claude-sonnet-4-6", instructions: "Greet users warmly. Then help them." },
195
+ tools: [],
196
+ toolConfigs: {},
197
+ channels: {
198
+ slack: {
199
+ botToken: { kind: "literal", value: "xoxb-fake" },
200
+ signingSecret: { kind: "env", name: "SLACK_SIGNING_SECRET" },
201
+ },
202
+ },
203
+ routing: { sessionKey: "thread" },
204
+ mcp_servers: {},
205
+ permissions: { rules: [] },
206
+ subAgents: [],
207
+ compaction: {},
208
+ };
209
+
210
+ test("emits the CLI-shaped SKILL.md from agent.instructions", () => {
211
+ const b = emitClaudePlugin(baseChannel, { author: { name: "x" } });
212
+ const skill = b.files.find((f) => f.path === "skills/slackbot/SKILL.md");
213
+ expect(skill).toBeDefined();
214
+ // description is the first sentence of the instructions.
215
+ expect(skill?.content).toContain("Greet users warmly.");
216
+ expect(skill?.content).toContain("Then help them.");
217
+ });
218
+
219
+ test("appends a CLAUDE_PLUGIN_NOTES.md flagging the channel daemon context", () => {
220
+ const b = emitClaudePlugin(baseChannel, { author: { name: "x" } });
221
+ const notes = b.files.find((f) => f.path === "CLAUDE_PLUGIN_NOTES.md");
222
+ expect(notes).toBeDefined();
223
+ expect(notes?.content).toContain("# Channel daemon notes");
224
+ expect(notes?.content).toContain("target: channel");
225
+ expect(notes?.content).toContain("lifecycle is NOT part of the");
226
+ });
227
+
228
+ test("forwards channel sub-agents into agents/<name>.md", () => {
229
+ const withSub: IrChannelV0 = {
230
+ ...baseChannel,
231
+ subAgents: [
232
+ {
233
+ name: "triage",
234
+ description: "Triage incoming messages",
235
+ instructions: "Sort by urgency",
236
+ tools: [],
237
+ permissions: "inherit",
238
+ inheritBypass: false,
239
+ },
240
+ ],
241
+ };
242
+ const b = emitClaudePlugin(withSub, { author: { name: "x" } });
243
+ const agent = b.files.find((f) => f.path === "agents/triage.md");
244
+ expect(agent).toBeDefined();
245
+ expect(agent?.content).toContain("name: triage");
246
+ expect(agent?.content).toContain("Sort by urgency");
247
+ });
248
+ });
249
+
250
+ describe("emitClaudePlugin — eval shape", () => {
251
+ const baseEval: IrEvalV0 = {
252
+ version: 0,
253
+ name: "qa-eval",
254
+ target: "eval",
255
+ agent: { model: "claude-sonnet-4-6", instructions: "Answer concisely.", tools: [] },
256
+ dataset: { name: "qa-bench", version: "v1", split: "dev" },
257
+ graders: [{ name: "exact_match" }],
258
+ concurrency: 4,
259
+ };
260
+
261
+ test("emits a single SKILL.md naming the dataset + split in its description", () => {
262
+ const b = emitClaudePlugin(baseEval, { author: { name: "x" } });
263
+ const skill = b.files.find((f) => f.path === "skills/qa-eval/SKILL.md");
264
+ expect(skill).toBeDefined();
265
+ expect(skill?.content).toContain("Eval harness over dataset qa-bench (dev)");
266
+ expect(skill?.content).toContain("Answer concisely.");
267
+ // eval shape emits exactly one skill (no per-grader files).
268
+ const skillPaths = b.files.map((f) => f.path).filter((p) => p.startsWith("skills/"));
269
+ expect(skillPaths).toEqual(["skills/qa-eval/SKILL.md"]);
270
+ });
271
+ });
272
+
273
+ describe("emitClaudePlugin — generic agent shapes", () => {
274
+ const managed: IrManagedV0 = {
275
+ version: 0,
276
+ name: "saas-bot",
277
+ target: "managed",
278
+ agent: { model: "claude-sonnet-4-6", instructions: "Serve every tenant." },
279
+ tenants: [],
280
+ permissions: { rules: [] },
281
+ compaction: {},
282
+ };
283
+
284
+ test("managed emits one SKILL.md with a `<target> agent` description", () => {
285
+ const b = emitClaudePlugin(managed, { author: { name: "x" } });
286
+ const skill = b.files.find((f) => f.path === "skills/saas-bot/SKILL.md");
287
+ expect(skill).toBeDefined();
288
+ expect(skill?.content).toContain("managed agent — see body for instructions.");
289
+ expect(skill?.content).toContain("Serve every tenant.");
290
+ });
291
+
292
+ // pipeline / research route through the same generic helper but with a
293
+ // dedicated switch arm; batch / voice / browser / onchain / onchain-game
294
+ // share a single arm. Exercise every arm so each `case` is covered.
295
+ test.each([
296
+ "pipeline",
297
+ "research",
298
+ "batch",
299
+ "voice",
300
+ "browser",
301
+ "onchain",
302
+ "onchain-game",
303
+ ] as const)("%s target emits a generic SKILL.md", (target) => {
304
+ const ir = {
305
+ version: 0,
306
+ name: `${target}-agent`,
307
+ target,
308
+ agent: { model: "m", instructions: `Run the ${target}.` },
309
+ permissions: { rules: [] },
310
+ compaction: {},
311
+ } as unknown as IrNode;
312
+ const b = emitClaudePlugin(ir, { author: { name: "x" } });
313
+ const skill = b.files.find((f) => f.path === `skills/${target}-agent/SKILL.md`);
314
+ expect(skill).toBeDefined();
315
+ expect(skill?.content).toContain(`${target} agent — see body for instructions.`);
316
+ expect(skill?.content).toContain(`Run the ${target}.`);
317
+ });
318
+ });
319
+
180
320
  describe("emitClaudePlugin — error handling", () => {
181
321
  test("throws on unsupported target shape", () => {
182
322
  expect(() =>