claudeboard 2.15.1 → 2.15.3
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/agents/claude-resolver.js +167 -0
- package/agents/developer.js +2 -42
- package/agents/qa.js +1 -34
- package/bin/cli.js +18 -14
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-resolver.js — Cross-platform Claude Code CLI detection
|
|
3
|
+
*
|
|
4
|
+
* Problem: On Windows, Claude Desktop installs a `claude.exe` that shadows
|
|
5
|
+
* the Claude Code CLI installed via `npm install -g @anthropic-ai/claude-code`.
|
|
6
|
+
* `which`/`where` may return the Desktop binary instead of the CLI.
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Honor CLAUDE_CODE_PATH env var (explicit override)
|
|
10
|
+
* 2. npm-based detection (most reliable — finds the npm-installed binary directly)
|
|
11
|
+
* 3. which/where — but validate it's not the Desktop app
|
|
12
|
+
* 4. Hardcoded platform-specific fallback paths
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
|
|
19
|
+
const isWin = process.platform === "win32";
|
|
20
|
+
const pathSep = isWin ? ";" : ":";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the candidate path looks like the Claude Code CLI
|
|
24
|
+
* and NOT the Claude Desktop app binary.
|
|
25
|
+
*/
|
|
26
|
+
function isClaudeCodePath(p) {
|
|
27
|
+
if (!p) return false;
|
|
28
|
+
const lower = p.toLowerCase().replace(/\\/g, "/");
|
|
29
|
+
// Desktop app on Windows: usually under AppData/Local/AnthropicClaude/
|
|
30
|
+
if (lower.includes("anthropicclaude")) return false;
|
|
31
|
+
if (lower.includes("anthropic claude")) return false;
|
|
32
|
+
// Desktop app on macOS: /Applications/Claude.app/...
|
|
33
|
+
if (lower.includes("claude.app/")) return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the path to the Claude Code CLI binary.
|
|
39
|
+
* Handles Windows / macOS / Linux, and the Desktop-vs-CLI shadowing conflict.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveClaudePath() {
|
|
42
|
+
// 1. Explicit override always wins
|
|
43
|
+
if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
|
|
44
|
+
|
|
45
|
+
// 2. npm global bin detection — bypasses PATH shadowing entirely
|
|
46
|
+
try {
|
|
47
|
+
const npmRoot = execSync("npm root -g", { stdio: "pipe", timeout: 5000 })
|
|
48
|
+
.toString().trim();
|
|
49
|
+
// npmRoot: /usr/local/lib/node_modules or C:\Users\<user>\AppData\Roaming\npm\node_modules
|
|
50
|
+
// The global bin dir is one level up from node_modules
|
|
51
|
+
const npmBinDir = path.dirname(npmRoot);
|
|
52
|
+
const candidates = isWin
|
|
53
|
+
? [
|
|
54
|
+
path.join(npmBinDir, "claude.cmd"),
|
|
55
|
+
path.join(npmBinDir, "claude"),
|
|
56
|
+
]
|
|
57
|
+
: [path.join(npmBinDir, "claude")];
|
|
58
|
+
for (const c of candidates) {
|
|
59
|
+
if (fs.existsSync(c)) return c;
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
63
|
+
// 3. which / where — validate it's not the Desktop app
|
|
64
|
+
try {
|
|
65
|
+
const raw = execSync(isWin ? "where claude" : "which claude", {
|
|
66
|
+
stdio: "pipe",
|
|
67
|
+
timeout: 5000,
|
|
68
|
+
}).toString().trim();
|
|
69
|
+
// `where` on Windows may return multiple lines; take the first valid one
|
|
70
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
71
|
+
for (const candidate of lines) {
|
|
72
|
+
if (isClaudeCodePath(candidate)) return candidate;
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
|
|
76
|
+
// 4. Hardcoded platform-specific fallback paths
|
|
77
|
+
if (isWin) {
|
|
78
|
+
const appData = process.env.APPDATA || "";
|
|
79
|
+
for (const p of [
|
|
80
|
+
path.join(appData, "npm", "claude.cmd"),
|
|
81
|
+
path.join(appData, "npm", "claude"),
|
|
82
|
+
]) {
|
|
83
|
+
if (fs.existsSync(p)) return p;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
for (const p of [
|
|
87
|
+
"/opt/homebrew/bin/claude",
|
|
88
|
+
"/usr/local/bin/claude",
|
|
89
|
+
`${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
|
|
90
|
+
`${process.env.HOME}/.npm-global/bin/claude`,
|
|
91
|
+
]) {
|
|
92
|
+
try {
|
|
93
|
+
execSync(`test -f "${p}"`, { stdio: "pipe" });
|
|
94
|
+
return p;
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds a cross-platform environment for subprocesses so that node/npm/npx
|
|
104
|
+
* are always resolvable inside Claude Code's shell.
|
|
105
|
+
* Also strips ANTHROPIC_API_KEY so Claude uses the user's subscription.
|
|
106
|
+
*/
|
|
107
|
+
export function buildEnv() {
|
|
108
|
+
// Use the current process's node binary dir — reliable on all platforms
|
|
109
|
+
const nodeBinDir = path.dirname(process.execPath);
|
|
110
|
+
|
|
111
|
+
const extraPaths = isWin
|
|
112
|
+
? [
|
|
113
|
+
process.env.APPDATA ? path.join(process.env.APPDATA, "npm") : "",
|
|
114
|
+
process.env.LOCALAPPDATA
|
|
115
|
+
? path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps")
|
|
116
|
+
: "",
|
|
117
|
+
]
|
|
118
|
+
: [
|
|
119
|
+
"/opt/homebrew/bin",
|
|
120
|
+
"/opt/homebrew/sbin",
|
|
121
|
+
"/usr/local/bin",
|
|
122
|
+
"/usr/bin",
|
|
123
|
+
"/bin",
|
|
124
|
+
`${process.env.HOME}/.npm-global/bin`,
|
|
125
|
+
`${process.env.HOME}/.nvm/versions/node/current/bin`,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const pathParts = [
|
|
129
|
+
process.env.PATH || "",
|
|
130
|
+
nodeBinDir,
|
|
131
|
+
...extraPaths,
|
|
132
|
+
].filter(Boolean);
|
|
133
|
+
|
|
134
|
+
const fullPath = [
|
|
135
|
+
...new Set(pathParts.join(pathSep).split(pathSep).filter(Boolean)),
|
|
136
|
+
].join(pathSep);
|
|
137
|
+
|
|
138
|
+
const env = { ...process.env, PATH: fullPath };
|
|
139
|
+
if (!isWin) env.HOME = process.env.HOME;
|
|
140
|
+
// Remove API key so Claude Code uses the Claude subscription (not API credits)
|
|
141
|
+
delete env.ANTHROPIC_API_KEY;
|
|
142
|
+
return env;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns a human-readable installation hint for the current platform.
|
|
147
|
+
*/
|
|
148
|
+
export function installHint() {
|
|
149
|
+
if (isWin) {
|
|
150
|
+
return [
|
|
151
|
+
"Claude Code CLI not found or shadowed by Claude Desktop.",
|
|
152
|
+
"",
|
|
153
|
+
"Install the CLI: npm install -g @anthropic-ai/claude-code",
|
|
154
|
+
"Then run: claude",
|
|
155
|
+
"",
|
|
156
|
+
"If already installed, set the explicit path to avoid conflicts:",
|
|
157
|
+
" set CLAUDE_CODE_PATH=C:\\Users\\<you>\\AppData\\Roaming\\npm\\claude.cmd",
|
|
158
|
+
" claudeboard run ...",
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
return [
|
|
162
|
+
"Claude Code CLI not found.",
|
|
163
|
+
"",
|
|
164
|
+
"Install: npm install -g @anthropic-ai/claude-code",
|
|
165
|
+
"Then run: claude",
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
package/agents/developer.js
CHANGED
|
@@ -8,47 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
10
10
|
import { startTask, completeTask, failTask, addLog } from "./board-client.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
// ── Resolve the global `claude` binary path at startup ────────────────────────
|
|
14
|
-
function resolveClaudePath() {
|
|
15
|
-
if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
|
|
16
|
-
try { return execSync("which claude", { stdio: "pipe" }).toString().trim(); } catch {}
|
|
17
|
-
for (const p of [
|
|
18
|
-
"/opt/homebrew/bin/claude",
|
|
19
|
-
"/usr/local/bin/claude",
|
|
20
|
-
`${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
|
|
21
|
-
`${process.env.HOME}/.npm-global/bin/claude`,
|
|
22
|
-
]) {
|
|
23
|
-
try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Build a full PATH so node/npm/npx are found inside the subprocess ─────────
|
|
29
|
-
// Exit code 127 = "command not found" — happens when PATH is stripped for global pkgs
|
|
30
|
-
function buildEnv() {
|
|
31
|
-
let nodeBinDir = "";
|
|
32
|
-
try { nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim(); } catch {}
|
|
33
|
-
|
|
34
|
-
const pathParts = [
|
|
35
|
-
process.env.PATH || "",
|
|
36
|
-
nodeBinDir,
|
|
37
|
-
"/opt/homebrew/bin",
|
|
38
|
-
"/opt/homebrew/sbin",
|
|
39
|
-
"/usr/local/bin",
|
|
40
|
-
"/usr/bin",
|
|
41
|
-
"/bin",
|
|
42
|
-
`${process.env.HOME}/.npm-global/bin`,
|
|
43
|
-
`${process.env.HOME}/.nvm/versions/node/current/bin`,
|
|
44
|
-
].filter(Boolean);
|
|
45
|
-
|
|
46
|
-
const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
|
|
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;
|
|
51
|
-
}
|
|
11
|
+
import { resolveClaudePath, buildEnv, installHint } from "./claude-resolver.js";
|
|
52
12
|
|
|
53
13
|
const CLAUDE_PATH = resolveClaudePath();
|
|
54
14
|
|
|
@@ -79,7 +39,7 @@ export async function runDeveloperAgent(task, projectPath, techStack, allTasks =
|
|
|
79
39
|
console.log(` 🤖 Claude Code working on: ${task.title}`);
|
|
80
40
|
|
|
81
41
|
if (!CLAUDE_PATH) {
|
|
82
|
-
const hint =
|
|
42
|
+
const hint = installHint();
|
|
83
43
|
await startTask(task.id, hint);
|
|
84
44
|
await failTask(task.id, hint);
|
|
85
45
|
console.error(`\n ✗ ${hint}\n`);
|
package/agents/qa.js
CHANGED
|
@@ -5,44 +5,11 @@ import { screenshotExpoWeb } from "../tools/screenshot.js";
|
|
|
5
5
|
import { screenshotSimulator } from "./expo-health.js";
|
|
6
6
|
import { callClaudeWithImage } from "./claude-api.js";
|
|
7
7
|
import { listFiles, readFile } from "../tools/filesystem.js";
|
|
8
|
-
import { execSync } from "child_process";
|
|
9
8
|
import chalk from "chalk";
|
|
10
9
|
import path from "path";
|
|
11
10
|
import fs from "fs";
|
|
12
11
|
import { createConnection } from "net";
|
|
13
|
-
|
|
14
|
-
// ── Reuse Claude Code path + env from developer ───────────────────────────────
|
|
15
|
-
function resolveClaudePath() {
|
|
16
|
-
if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
|
|
17
|
-
try { return execSync("which claude", { stdio: "pipe" }).toString().trim(); } catch {}
|
|
18
|
-
for (const p of [
|
|
19
|
-
"/opt/homebrew/bin/claude",
|
|
20
|
-
"/usr/local/bin/claude",
|
|
21
|
-
`${process.env.HOME}/.nvm/versions/node/current/bin/claude`,
|
|
22
|
-
`${process.env.HOME}/.npm-global/bin/claude`,
|
|
23
|
-
]) {
|
|
24
|
-
try { execSync(`test -f "${p}"`, { stdio: "pipe" }); return p; } catch {}
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function buildEnv() {
|
|
30
|
-
let nodeBinDir = "";
|
|
31
|
-
try { nodeBinDir = execSync("dirname $(which node)", { stdio: "pipe" }).toString().trim(); } catch {}
|
|
32
|
-
const pathParts = [
|
|
33
|
-
process.env.PATH || "",
|
|
34
|
-
nodeBinDir,
|
|
35
|
-
"/opt/homebrew/bin", "/opt/homebrew/sbin",
|
|
36
|
-
"/usr/local/bin", "/usr/bin", "/bin",
|
|
37
|
-
`${process.env.HOME}/.npm-global/bin`,
|
|
38
|
-
`${process.env.HOME}/.nvm/versions/node/current/bin`,
|
|
39
|
-
].filter(Boolean);
|
|
40
|
-
const fullPath = [...new Set(pathParts.join(":").split(":"))].join(":");
|
|
41
|
-
const env = { ...process.env, PATH: fullPath, HOME: process.env.HOME };
|
|
42
|
-
// Remove API key so Claude Code uses the Claude subscription (not API credits)
|
|
43
|
-
delete env.ANTHROPIC_API_KEY;
|
|
44
|
-
return env;
|
|
45
|
-
}
|
|
12
|
+
import { resolveClaudePath, buildEnv } from "./claude-resolver.js";
|
|
46
13
|
|
|
47
14
|
const CLAUDE_PATH = resolveClaudePath();
|
|
48
15
|
|
package/bin/cli.js
CHANGED
|
@@ -16,9 +16,9 @@ const _pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"),
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
const LOGO = `
|
|
19
|
-
${chalk.cyan("
|
|
20
|
-
${chalk.cyan("║")} ${chalk.bold.white("●")} ${chalk.bold.cyan("CLAUDEBOARD")} ${chalk.dim("
|
|
21
|
-
${chalk.cyan("
|
|
19
|
+
${chalk.cyan("╔════════════════════════════════════════════╗")}
|
|
20
|
+
${chalk.cyan("║")} ${chalk.bold.white("●")} ${chalk.bold.cyan("CLAUDEBOARD")} ${chalk.dim("powered by High Value, LLC")} ${chalk.cyan("║")}
|
|
21
|
+
${chalk.cyan("╚════════════════════════════════════════════╝")}
|
|
22
22
|
`;
|
|
23
23
|
|
|
24
24
|
function loadConfig() {
|
|
@@ -48,7 +48,7 @@ program
|
|
|
48
48
|
console.log(LOGO);
|
|
49
49
|
const { default: Enquirer } = await import("enquirer");
|
|
50
50
|
const enquirer = new Enquirer();
|
|
51
|
-
console.log(chalk.bold("Let's set up your AI engineering team.\n"));
|
|
51
|
+
console.log(chalk.bold("Let's set up your AI engineering team — powered by High Value, LLC.\n"));
|
|
52
52
|
|
|
53
53
|
const answers = await enquirer.prompt([
|
|
54
54
|
{ type: "input", name: "projectName", message: "Project name:", initial: path.basename(process.cwd()) },
|
|
@@ -158,18 +158,22 @@ program
|
|
|
158
158
|
|
|
159
159
|
// ── Verify Claude Code CLI is installed ───────────────────────────────────
|
|
160
160
|
const { execSync } = await import("child_process");
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.log(chalk.dim(` Claude Code → ${version}\n`));
|
|
165
|
-
} catch {
|
|
161
|
+
const { resolveClaudePath, installHint } = await import("../agents/claude-resolver.js");
|
|
162
|
+
const claudePath = resolveClaudePath();
|
|
163
|
+
if (!claudePath) {
|
|
166
164
|
console.log(chalk.red("\n ✗ Claude Code CLI not found!\n"));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
console.log(
|
|
165
|
+
for (const line of installHint().split("\n")) {
|
|
166
|
+
console.log(chalk.yellow(` ${line}`));
|
|
167
|
+
}
|
|
168
|
+
console.log();
|
|
171
169
|
process.exit(1);
|
|
172
170
|
}
|
|
171
|
+
try {
|
|
172
|
+
const version = execSync(`"${claudePath}" --version`, { stdio: "pipe" }).toString().trim();
|
|
173
|
+
console.log(chalk.dim(` Claude Code → ${version} (${claudePath})\n`));
|
|
174
|
+
} catch {
|
|
175
|
+
console.log(chalk.dim(` Claude Code → ${claudePath}\n`));
|
|
176
|
+
}
|
|
173
177
|
|
|
174
178
|
const resolvedProject = path.resolve(opts.project);
|
|
175
179
|
|
|
@@ -241,7 +245,7 @@ program
|
|
|
241
245
|
|
|
242
246
|
program
|
|
243
247
|
.name("claudeboard")
|
|
244
|
-
.description("AI engineering team — from PRD to working app, autonomously")
|
|
248
|
+
.description("AI engineering team — from PRD to working app, autonomously — powered by High Value, LLC")
|
|
245
249
|
.version(_pkg.version);
|
|
246
250
|
|
|
247
251
|
program.parse();
|