claudeboard 2.15.4 → 3.1.0

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.
@@ -1,141 +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 = `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
- /**
19
- * Build a concise snapshot of the existing project:
20
- * - Top-level file/folder structure
21
- * - package.json dependencies
22
- * - app.json expo config (if present)
23
- */
24
- function getProjectSnapshot(projectPath) {
25
- const lines = [];
26
-
27
- // Top-level structure (exclude hidden files and node_modules)
28
- try {
29
- const entries = fs.readdirSync(projectPath)
30
- .filter(f => !f.startsWith('.') && f !== 'node_modules')
31
- .sort();
32
- lines.push("### Project structure (top level)");
33
- for (const entry of entries) {
34
- const fullPath = path.join(projectPath, entry);
35
- const isDir = fs.statSync(fullPath).isDirectory();
36
- if (isDir) {
37
- // List one level deep for key folders
38
- const children = fs.readdirSync(fullPath)
39
- .filter(f => !f.startsWith('.') && f !== 'node_modules')
40
- .slice(0, 15);
41
- lines.push(`${entry}/`);
42
- for (const child of children) lines.push(` ${entry}/${child}`);
43
- } else {
44
- lines.push(entry);
45
- }
46
- }
47
- } catch {}
48
-
49
- // package.json
50
- try {
51
- const pkg = JSON.parse(fs.readFileSync(path.join(projectPath, "package.json"), "utf8"));
52
- lines.push("\n### package.json dependencies");
53
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
54
- for (const [name, version] of Object.entries(deps)) {
55
- lines.push(` ${name}: ${version}`);
56
- }
57
- } catch {}
58
-
59
- // app.json
60
- try {
61
- const appJson = JSON.parse(fs.readFileSync(path.join(projectPath, "app.json"), "utf8"));
62
- lines.push("\n### app.json (expo config)");
63
- lines.push(JSON.stringify(appJson.expo || appJson, null, 2).slice(0, 1000));
64
- } catch {}
65
-
66
- return lines.join("\n");
67
- }
68
-
69
- export async function runArchitectAgent(prdContent, projectName, options = {}) {
70
- const { buildOnExisting = false, projectPath = null } = options;
71
-
72
- console.log(" Architect analyzing PRD...");
73
-
74
- const projectSnapshot = (buildOnExisting && projectPath)
75
- ? getProjectSnapshot(projectPath)
76
- : null;
77
-
78
- const existingProjectSection = projectSnapshot
79
- ? `\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.`
80
- : "";
81
-
82
- const result = await callClaudeJSON(SYSTEM, `
83
- Project: ${projectName}
84
-
85
- PRD:
86
- ${prdContent}
87
- ${existingProjectSection}
88
-
89
- Return this JSON structure:
90
- {
91
- "techStack": {
92
- "framework": "expo",
93
- "navigation": "expo-router or react-navigation",
94
- "stateManagement": "...",
95
- "backend": "supabase / firebase / none",
96
- "ui": "nativewind / tamagui / stylesheet",
97
- "otherDeps": ["list of npm packages needed"]
98
- },
99
- "epics": [
100
- {
101
- "name": "Epic name (e.g. Project Setup, Authentication, Home Screen)",
102
- "order": 1,
103
- "tasks": [
104
- {
105
- "title": "Specific task title",
106
- "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.",
107
- "priority": "high|medium|low",
108
- "type": "config|feature|test",
109
- "acceptanceCriteria": "How to verify this task is done correctly"
110
- }
111
- ]
112
- }
113
- ]
114
- }
115
-
116
- Order epics from first to last in implementation order.
117
- Include as many tasks as needed for a complete, production-ready mobile app — do not artificially limit the count.`, { maxTokens: 16000 });
118
-
119
- console.log(` ✓ Architect created ${result.epics.length} epics`);
120
-
121
- // Persist to board
122
- let totalTasks = 0;
123
- for (let i = 0; i < result.epics.length; i++) {
124
- const epicData = result.epics[i];
125
- const epicId = await createEpic(epicData.name);
126
-
127
- for (const task of epicData.tasks) {
128
- await createTask({
129
- epicId,
130
- title: task.title,
131
- description: `${task.description}\n\nAcceptance: ${task.acceptanceCriteria || ""}`,
132
- priority: task.priority,
133
- type: task.type,
134
- });
135
- totalTasks++;
136
- }
137
- }
138
-
139
- console.log(` ✓ Created ${totalTasks} tasks in board`);
140
- return { techStack: result.techStack, epics: result.epics, totalTasks };
141
- }
@@ -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
- }
@@ -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
- }