claudeboard 2.9.1 → 2.10.1

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.
@@ -44,7 +44,10 @@ function buildEnv() {
44
44
  ].filter(Boolean);
45
45
 
46
46
  const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
47
- return { ...process.env, PATH: fullPath, HOME: process.env.HOME };
47
+ const env = { ...process.env, PATH: fullPath, HOME: process.env.HOME };
48
+ // Remove API key so Claude Code uses the Claude subscription (not API credits)
49
+ delete env.ANTHROPIC_API_KEY;
50
+ return env;
48
51
  }
49
52
 
50
53
  const CLAUDE_PATH = resolveClaudePath();
@@ -152,7 +152,7 @@ async function tryStartExpo(projectPath, port) {
152
152
  const { spawn } = require("child_process");
153
153
  proc = spawn("npx", ["expo", "start", "--web", "--port", String(port)], {
154
154
  cwd: projectPath,
155
- env: { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" },
155
+ env: (() => { const e = { ...process.env, CI: "1", EXPO_NO_INTERACTIVE: "1", EXPO_NO_DOTENV: "0" }; delete e.ANTHROPIC_API_KEY; return e; })(),
156
156
  stdio: "pipe",
157
157
  });
158
158
 
package/agents/qa.js CHANGED
@@ -1,177 +1,221 @@
1
- import { callClaude, callClaudeJSON, callClaudeWithImage } from "./claude-api.js";
2
- import { addLog } from "./board-client.js";
3
- import { readFile, listFiles } from "../tools/filesystem.js";
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { addLog, completeTask, failTask } from "./board-client.js";
4
3
  import { runCommand } from "../tools/terminal.js";
5
4
  import { screenshotExpoWeb } from "../tools/screenshot.js";
6
- import { readErrorLogs } from "../tools/supabase-reader.js";
5
+ import { listFiles, readFile } from "../tools/filesystem.js";
6
+ import { execSync } from "child_process";
7
7
  import path from "path";
8
8
  import fs from "fs";
9
+ import { createConnection } from "net";
10
+
11
+ // ── Reuse Claude Code path + env from developer ───────────────────────────────
12
+ function resolveClaudePath() {
13
+ if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
14
+ try { return execSync("which claude", { stdio: "pipe" }).toString().trim(); } catch {}
15
+ for (const p of [
16
+ "/opt/homebrew/bin/claude",
17
+ "/usr/local/bin/claude",
18
+ `${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
19
+ `${process.env.HOME}/.npm-global/bin/claude`,
20
+ ]) {
21
+ try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function buildEnv() {
27
+ let nodeBinDir = "";
28
+ try { nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim(); } catch {}
29
+ const pathParts = [
30
+ process.env.PATH || "",
31
+ nodeBinDir,
32
+ "/opt/homebrew/bin", "/opt/homebrew/sbin",
33
+ "/usr/local/bin", "/usr/bin", "/bin",
34
+ `${process.env.HOME}/.npm-global/bin`,
35
+ `${process.env.HOME}/.nvm/versions/node/current/bin`,
36
+ ].filter(Boolean);
37
+ const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
38
+ const env = { ...process.env, PATH: fullPath, HOME: process.env.HOME };
39
+ // Remove API key so Claude Code uses the Claude subscription (not API credits)
40
+ delete env.ANTHROPIC_API_KEY;
41
+ return env;
42
+ }
43
+
44
+ const CLAUDE_PATH = resolveClaudePath();
9
45
 
10
- const SYSTEM_QA = `You are a senior QA engineer for React Native / Expo apps.
11
- Verify that implemented features work correctly. Be specific about actual issues.
12
- If Expo is not running, evaluate purely on code quality and completeness.`;
46
+ const QA_TOOLS = ["Read", "Glob", "Grep", "Bash"];
13
47
 
48
+ // ── Main QA entry point ───────────────────────────────────────────────────────
14
49
  export async function runQAAgent(task, devResult, projectPath, prdContent, expoPort = 8081) {
15
50
  console.log(` 🔍 QA checking: ${task.title}`);
16
51
  await addLog(task.id, "QA starting", "progress");
17
52
 
18
- // ── 1. TypeScript check ────────────────────────────────────────────────────
19
- const tsResult = await runCommand("npx tsc --noEmit 2>&1", projectPath, 60000);
20
- const tsErrors = tsResult.stdout.includes("error TS") ? tsResult.stdout : null;
21
- if (tsErrors) await addLog(task.id, `TypeScript errors detected`, "error");
22
-
23
- // ── 2. Check for truncated/incomplete files ────────────────────────────────
24
- const truncationIssues = await checkForTruncatedFiles(task, projectPath);
25
- if (truncationIssues.length > 0) {
26
- const msg = `Incomplete files: ${truncationIssues.join(", ")}`;
53
+ // ── 1. Check for truncated files (cheap, no Claude needed) ────────────────
54
+ const truncated = await checkForTruncatedFiles(task, projectPath);
55
+ if (truncated.length > 0) {
56
+ const msg = `Incomplete files detected: ${truncated.join(", ")}`;
27
57
  await addLog(task.id, msg, "error");
28
58
  return {
29
59
  passed: false,
30
- issues: truncationIssues,
31
- fixInstructions: `These files are truncated/incomplete:\n${truncationIssues.join("\n")}\n\nRe-implement them completely from scratch.`,
32
- screenshotPath: null,
60
+ issues: truncated,
61
+ fixInstructions: `These files are truncated/incomplete:\n${truncated.join("\n")}\n\nRe-implement them completely.`,
33
62
  };
34
63
  }
35
64
 
36
- // ── 3. Screenshot + Metro error check (only if Expo is running) ───────────
37
- let visualVerdict = null;
65
+ // ── 2. Check Metro health (if Expo is running) ────────────────────────────
38
66
  const expoRunning = await isPortOpen(expoPort);
39
-
40
67
  if (expoRunning) {
41
- // Verify Metro is actually serving without errors (not just port open)
42
- const metroCheck = await checkMetroHealth(expoPort, projectPath);
43
- if (!metroCheck.healthy) {
44
- await addLog(task.id, `Metro error detected: ${metroCheck.error?.slice(0, 100)}`, "error");
68
+ const metroErr = await getMetroError(projectPath);
69
+ if (metroErr) {
70
+ await addLog(task.id, `Metro error: ${metroErr.slice(0, 100)}`, "error");
45
71
  return {
46
72
  passed: false,
47
- issues: [`Expo/Metro has active errors: ${metroCheck.error?.slice(0, 200)}`],
48
- fixInstructions: `Metro is crashing with this error:\n${metroCheck.error}\n\nFix this error so the app loads without crashing on the device.`,
49
- screenshotPath: null,
73
+ issues: [`Metro crash: ${metroErr.slice(0, 200)}`],
74
+ fixInstructions: `Metro is crashing:\n${metroErr}\n\nFix this so the app loads on device without errors.`,
50
75
  };
51
76
  }
52
- await addLog(task.id, "Taking screenshot...", "progress");
53
- const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
54
- const screenshot = await screenshotExpoWeb(expoPort, screenshotDir);
55
- if (screenshot.success && screenshot.base64) {
56
- const visionResult = await callClaudeWithImage(
57
- SYSTEM_QA,
58
- `Task: ${task.title}\nExpected: ${task.description}\n\nDoes the UI look correct? Any visual bugs?`,
59
- screenshot.base64
60
- );
61
- visualVerdict = visionResult.text;
62
- await addLog(task.id, `Visual: ${visualVerdict}`, "progress");
63
- } else {
64
- await addLog(task.id, `Screenshot failed: ${screenshot.error}`, "progress");
65
- }
66
- } else {
67
- await addLog(task.id, "Expo not running — code-only QA", "progress");
68
77
  }
69
78
 
70
- // ── 4. Read ALL relevant code no artificial limits ──────────────────────
71
- // Claude Code already wrote these files — we read them fully for QA review
72
- const relevantFiles = getRelevantFiles(task, projectPath);
73
- let codeContext = "";
74
- for (const f of relevantFiles) {
75
- const content = readFile(f);
76
- if (content) {
77
- // No slice read the complete file
78
- codeContext += `\n### ${path.relative(projectPath, f)}\n${content}\n`;
79
+ // ── 3. Take screenshot if Expo is running ─────────────────────────────────
80
+ let screenshotBase64 = null;
81
+ let screenshotPath = null;
82
+ if (expoRunning) {
83
+ await addLog(task.id, "Taking screenshot...", "progress");
84
+ const screenshotDir = path.join(projectPath, ".claudeboard-screenshots");
85
+ const shot = await screenshotExpoWeb(expoPort, screenshotDir);
86
+ if (shot.success && shot.base64) {
87
+ screenshotBase64 = shot.base64;
88
+ screenshotPath = shot.path;
79
89
  }
80
90
  }
81
91
 
82
- // ── 5. App logs (full) ─────────────────────────────────────────────────────
83
- let appLogs = "";
84
- try {
85
- const logFile = path.join(projectPath, ".claudeboard-logs.txt");
86
- if (fs.existsSync(logFile)) appLogs = fs.readFileSync(logFile, "utf8");
87
- } catch {}
88
-
89
- // ── 6. Supabase errors ─────────────────────────────────────────────────────
90
- let supabaseLogs = [];
91
- try { supabaseLogs = await readErrorLogs("logs", 20); } catch {}
92
-
93
- // ── 7. Functional verdict ─────────────────────────────────────────────────
94
- const verdict = await callClaudeJSON(
95
- SYSTEM_QA,
96
- `Task: ${task.title}
97
- Description: ${task.description}
92
+ // ── 4. Ask Claude Code to review the implementation ──────────────────────
93
+ const qaResult = await runClaudeCodeQA(task, projectPath, prdContent, expoRunning, screenshotBase64, screenshotPath);
98
94
 
99
- Implemented code (complete files):
100
- ${codeContext || "Could not read relevant files"}
101
-
102
- TypeScript: ${tsErrors ? `ERRORS:\n${tsErrors}` : "Clean — no errors"}
103
- App logs: ${appLogs || "None"}
104
- Supabase errors: ${supabaseLogs.length > 0 ? JSON.stringify(supabaseLogs, null, 2) : "None"}
105
- Visual QA: ${visualVerdict || "No screenshot (Expo not running — evaluate code only)"}
106
-
107
- EVALUATION RULES:
108
- - PASS if: code is complete, TypeScript is clean, implementation matches description
109
- - FAIL only if: code is incomplete/truncated, has TS errors, or implementation is clearly wrong
110
- - Do NOT fail just because Expo isn't running
111
- - Do NOT fail for minor style preferences
112
-
113
- Respond with JSON:
114
- {
115
- "passed": true/false,
116
- "confidence": 0-100,
117
- "issues": ["specific issue 1", "specific issue 2"],
118
- "summary": "One sentence verdict",
119
- "fixInstructions": "Specific fix instructions if failed, null if passed"
120
- }`
121
- );
122
-
123
- const qaReport = {
124
- passed: verdict.passed,
125
- issues: verdict.issues || [],
126
- fixInstructions: verdict.fixInstructions,
127
- screenshotPath: null,
128
- };
129
-
130
- if (qaReport.passed) {
131
- await addLog(task.id, `✓ QA passed (${verdict.confidence}%): ${verdict.summary}`, "complete");
95
+ if (qaResult.passed) {
96
+ await addLog(task.id, `✓ QA passed: ${qaResult.summary}`, "complete");
132
97
  console.log(` ✓ QA passed: ${task.title}`);
133
98
  } else {
134
- await addLog(task.id, `✗ QA failed: ${verdict.summary}`, "error");
135
- console.log(` ✗ QA failed: ${task.title} — ${verdict.issues?.slice(0,2).join(", ")}`);
99
+ await addLog(task.id, `✗ QA failed: ${qaResult.summary}`, "error");
100
+ console.log(` ✗ QA failed: ${task.title} — ${qaResult.issues?.slice(0, 2).join(", ")}`);
136
101
  }
137
102
 
138
- return qaReport;
103
+ return qaResult;
139
104
  }
140
105
 
141
- // ── Detect truncated/incomplete files ──────────────────────────────────────────
142
- async function checkForTruncatedFiles(task, projectPath) {
143
- const issues = [];
144
- const keywords = task.title.toLowerCase().split(" ").filter(w => w.length > 4);
106
+ // ── Claude Code QA review ─────────────────────────────────────────────────────
107
+ async function runClaudeCodeQA(task, projectPath, prdContent, expoRunning, screenshotBase64, screenshotPath) {
108
+ const screenshotNote = screenshotBase64
109
+ ? `A screenshot of the app has been saved to: ${screenshotPath}\nRead it with the Read tool and evaluate the visual result.`
110
+ : expoRunning
111
+ ? "Expo is running but screenshot failed — evaluate code only."
112
+ : "Expo is not running — evaluate code quality only (TypeScript, completeness, correctness).";
145
113
 
146
- const files = listFiles(projectPath, [".ts", ".tsx"])
147
- .filter(f => {
148
- const rel = path.relative(projectPath, f).toLowerCase();
149
- if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
150
- return keywords.some(kw => rel.includes(kw));
151
- });
114
+ const prompt = `You are a senior QA engineer reviewing a React Native / Expo implementation.
152
115
 
153
- for (const f of files) {
154
- const content = readFile(f);
155
- if (!content) continue;
156
- const rel = path.relative(projectPath, f);
157
- const lastLines = content.split("\n").slice(-5).join("\n").trim();
158
- const openBraces = (content.match(/\{/g) || []).length;
159
- const closeBraces = (content.match(/\}/g) || []).length;
116
+ TASK THAT WAS IMPLEMENTED:
117
+ Title: ${task.title}
118
+ Description: ${task.description}
160
119
 
161
- const truncated =
162
- lastLines.endsWith("{") || lastLines.endsWith("(") || lastLines.endsWith(",") ||
163
- lastLines.match(/^(import|\/\/) *$/) ||
164
- openBraces > closeBraces + 3 ||
165
- (content.length < 80 && (rel.includes("store") || rel.includes("hook") || rel.includes("screen")));
120
+ ${screenshotNote}
121
+
122
+ YOUR JOB:
123
+ 1. Run: npx tsc --noEmit 2>&1 | head -50
124
+ - If TypeScript errors exist FAIL with specific error details
125
+ 2. Find and read the files related to this task (use Glob + Read)
126
+ - Check for: incomplete implementations, missing imports, wrong logic
127
+ 3. ${screenshotBase64 ? "Read the screenshot file and check if the UI matches what was expected" : "Evaluate the code for correctness based on the description"}
128
+
129
+ PASS criteria:
130
+ - TypeScript is clean (no errors)
131
+ - Files are complete (not truncated)
132
+ - Implementation matches the task description
133
+
134
+ FAIL criteria:
135
+ - TypeScript errors
136
+ - Files clearly incomplete or truncated
137
+ - Implementation completely missing or wrong
138
+
139
+ When done, output EXACTLY one of these lines:
140
+ QA_PASS: <one sentence summary of what was verified>
141
+ QA_FAIL: <specific reason> | FIX: <exact instructions for the developer>`;
166
142
 
167
- if (truncated) {
168
- issues.push(`${rel} (ends: "${lastLines.slice(-80)}")`);
143
+ if (!CLAUDE_PATH) {
144
+ console.log(" ⚠ Claude Code not found — skipping QA");
145
+ return { passed: true, summary: "Skipped (Claude Code not found)", issues: [] };
146
+ }
147
+
148
+ try {
149
+ let output = "";
150
+ let passed = null;
151
+ let summary = "";
152
+ let fixInstructions = "";
153
+
154
+ for await (const message of query({
155
+ prompt,
156
+ options: {
157
+ cwd: projectPath,
158
+ tools: QA_TOOLS,
159
+ permissionMode: "bypassPermissions",
160
+ maxTurns: 20,
161
+ pathToClaudeCodeExecutable: CLAUDE_PATH,
162
+ env: buildEnv(),
163
+ },
164
+ })) {
165
+ if (message.type === "assistant") {
166
+ for (const block of message.message?.content || []) {
167
+ if (block.type === "text") {
168
+ const text = block.text;
169
+ output += text + "\n";
170
+
171
+ if (text.includes("QA_PASS:")) {
172
+ passed = true;
173
+ summary = text.split("QA_PASS:")[1]?.trim().split("\n")[0] || task.title;
174
+ } else if (text.includes("QA_FAIL:")) {
175
+ passed = false;
176
+ const parts = text.split("QA_FAIL:")[1]?.split("| FIX:") || [];
177
+ summary = parts[0]?.trim() || "QA failed";
178
+ fixInstructions = parts[1]?.trim() || summary;
179
+ }
180
+ }
181
+ if (block.type === "tool_use") {
182
+ await addLog(task.id, `QA: ${block.name} ${JSON.stringify(block.input).slice(0, 60)}`, "progress");
183
+ }
184
+ }
185
+ }
186
+
187
+ if (message.type === "result") {
188
+ // Check result text too
189
+ const text = message.result || "";
190
+ if (!passed && text.includes("QA_PASS:")) {
191
+ passed = true;
192
+ summary = text.split("QA_PASS:")[1]?.trim().split("\n")[0] || task.title;
193
+ } else if (passed === null && text.includes("QA_FAIL:")) {
194
+ passed = false;
195
+ const parts = text.split("QA_FAIL:")[1]?.split("| FIX:") || [];
196
+ summary = parts[0]?.trim() || "QA failed";
197
+ fixInstructions = parts[1]?.trim() || summary;
198
+ }
199
+ // Default to pass if Claude Code ran but didn't signal explicitly
200
+ if (passed === null) passed = true;
201
+ }
169
202
  }
203
+
204
+ return {
205
+ passed: passed ?? true,
206
+ summary: summary || task.title,
207
+ issues: passed ? [] : [summary],
208
+ fixInstructions: passed ? null : fixInstructions,
209
+ };
210
+
211
+ } catch (e) {
212
+ // If Claude Code fails for any reason, don't block development
213
+ console.log(` ⚠ QA error: ${e.message?.slice(0, 80)} — skipping`);
214
+ return { passed: true, summary: "QA skipped (error)", issues: [] };
170
215
  }
171
- return issues;
172
216
  }
173
217
 
174
- // ── Full app QA ────────────────────────────────────────────────────────────────
218
+ // ── Full app QA after all tasks complete ──────────────────────────────────────
175
219
  export async function runFullAppQA(projectPath, prdContent, expoPort = 8081) {
176
220
  console.log("\n 🔍 Running full app QA...");
177
221
 
@@ -196,65 +240,55 @@ export async function runFullAppQA(projectPath, prdContent, expoPort = 8081) {
196
240
  await new Promise(r => setTimeout(r, 800));
197
241
  }
198
242
 
199
- const report = { passed: true, routes: routeFiles, screenshotsCaptures: screenshots.length, issues: [] };
200
-
201
- for (const shot of screenshots) {
202
- if (!shot.base64) continue;
203
- const verdict = await callClaudeWithImage(
204
- SYSTEM_QA,
205
- `Route: ${shot.route}\n\nFull PRD:\n${prdContent}\n\nAny obvious issues?`,
206
- shot.base64
207
- );
208
- if (verdict.text.toLowerCase().includes("issue") || verdict.text.toLowerCase().includes("problem")) {
209
- report.issues.push({ route: shot.route, note: verdict.text });
210
- }
211
- }
212
-
213
- return report;
243
+ return { passed: true, routes: routeFiles, screenshotsCaptures: screenshots.length, issues: [] };
214
244
  }
215
245
 
216
- // ── Helpers ───────────────────────────────────────────────────────────────────
217
- function getRelevantFiles(task, projectPath) {
246
+ // ── Detect truncated files ─────────────────────────────────────────────────────
247
+ async function checkForTruncatedFiles(task, projectPath) {
248
+ const issues = [];
218
249
  const keywords = task.title.toLowerCase().split(" ").filter(w => w.length > 4);
219
- return listFiles(projectPath, [".ts", ".tsx"])
220
- .filter(f => {
221
- const rel = path.relative(projectPath, f).toLowerCase();
222
- if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
223
- return keywords.some(kw => rel.includes(kw));
224
- });
225
- // No .slice() return all matching files
250
+ const files = listFiles(projectPath, [".ts", ".tsx"]).filter(f => {
251
+ const rel = path.relative(projectPath, f).toLowerCase();
252
+ if (rel.includes("node_modules") || rel.includes(".claudeboard")) return false;
253
+ return keywords.some(kw => rel.includes(kw));
254
+ });
255
+
256
+ for (const f of files) {
257
+ const content = readFile(f);
258
+ if (!content) continue;
259
+ const rel = path.relative(projectPath, f);
260
+ const lastLines = content.split("\n").slice(-5).join("\n").trim();
261
+ const openBraces = (content.match(/\{/g) || []).length;
262
+ const closeBraces = (content.match(/\}/g) || []).length;
263
+ const truncated =
264
+ lastLines.endsWith("{") || lastLines.endsWith("(") || lastLines.endsWith(",") ||
265
+ openBraces > closeBraces + 3 ||
266
+ (content.length < 80 && (rel.includes("store") || rel.includes("hook") || rel.includes("screen")));
267
+ if (truncated) issues.push(`${rel} (ends: "${lastLines.slice(-80)}")`);
268
+ }
269
+ return issues;
226
270
  }
227
271
 
228
- async function isPortOpen(port) {
229
- try {
230
- const { default: net } = await import("net");
231
- return new Promise(resolve => {
232
- const sock = new net.Socket();
233
- sock.setTimeout(800);
234
- sock.once("connect", () => { sock.destroy(); resolve(true); });
235
- sock.once("error", () => resolve(false));
236
- sock.once("timeout", () => resolve(false));
237
- sock.connect(port, "127.0.0.1");
238
- });
239
- } catch { return false; }
272
+ // ── Helpers ───────────────────────────────────────────────────────────────────
273
+ function isPortOpen(port) {
274
+ return new Promise(resolve => {
275
+ const sock = createConnection({ port, host: "127.0.0.1" });
276
+ sock.setTimeout(800);
277
+ sock.once("connect", () => { sock.destroy(); resolve(true); });
278
+ sock.once("error", () => resolve(false));
279
+ sock.once("timeout", () => resolve(false));
280
+ });
240
281
  }
241
282
 
242
- // Check Metro is serving without errors by fetching the bundle status
243
- async function checkMetroHealth(port, projectPath) {
283
+ async function getMetroError(projectPath) {
244
284
  try {
245
- // Read the expo error file if it exists (written by health check)
246
- const errorFile = path.join(projectPath, ".claudeboard-expo-error.txt");
247
- if (fs.existsSync(errorFile)) {
248
- const errorContent = fs.readFileSync(errorFile, "utf8");
249
- // Only fail if error is recent (file modified in last 5 min)
250
- const stat = fs.statSync(errorFile);
251
- const ageMs = Date.now() - stat.mtimeMs;
252
- if (ageMs < 5 * 60 * 1000 && errorContent.includes("Unable to resolve")) {
253
- return { healthy: false, error: errorContent.slice(-800) };
254
- }
255
- }
256
- return { healthy: true };
257
- } catch {
258
- return { healthy: true }; // Don't fail if we can't check
259
- }
285
+ const f = path.join(projectPath, ".claudeboard-expo-error.txt");
286
+ if (!fs.existsSync(f)) return null;
287
+ const stat = fs.statSync(f);
288
+ if (Date.now() - stat.mtimeMs > 5 * 60 * 1000) return null; // older than 5min
289
+ const content = fs.readFileSync(f, "utf8");
290
+ return content.includes("Unable to resolve") || content.includes("Cannot find module")
291
+ ? content.slice(-800)
292
+ : null;
293
+ } catch { return null; }
260
294
  }
package/bin/cli.js CHANGED
@@ -8,8 +8,12 @@ import path from "path";
8
8
  import fs from "fs";
9
9
  import { spawn } from "child_process";
10
10
  import open from "open";
11
+ import { createRequire } from "module";
11
12
 
13
+ const _require = createRequire(import.meta.url);
12
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const _pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
16
+
13
17
 
14
18
  const LOGO = `
15
19
  ${chalk.cyan("╔══════════════════════════════════════╗")}
@@ -236,6 +240,6 @@ program
236
240
  program
237
241
  .name("claudeboard")
238
242
  .description("AI engineering team — from PRD to working app, autonomously")
239
- .version("1.0.0");
243
+ .version(_pkg.version);
240
244
 
241
245
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeboard",
3
- "version": "2.9.1",
3
+ "version": "2.10.1",
4
4
  "description": "AI engineering team — from PRD to working mobile app, autonomously",
5
5
  "type": "module",
6
6
  "bin": {