claudeboard 2.16.0 → 3.1.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.
- package/README.md +89 -93
- package/bin/cli.js +198 -238
- package/bin/init-context.js +22 -0
- package/package.json +25 -43
- package/public/app.js +1411 -0
- package/public/index.html +250 -0
- package/public/style.css +1872 -0
- package/src/context-template.md +20 -0
- package/src/notifier.js +65 -0
- package/src/orchestrator.js +939 -0
- package/src/scanner.js +153 -0
- package/src/server.js +205 -0
- package/src/store.js +182 -0
- package/src/verifier.js +131 -0
- package/agents/architect.js +0 -166
- package/agents/board-client.js +0 -126
- package/agents/claude-api.js +0 -124
- package/agents/claude-resolver.js +0 -167
- package/agents/developer.js +0 -224
- package/agents/expo-health.js +0 -727
- package/agents/orchestrator.js +0 -306
- package/agents/qa.js +0 -336
- package/dashboard/index.html +0 -1980
- package/dashboard/server.js +0 -412
- package/sql/setup.sql +0 -57
- package/tools/filesystem.js +0 -95
- package/tools/screenshot.js +0 -74
- package/tools/supabase-reader.js +0 -74
- package/tools/terminal.js +0 -63
package/agents/architect.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { callClaudeJSON } from "./claude-api.js";
|
|
2
|
-
import { createEpic, createTask } from "./board-client.js";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import path from "path";
|
|
5
|
-
|
|
6
|
-
const SYSTEM_MOBILE = `You are a senior mobile app architect specializing in React Native / Expo apps.
|
|
7
|
-
Your job is to read a PRD and produce a complete, ordered task breakdown.
|
|
8
|
-
|
|
9
|
-
Rules:
|
|
10
|
-
- Think like a real engineering team: setup first, core features, then polish, then QA
|
|
11
|
-
- Each task must be self-contained and implementable by a single developer agent
|
|
12
|
-
- Be specific — tasks like "implement auth" are too vague. Break into: "create login screen UI", "implement Supabase auth hook", "add protected route navigation"
|
|
13
|
-
- Always include: project setup, navigation, data layer, each feature screen, error handling, loading states, and final QA tasks
|
|
14
|
-
- Priority: high = blocking/core, medium = main features, low = polish/nice-to-have
|
|
15
|
-
- Types: config (setup/deps), feature (new screen or functionality), bug (fix), refactor, test (QA task)
|
|
16
|
-
- When building on an existing project: do NOT recreate files that already exist — create tasks that extend or integrate with them`;
|
|
17
|
-
|
|
18
|
-
const SYSTEM_WEB = `You are a senior web app architect specializing in React + Vite apps.
|
|
19
|
-
Your job is to read a PRD and produce a complete, ordered task breakdown.
|
|
20
|
-
|
|
21
|
-
Rules:
|
|
22
|
-
- Think like a real engineering team: setup first, core features, then polish, then QA
|
|
23
|
-
- Each task must be self-contained and implementable by a single developer agent
|
|
24
|
-
- Be specific — tasks like "implement auth" are too vague. Break into: "create login page component", "implement Supabase auth hook", "add protected route with React Router"
|
|
25
|
-
- Always include: project setup (Vite + React), routing, data layer, each page/view, error handling, loading states, and final QA tasks
|
|
26
|
-
- Use React Router v6 for routing, TailwindCSS or CSS Modules for styles, Vite as build tool
|
|
27
|
-
- Priority: high = blocking/core, medium = main features, low = polish/nice-to-have
|
|
28
|
-
- Types: config (setup/deps), feature (new page or functionality), bug (fix), refactor, test (QA task)
|
|
29
|
-
- When building on an existing project: do NOT recreate files that already exist — create tasks that extend or integrate with them`;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Build a concise snapshot of the existing project:
|
|
33
|
-
* - Top-level file/folder structure
|
|
34
|
-
* - package.json dependencies
|
|
35
|
-
* - app.json expo config (if present)
|
|
36
|
-
*/
|
|
37
|
-
function getProjectSnapshot(projectPath) {
|
|
38
|
-
const lines = [];
|
|
39
|
-
|
|
40
|
-
// Top-level structure (exclude hidden files and node_modules)
|
|
41
|
-
try {
|
|
42
|
-
const entries = fs.readdirSync(projectPath)
|
|
43
|
-
.filter(f => !f.startsWith('.') && f !== 'node_modules')
|
|
44
|
-
.sort();
|
|
45
|
-
lines.push("### Project structure (top level)");
|
|
46
|
-
for (const entry of entries) {
|
|
47
|
-
const fullPath = path.join(projectPath, entry);
|
|
48
|
-
const isDir = fs.statSync(fullPath).isDirectory();
|
|
49
|
-
if (isDir) {
|
|
50
|
-
// List one level deep for key folders
|
|
51
|
-
const children = fs.readdirSync(fullPath)
|
|
52
|
-
.filter(f => !f.startsWith('.') && f !== 'node_modules')
|
|
53
|
-
.slice(0, 15);
|
|
54
|
-
lines.push(`${entry}/`);
|
|
55
|
-
for (const child of children) lines.push(` ${entry}/${child}`);
|
|
56
|
-
} else {
|
|
57
|
-
lines.push(entry);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} catch {}
|
|
61
|
-
|
|
62
|
-
// package.json
|
|
63
|
-
try {
|
|
64
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"), "utf8"));
|
|
65
|
-
lines.push("\n### package.json dependencies");
|
|
66
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
67
|
-
for (const [name, version] of Object.entries(deps)) {
|
|
68
|
-
lines.push(` ${name}: ${version}`);
|
|
69
|
-
}
|
|
70
|
-
} catch {}
|
|
71
|
-
|
|
72
|
-
// app.json
|
|
73
|
-
try {
|
|
74
|
-
const appJson = JSON.parse(fs.readFileSync(path.join(projectPath, "app.json"), "utf8"));
|
|
75
|
-
lines.push("\n### app.json (expo config)");
|
|
76
|
-
lines.push(JSON.stringify(appJson.expo || appJson, null, 2).slice(0, 1000));
|
|
77
|
-
} catch {}
|
|
78
|
-
|
|
79
|
-
return lines.join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function runArchitectAgent(prdContent, projectName, options = {}) {
|
|
83
|
-
const { buildOnExisting = false, projectPath = null, appType = "mobile" } = options;
|
|
84
|
-
const SYSTEM = appType === "web" ? SYSTEM_WEB : SYSTEM_MOBILE;
|
|
85
|
-
|
|
86
|
-
console.log(" Architect analyzing PRD...");
|
|
87
|
-
|
|
88
|
-
const projectSnapshot = (buildOnExisting && projectPath)
|
|
89
|
-
? getProjectSnapshot(projectPath)
|
|
90
|
-
: null;
|
|
91
|
-
|
|
92
|
-
const existingProjectSection = projectSnapshot
|
|
93
|
-
? `\n\nEXISTING PROJECT SNAPSHOT:\n${projectSnapshot}\n\nIMPORTANT: Create tasks that BUILD ON top of what already exists. Do NOT recreate files or setup that is already in place. Analyze the snapshot carefully and only create tasks for what is missing or needs to be extended.`
|
|
94
|
-
: "";
|
|
95
|
-
|
|
96
|
-
const techStackShape = appType === "web"
|
|
97
|
-
? `"techStack": {
|
|
98
|
-
"framework": "vite+react",
|
|
99
|
-
"routing": "react-router-dom",
|
|
100
|
-
"stateManagement": "...",
|
|
101
|
-
"backend": "supabase / firebase / none",
|
|
102
|
-
"ui": "tailwindcss / css-modules / shadcn-ui",
|
|
103
|
-
"otherDeps": ["list of npm packages needed"]
|
|
104
|
-
}`
|
|
105
|
-
: `"techStack": {
|
|
106
|
-
"framework": "expo",
|
|
107
|
-
"navigation": "expo-router or react-navigation",
|
|
108
|
-
"stateManagement": "...",
|
|
109
|
-
"backend": "supabase / firebase / none",
|
|
110
|
-
"ui": "nativewind / tamagui / stylesheet",
|
|
111
|
-
"otherDeps": ["list of npm packages needed"]
|
|
112
|
-
}`;
|
|
113
|
-
|
|
114
|
-
const result = await callClaudeJSON(SYSTEM, `
|
|
115
|
-
Project: ${projectName}
|
|
116
|
-
|
|
117
|
-
PRD:
|
|
118
|
-
${prdContent}
|
|
119
|
-
${existingProjectSection}
|
|
120
|
-
|
|
121
|
-
Return this JSON structure:
|
|
122
|
-
{
|
|
123
|
-
${techStackShape},
|
|
124
|
-
"epics": [
|
|
125
|
-
{
|
|
126
|
-
"name": "Epic name (e.g. Project Setup, Authentication, Home Screen)",
|
|
127
|
-
"order": 1,
|
|
128
|
-
"tasks": [
|
|
129
|
-
{
|
|
130
|
-
"title": "Specific task title",
|
|
131
|
-
"description": "Detailed description of exactly what needs to be implemented. Include: what file(s) to create/modify, what the component/function should do, any specific requirements from the PRD.",
|
|
132
|
-
"priority": "high|medium|low",
|
|
133
|
-
"type": "config|feature|test",
|
|
134
|
-
"acceptanceCriteria": "How to verify this task is done correctly"
|
|
135
|
-
}
|
|
136
|
-
]
|
|
137
|
-
}
|
|
138
|
-
]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
Order epics from first to last in implementation order.
|
|
142
|
-
Include as many tasks as needed for a complete, production-ready mobile app — do not artificially limit the count.`, { maxTokens: 16000 });
|
|
143
|
-
|
|
144
|
-
console.log(` ✓ Architect created ${result.epics.length} epics`);
|
|
145
|
-
|
|
146
|
-
// Persist to board
|
|
147
|
-
let totalTasks = 0;
|
|
148
|
-
for (let i = 0; i < result.epics.length; i++) {
|
|
149
|
-
const epicData = result.epics[i];
|
|
150
|
-
const epicId = await createEpic(epicData.name);
|
|
151
|
-
|
|
152
|
-
for (const task of epicData.tasks) {
|
|
153
|
-
await createTask({
|
|
154
|
-
epicId,
|
|
155
|
-
title: task.title,
|
|
156
|
-
description: `${task.description}\n\nAcceptance: ${task.acceptanceCriteria || ""}`,
|
|
157
|
-
priority: task.priority,
|
|
158
|
-
type: task.type,
|
|
159
|
-
});
|
|
160
|
-
totalTasks++;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
console.log(` ✓ Created ${totalTasks} tasks in board`);
|
|
165
|
-
return { techStack: result.techStack, epics: result.epics, totalTasks };
|
|
166
|
-
}
|
package/agents/board-client.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { createClient } from "@supabase/supabase-js";
|
|
2
|
-
|
|
3
|
-
let supabase = null;
|
|
4
|
-
let PROJECT = "default";
|
|
5
|
-
|
|
6
|
-
export function initBoard(url, key, project) {
|
|
7
|
-
supabase = createClient(url, key);
|
|
8
|
-
PROJECT = project;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function getNextTask() {
|
|
12
|
-
const { data } = await supabase
|
|
13
|
-
.from("cb_tasks")
|
|
14
|
-
.select("*, cb_epics(name)")
|
|
15
|
-
.eq("project", PROJECT)
|
|
16
|
-
.eq("status", "todo")
|
|
17
|
-
.order("priority_order", { ascending: true })
|
|
18
|
-
.limit(1)
|
|
19
|
-
.single();
|
|
20
|
-
return data || null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function getAllTasks() {
|
|
24
|
-
const { data } = await supabase
|
|
25
|
-
.from("cb_tasks")
|
|
26
|
-
.select("*, cb_epics(name)")
|
|
27
|
-
.eq("project", PROJECT)
|
|
28
|
-
.order("priority_order");
|
|
29
|
-
return data || [];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function startTask(id, log) {
|
|
33
|
-
await supabase.from("cb_tasks").update({ status: "in_progress", started_at: new Date().toISOString() }).eq("id", id);
|
|
34
|
-
if (log) await addLog(id, log, "start");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function completeTask(id, log) {
|
|
38
|
-
await supabase.from("cb_tasks").update({ status: "done", completed_at: new Date().toISOString() }).eq("id", id);
|
|
39
|
-
if (log) await addLog(id, log, "complete");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function failTask(id, log) {
|
|
43
|
-
await supabase.from("cb_tasks").update({ status: "error" }).eq("id", id);
|
|
44
|
-
if (log) await addLog(id, log, "error");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function blockTask(id, log) {
|
|
48
|
-
await supabase.from("cb_tasks").update({ status: "blocked" }).eq("id", id);
|
|
49
|
-
if (log) await addLog(id, log, "error");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function addLog(taskId, message, type = "progress") {
|
|
53
|
-
await supabase.from("cb_logs").insert({ project: PROJECT, task_id: taskId, message, type });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function createEpic(name) {
|
|
57
|
-
// Check if epic with this name already exists for this project
|
|
58
|
-
const { data: existing } = await supabase
|
|
59
|
-
.from("cb_epics")
|
|
60
|
-
.select("id")
|
|
61
|
-
.eq("project", PROJECT)
|
|
62
|
-
.eq("name", name)
|
|
63
|
-
.single();
|
|
64
|
-
if (existing) return existing.id;
|
|
65
|
-
const { data } = await supabase.from("cb_epics").insert({ name, project: PROJECT }).select().single();
|
|
66
|
-
return data?.id;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function createTask({ epicId, title, description, priority = "medium", priorityOrder, type = "feature", status = "todo" }) {
|
|
70
|
-
const defaultOrder = { high: 1, medium: 2, low: 3 };
|
|
71
|
-
const { data } = await supabase
|
|
72
|
-
.from("cb_tasks")
|
|
73
|
-
.insert({
|
|
74
|
-
project: PROJECT,
|
|
75
|
-
epic_id: epicId,
|
|
76
|
-
title,
|
|
77
|
-
description,
|
|
78
|
-
priority,
|
|
79
|
-
priority_order: priorityOrder ?? defaultOrder[priority] ?? 2,
|
|
80
|
-
type,
|
|
81
|
-
status,
|
|
82
|
-
})
|
|
83
|
-
.select()
|
|
84
|
-
.single();
|
|
85
|
-
return data;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Check if this project already has tasks in the board
|
|
90
|
-
* Used to detect resume vs fresh start
|
|
91
|
-
*/
|
|
92
|
-
export async function hasTasks() {
|
|
93
|
-
const { data, error } = await supabase
|
|
94
|
-
.from("cb_tasks")
|
|
95
|
-
.select("id")
|
|
96
|
-
.eq("project", PROJECT)
|
|
97
|
-
.limit(1);
|
|
98
|
-
return Array.isArray(data) && data.length > 0;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Reset any stuck "in_progress" tasks back to "todo"
|
|
103
|
-
* Called on resume so the agent re-picks them up
|
|
104
|
-
*/
|
|
105
|
-
export async function resetStuckTasks() {
|
|
106
|
-
const { data } = await supabase
|
|
107
|
-
.from("cb_tasks")
|
|
108
|
-
.update({ status: "todo", started_at: null })
|
|
109
|
-
.eq("project", PROJECT)
|
|
110
|
-
.eq("status", "in_progress")
|
|
111
|
-
.select();
|
|
112
|
-
return data?.length || 0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function getStats() {
|
|
116
|
-
const tasks = await getAllTasks();
|
|
117
|
-
return {
|
|
118
|
-
total: tasks.length,
|
|
119
|
-
todo: tasks.filter((t) => t.status === "todo").length,
|
|
120
|
-
in_progress: tasks.filter((t) => t.status === "in_progress").length,
|
|
121
|
-
done: tasks.filter((t) => t.status === "done").length,
|
|
122
|
-
error: tasks.filter((t) => t.status === "error").length,
|
|
123
|
-
blocked: tasks.filter((t) => t.status === "blocked").length,
|
|
124
|
-
pct: tasks.length > 0 ? Math.round((tasks.filter((t) => t.status === "done").length / tasks.length) * 100) : 0,
|
|
125
|
-
};
|
|
126
|
-
}
|
package/agents/claude-api.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core Claude API caller for all agents
|
|
3
|
-
* All agents use claude-sonnet-4-20250514 with specific system prompts and tools
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const MODEL = "claude-sonnet-4-20250514";
|
|
7
|
-
const MAX_TOKENS = 16000; // Max output tokens — input context window is 200k, no limits there
|
|
8
|
-
|
|
9
|
-
function getHeaders() {
|
|
10
|
-
const key = process.env.ANTHROPIC_API_KEY;
|
|
11
|
-
if (!key) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
"ANTHROPIC_API_KEY not set.\n" +
|
|
14
|
-
"Run: claudeboard init (and enter your API key)\n" +
|
|
15
|
-
"Or: export ANTHROPIC_API_KEY=sk-ant-..."
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
"Content-Type": "application/json",
|
|
20
|
-
"x-api-key": key,
|
|
21
|
-
"anthropic-version": "2023-06-01",
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Call Claude API and get text response
|
|
27
|
-
*/
|
|
28
|
-
export async function callClaude(systemPrompt, userMessage, options = {}) {
|
|
29
|
-
const body = {
|
|
30
|
-
model: MODEL,
|
|
31
|
-
max_tokens: options.maxTokens || MAX_TOKENS,
|
|
32
|
-
system: systemPrompt,
|
|
33
|
-
messages: [{ role: "user", content: userMessage }],
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (options.tools) body.tools = options.tools;
|
|
37
|
-
|
|
38
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
39
|
-
method: "POST",
|
|
40
|
-
headers: getHeaders(),
|
|
41
|
-
body: JSON.stringify(body),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (!response.ok) {
|
|
45
|
-
const err = await response.text();
|
|
46
|
-
throw new Error(`Claude API error ${response.status}: ${err}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const data = await response.json();
|
|
50
|
-
|
|
51
|
-
// Extract text content
|
|
52
|
-
const text = data.content
|
|
53
|
-
.filter((b) => b.type === "text")
|
|
54
|
-
.map((b) => b.text)
|
|
55
|
-
.join("");
|
|
56
|
-
|
|
57
|
-
return { text, raw: data };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Call Claude API expecting JSON response
|
|
62
|
-
* Robust: tries 3 extraction strategies + auto-repair for truncated responses
|
|
63
|
-
*/
|
|
64
|
-
export async function callClaudeJSON(systemPrompt, userMessage, options = {}) {
|
|
65
|
-
const sys = systemPrompt + "\n\nYou MUST respond with valid JSON only. No markdown, no explanation, no backticks. Pure JSON.";
|
|
66
|
-
const { text } = await callClaude(sys, userMessage, { ...options, maxTokens: options.maxTokens || MAX_TOKENS });
|
|
67
|
-
|
|
68
|
-
// Try 1: direct parse after stripping backticks
|
|
69
|
-
try {
|
|
70
|
-
const clean = text.replace(/```json\n?|```/g, "").trim();
|
|
71
|
-
return JSON.parse(clean);
|
|
72
|
-
} catch {}
|
|
73
|
-
|
|
74
|
-
// Try 2: extract first { } or [ ] block
|
|
75
|
-
try {
|
|
76
|
-
const match = text.match(/(\{[\s\S]*\}|\[[\s\S]*\])/);
|
|
77
|
-
if (match) return JSON.parse(match[1]);
|
|
78
|
-
} catch {}
|
|
79
|
-
|
|
80
|
-
// Try 3: ask Claude to repair truncated/broken JSON
|
|
81
|
-
try {
|
|
82
|
-
const repair = await callClaude(
|
|
83
|
-
"You are a JSON repair tool. Fix this broken or truncated JSON. Return ONLY valid JSON, nothing else, no backticks.",
|
|
84
|
-
`Broken JSON:\n${text.slice(0, 8000)}`
|
|
85
|
-
);
|
|
86
|
-
const clean = repair.text.replace(/```json\n?|```/g, "").trim();
|
|
87
|
-
return JSON.parse(clean);
|
|
88
|
-
} catch {}
|
|
89
|
-
|
|
90
|
-
throw new Error(`Failed to parse JSON after 3 attempts. Preview: ${text.slice(0, 300)}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Call Claude with an image (for visual QA)
|
|
95
|
-
*/
|
|
96
|
-
export async function callClaudeWithImage(systemPrompt, userMessage, imageBase64, mediaType = "image/png") {
|
|
97
|
-
const body = {
|
|
98
|
-
model: MODEL,
|
|
99
|
-
max_tokens: MAX_TOKENS,
|
|
100
|
-
system: systemPrompt,
|
|
101
|
-
messages: [
|
|
102
|
-
{
|
|
103
|
-
role: "user",
|
|
104
|
-
content: [
|
|
105
|
-
{
|
|
106
|
-
type: "image",
|
|
107
|
-
source: { type: "base64", media_type: mediaType, data: imageBase64 },
|
|
108
|
-
},
|
|
109
|
-
{ type: "text", text: userMessage },
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
116
|
-
method: "POST",
|
|
117
|
-
headers: getHeaders(),
|
|
118
|
-
body: JSON.stringify(body),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const data = await response.json();
|
|
122
|
-
const text = data.content?.filter((b) => b.type === "text").map((b) => b.text).join("") || "";
|
|
123
|
-
return { text, raw: data };
|
|
124
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
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
|
-
}
|