@rayburst/cc 3.1.4 → 3.1.5

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Always-on product context for Claude Code — features, acceptance criteria, and board cards injected automatically.",
9
- "version": "3.1.4"
9
+ "version": "3.1.5"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "rayburst",
14
14
  "source": "./",
15
15
  "description": "Always-on product context for Claude Code. Automatically injects your Rayburst Product Requirement Registry (features, Gherkin acceptance criteria, board cards) into every coding session.",
16
- "version": "3.1.4",
16
+ "version": "3.1.5",
17
17
  "author": {
18
18
  "name": "Rayburst"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayburst",
3
- "version": "3.1.4",
3
+ "version": "3.1.5",
4
4
  "description": "Always-on product context for Claude Code. Automatically injects feature atlas and acceptance criteria into every coding session — no slash commands needed.",
5
5
  "author": {
6
6
  "name": "Rayburst",
@@ -1,17 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PreToolUse hook for the Rayburst plugin.
5
- *
6
- * Triggered on Write and Edit tool calls. Reads the active feature
7
- * from cache and injects a coding reminder with criteria checklist.
8
- */
3
+ // src/hooks/rb-cache.ts
4
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
5
+ import { createHash } from "node:crypto";
6
+ function getProjectDir() {
7
+ return process.env["CLAUDE_PROJECT_DIR"] || process.env["CONTEXT_MODE_PROJECT_DIR"] || process.cwd();
8
+ }
9
+ function getProjectHash() {
10
+ return createHash("md5").update(getProjectDir()).digest("hex").slice(0, 12);
11
+ }
12
+ function getCachePath(type) {
13
+ return `/tmp/rb-${type}-${getProjectHash()}.json`;
14
+ }
15
+ function readCache(type) {
16
+ try {
17
+ const path = getCachePath(type);
18
+ if (!existsSync(path)) return null;
19
+ const raw = readFileSync(path, "utf-8");
20
+ return JSON.parse(raw);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
9
25
 
10
- import { readCache } from "./rb-cache.mjs";
11
- import { buildCodingReminderBlock } from "./product-context-block.mjs";
26
+ // src/hooks/product-context-block.ts
27
+ function buildCodingReminderBlock(activeFeature2, filePath2, relatedFeatures2) {
28
+ if (!activeFeature2) return "";
29
+ const criteria = (activeFeature2.criteria || []).map((c) => ` - [ ] ${escapeXml(c.title || c.description || "")}`).join("\n");
30
+ let relatedNote = "";
31
+ if (relatedFeatures2 && relatedFeatures2.length > 0) {
32
+ const names = relatedFeatures2.map((f) => `"${escapeXml(f.title)}"`).join(", ");
33
+ relatedNote = `
34
+ <related_features>This file may also relate to: ${names}. Check that your changes don't break their criteria.</related_features>`;
35
+ }
36
+ return `<rayburst_coding_reminder>
37
+ <active_feature>${escapeXml(activeFeature2.title)} (${activeFeature2.id})</active_feature>
38
+ <criteria_checklist>
39
+ ${criteria}
40
+ </criteria_checklist>
41
+ <file>${escapeXml(filePath2 || "unknown")}</file>${relatedNote}
42
+ <post_implementation_required>After writing this code, you MUST update Rayburst in the same response: add/update criteria for any new behaviors via rb_add_criterion, update the feature description if it changed via rb_update_feature. Do NOT skip this step.</post_implementation_required>
43
+ </rayburst_coding_reminder>`;
44
+ }
45
+ function escapeXml(str) {
46
+ return (str || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
47
+ }
12
48
 
13
- // Read stdin for the hook input
14
- let input = "";
49
+ // src/hooks/pretooluse.ts
50
+ var input = "";
15
51
  try {
16
52
  const chunks = [];
17
53
  for await (const chunk of process.stdin) {
@@ -21,67 +57,48 @@ try {
21
57
  } catch {
22
58
  process.exit(0);
23
59
  }
24
-
25
- let hookInput;
60
+ var hookInput;
26
61
  try {
27
62
  hookInput = JSON.parse(input);
28
63
  } catch {
29
64
  process.exit(0);
30
65
  }
31
-
32
- const toolName = hookInput?.input?.tool_name;
33
- const toolInput = hookInput?.input?.tool_input;
34
-
35
- // Only act on Write and Edit
66
+ var toolName = hookInput?.input?.tool_name;
67
+ var toolInput = hookInput?.input?.tool_input;
36
68
  if (toolName !== "Write" && toolName !== "Edit") {
37
69
  process.exit(0);
38
70
  }
39
-
40
- // Read active feature from cache
41
- const activeFeature = readCache("active-feature");
71
+ var activeFeature = readCache("active-feature");
42
72
  if (!activeFeature) {
43
73
  process.exit(0);
44
74
  }
45
-
46
- // Get the file path being edited
47
- const filePath = toolInput?.file_path || toolInput?.path || "";
48
-
49
- // Check if other features mention this file path (basic matching)
50
- const featureList = readCache("features") || [];
51
- const relatedFeatures = [];
52
-
75
+ var filePath = toolInput?.file_path || toolInput?.path || "";
76
+ var featureList = readCache("features") || [];
77
+ var relatedFeatures = [];
53
78
  if (filePath) {
54
79
  const fileBasename = filePath.split("/").pop() || "";
55
80
  const fileDir = filePath.split("/").slice(-2, -1)[0] || "";
56
-
57
81
  for (const f of featureList) {
58
82
  if (f.id === activeFeature.id) continue;
59
83
  const desc = (f.description || "").toLowerCase();
60
84
  const title = (f.title || "").toLowerCase();
61
- if (
62
- desc.includes(fileBasename.toLowerCase()) ||
63
- desc.includes(fileDir.toLowerCase()) ||
64
- title.includes(fileBasename.toLowerCase().replace(/\.\w+$/, ""))
65
- ) {
85
+ if (desc.includes(fileBasename.toLowerCase()) || desc.includes(fileDir.toLowerCase()) || title.includes(fileBasename.toLowerCase().replace(/\.\w+$/, ""))) {
66
86
  relatedFeatures.push(f);
67
87
  }
68
88
  }
69
89
  }
70
-
71
- // Build and inject the Coding Reminder Block
72
- const contextBlock = buildCodingReminderBlock(
90
+ var contextBlock = buildCodingReminderBlock(
73
91
  activeFeature,
74
92
  filePath,
75
93
  relatedFeatures.slice(0, 3)
76
94
  );
77
-
78
95
  if (contextBlock) {
79
96
  console.log(
80
97
  JSON.stringify({
81
98
  hookSpecificOutput: {
82
99
  hookEventName: "PreToolUse",
83
- additionalContext: contextBlock,
84
- },
100
+ additionalContext: contextBlock
101
+ }
85
102
  })
86
103
  );
87
104
  }
@@ -1,59 +1,206 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * SessionStart hook for the Rayburst plugin.
5
- *
6
- * Fetches features + cards from the Rayburst API and injects a
7
- * Product Context Block as additionalContext. Also writes a feature
8
- * cache for UserPromptSubmit and PreToolUse hooks to read.
9
- */
3
+ // src/hooks/rb-cache.ts
4
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { createHash } from "node:crypto";
7
+ function getProjectDir() {
8
+ return process.env["CLAUDE_PROJECT_DIR"] || process.env["CONTEXT_MODE_PROJECT_DIR"] || process.cwd();
9
+ }
10
+ function getProjectHash() {
11
+ return createHash("md5").update(getProjectDir()).digest("hex").slice(0, 12);
12
+ }
13
+ function getCachePath(type) {
14
+ return `/tmp/rb-${type}-${getProjectHash()}.json`;
15
+ }
16
+ function writeCache(type, data) {
17
+ try {
18
+ const path = getCachePath(type);
19
+ writeFileSync(path, JSON.stringify(data), "utf-8");
20
+ } catch {
21
+ }
22
+ }
23
+ function readConfig() {
24
+ const projectDir = getProjectDir();
25
+ const configPath = resolve(projectDir, ".claude", "rb-config.md");
26
+ if (!existsSync(configPath)) return null;
27
+ const content = readFileSync(configPath, "utf-8");
28
+ function parseField(section, key) {
29
+ const sectionMatch = content.match(
30
+ new RegExp(`## ${section}[\\s\\S]*?(?=\\n## |$)`)
31
+ );
32
+ if (!sectionMatch) return null;
33
+ if (key) {
34
+ const lineMatch = sectionMatch[0].match(
35
+ new RegExp(`-\\s*${key}:\\s*(.+)`, "i")
36
+ );
37
+ if (lineMatch) return lineMatch[1].trim();
38
+ }
39
+ const lines = sectionMatch[0].split("\n").filter((l) => l.trim() && !l.startsWith("#"));
40
+ return lines[0]?.trim() || null;
41
+ }
42
+ const apiKey = process.env["RAYBURST_API_KEY"] || parseField("API", "API Key");
43
+ const apiUrl = process.env["RAYBURST_API_URL"] || parseField("API", "API URL") || "https://api.rayburst.app/api/v1/mcp";
44
+ const agentId = process.env["RAYBURST_AGENT_ID"];
45
+ const boardId = parseField("Board", "ID");
46
+ const boardSlug = parseField("Board", "Slug");
47
+ const frontendProjectId = parseField("Projects", "Frontend");
48
+ const backendProjectId = parseField("Projects", "Backend");
49
+ const projectUrl = parseField("Project URL", null);
50
+ if (!apiKey) return null;
51
+ return {
52
+ apiKey,
53
+ apiUrl,
54
+ agentId: agentId || void 0,
55
+ boardId: boardId || void 0,
56
+ boardSlug: boardSlug || void 0,
57
+ frontendProjectId: frontendProjectId || void 0,
58
+ backendProjectId: backendProjectId || void 0,
59
+ projectUrl: projectUrl || void 0
60
+ };
61
+ }
62
+ var requestId = 0;
63
+ async function mcpCall(config2, toolName, args = {}) {
64
+ const headers = {
65
+ "Content-Type": "application/json",
66
+ Accept: "application/json, text/event-stream",
67
+ Authorization: `Bearer ${config2.apiKey}`
68
+ };
69
+ if (config2.agentId) headers["X-Agent-Id"] = config2.agentId;
70
+ const controller = new AbortController();
71
+ const timeout = setTimeout(() => controller.abort(), 3e3);
72
+ try {
73
+ const res = await fetch(config2.apiUrl, {
74
+ method: "POST",
75
+ headers,
76
+ body: JSON.stringify({
77
+ jsonrpc: "2.0",
78
+ id: ++requestId,
79
+ method: "tools/call",
80
+ params: { name: toolName, arguments: args }
81
+ }),
82
+ signal: controller.signal
83
+ });
84
+ clearTimeout(timeout);
85
+ if (!res.ok) return null;
86
+ const contentType = res.headers.get("content-type") || "";
87
+ if (contentType.includes("text/event-stream")) {
88
+ const text = await res.text();
89
+ const lines = text.split("\n");
90
+ let lastData = null;
91
+ for (const line of lines) {
92
+ if (line.startsWith("data: ")) lastData = line.slice(6);
93
+ }
94
+ if (!lastData) return null;
95
+ try {
96
+ const parsed = JSON.parse(lastData);
97
+ return parsed.result ?? parsed;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ const json = await res.json();
103
+ return json.result ?? json;
104
+ } catch {
105
+ clearTimeout(timeout);
106
+ return null;
107
+ }
108
+ }
109
+ function extractData(result) {
110
+ if (!result) return null;
111
+ const r = result;
112
+ if (r.content && Array.isArray(r.content)) {
113
+ const textItem = r.content.find((c) => c.type === "text");
114
+ if (textItem) {
115
+ try {
116
+ return JSON.parse(textItem.text);
117
+ } catch {
118
+ return textItem.text;
119
+ }
120
+ }
121
+ }
122
+ return result;
123
+ }
124
+
125
+ // src/hooks/product-context-block.ts
126
+ function buildProductContextBlock(features, cards) {
127
+ const featureList = Array.isArray(features) ? features : features?.data ?? [];
128
+ const cardList = Array.isArray(cards) ? cards : cards?.data ?? [];
129
+ const counts = { draft: 0, active: 0, completed: 0, archived: 0 };
130
+ for (const f of featureList) {
131
+ const status = f.status || "draft";
132
+ counts[status] = (counts[status] || 0) + 1;
133
+ }
134
+ const total = featureList.length;
135
+ const cardCounts = {};
136
+ for (const c of cardList) {
137
+ const status = c.status || "draft";
138
+ cardCounts[status] = (cardCounts[status] || 0) + 1;
139
+ }
140
+ const featureLines = featureList.slice(0, 60).map((f) => {
141
+ const critCount = f.criteriaCount ?? f.criteria?.length ?? "?";
142
+ return ` <feature id="${f.id}" status="${f.status}" criteria="${critCount}">${escapeXml(f.title)}</feature>`;
143
+ }).join("\n");
144
+ const truncated = featureList.length > 60 ? `
145
+ <truncated shown="60" total="${featureList.length}"/>` : "";
146
+ const activeCards = cardList.filter(
147
+ (c) => c.status === "in-progress" || c.status === "validation" || c.status === "ready"
148
+ );
149
+ const cardLines = activeCards.slice(0, 20).map((c) => {
150
+ const featureIds = (c.features || []).map((f) => f.id || f).join(",");
151
+ return ` <card id="${c.id}" status="${c.status}" features="${featureIds}">${escapeXml(c.title)}</card>`;
152
+ }).join("\n");
153
+ return `<rayburst_product_context>
154
+ <rules>
155
+ <rule>You have access to a Rayburst feature atlas \u2014 a knowledge graph of product features with Gherkin acceptance criteria. Use it to understand what you're building.</rule>
156
+ <rule>Before implementing any task, check if a matching feature exists in the atlas below. If found, read its criteria with rb_get_feature and work against them.</rule>
157
+ <rule>If your changes may affect behaviors described in OTHER features' criteria, flag this to the user before proceeding.</rule>
158
+ <rule>NEVER create, update, or delete features or criteria in the atlas without explicitly asking the user for confirmation first. Show them what you want to change and wait for approval.</rule>
159
+ <rule>When implementation is complete, summarize which acceptance criteria were addressed and which remain pending.</rule>
160
+ <rule>MANDATORY: After every implementation \u2014 in the same response as the code changes \u2014 you MUST update Rayburst without being asked: (1) update affected feature descriptions if behavior changed via rb_update_feature, (2) add new acceptance criteria for every new behavior introduced via rb_add_criterion, (3) update criterion status where applicable via rb_update_criterion. Do NOT wait for the user to ask. Skipping this step is a workflow violation.</rule>
161
+ <rule>Use mcp__plugin_rayburst_rayburst__rb_* MCP tools to interact with the atlas. Use rb_get_feature to load full criteria for a specific feature.</rule>
162
+ </rules>
10
163
 
11
- import { readConfig, mcpCall, extractData, writeCache } from "./rb-cache.mjs";
12
- import { buildProductContextBlock } from "./product-context-block.mjs";
164
+ <atlas summary="${total} features (${counts["draft"]} draft, ${counts["active"]} active, ${counts["completed"]} completed)">
165
+ ${featureLines}${truncated}
166
+ </atlas>
13
167
 
14
- const config = readConfig();
168
+ <board summary="${cardList.length} cards \u2014 ${cardCounts["in-progress"] || 0} in-progress, ${cardCounts["validation"] || 0} validation, ${cardCounts["done"] || 0} done">
169
+ ${cardLines}
170
+ </board>
171
+ </rayburst_product_context>`;
172
+ }
173
+ function escapeXml(str) {
174
+ return (str || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
175
+ }
15
176
 
16
- // Exit silently if no config — user hasn't run /rb:setup yet
177
+ // src/hooks/sessionstart.ts
178
+ var config = readConfig();
17
179
  if (!config || !config.apiKey) {
18
180
  process.exit(0);
19
181
  }
20
-
21
182
  try {
22
- // Fetch features and cards in parallel
23
183
  const [featuresRaw, cardsRaw] = await Promise.all([
24
184
  mcpCall(config, "list_features", { limit: 100 }),
25
- config.boardId
26
- ? mcpCall(config, "list_cards", { boardId: config.boardId })
27
- : Promise.resolve(null),
185
+ config.boardId ? mcpCall(config, "list_cards", { boardId: config.boardId }) : Promise.resolve(null)
28
186
  ]);
29
-
30
187
  const features = extractData(featuresRaw);
31
188
  const cards = extractData(cardsRaw);
32
-
33
189
  if (!features) {
34
- // API unreachable — exit silently
35
190
  process.exit(0);
36
191
  }
37
-
38
192
  const featureList = Array.isArray(features) ? features : features?.data ?? [];
39
193
  const cardList = Array.isArray(cards) ? cards : cards?.data ?? [];
40
-
41
- // Write feature cache for downstream hooks
42
194
  writeCache("features", featureList);
43
-
44
- // Build and inject the Product Context Block
45
195
  const contextBlock = buildProductContextBlock(featureList, cardList);
46
-
47
- // Output as JSON for Claude Code hook system
48
196
  console.log(
49
197
  JSON.stringify({
50
198
  hookSpecificOutput: {
51
199
  hookEventName: "SessionStart",
52
- additionalContext: contextBlock,
53
- },
200
+ additionalContext: contextBlock
201
+ }
54
202
  })
55
203
  );
56
204
  } catch {
57
- // Silent failure — don't block session startup
58
205
  process.exit(0);
59
206
  }