claude-think 0.4.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-think",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Personal context manager for Claude - manage your preferences, patterns, and memory",
5
5
  "author": "Amit Feldman",
6
6
  "license": "MIT",
@@ -1,18 +1,11 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { writeFile, readFile, readdir } from "fs/promises";
1
+ import { existsSync } from "fs";
2
+ import { writeFile } from "fs/promises";
3
3
  import { join } from "path";
4
4
  import chalk from "chalk";
5
- import { detectProject, ProjectType } from "../../core/project-detect";
6
-
7
- const IGNORE = new Set([
8
- "node_modules", ".git", "dist", "build", ".next", "__pycache__",
9
- ".venv", "venv", "target", ".cache", "coverage", ".turbo", ".DS_Store",
10
- ".idea", ".vscode", "vendor", "tmp", "temp", "logs",
11
- ]);
5
+ import { detectProject } from "../../core/project-detect";
12
6
 
13
7
  /**
14
- * Generate a compact project CLAUDE.md
15
- * Efficient: just structure summary, no AI, minimal context usage
8
+ * Generate a project CLAUDE.md with detected info
16
9
  */
17
10
  export async function projectLearnCommand(options: {
18
11
  force?: boolean;
@@ -26,191 +19,95 @@ export async function projectLearnCommand(options: {
26
19
  return;
27
20
  }
28
21
 
29
- console.log(chalk.blue("Scanning project..."));
22
+ console.log(chalk.blue("Analyzing project..."));
30
23
 
31
24
  const project = await detectProject(cwd);
32
- const structure = await scanStructure(cwd);
33
- const entryPoints = findEntryPoints(cwd, project.type);
34
-
35
- // Build compact CLAUDE.md
36
25
  const lines: string[] = [];
37
26
 
27
+ // Header
38
28
  lines.push(`# ${project.name}`);
39
29
  lines.push("");
40
- lines.push(`${project.type} project${structure.description ? `: ${structure.description}` : ""}`);
41
- lines.push("");
42
30
 
43
- // Entry points (most important for navigation)
44
- if (entryPoints.length > 0) {
45
- lines.push(`**Entry:** ${entryPoints.join(", ")}`);
31
+ // Description
32
+ if (project.description) {
33
+ lines.push(project.description);
46
34
  lines.push("");
47
35
  }
48
36
 
49
- // Compact directory summary
50
- lines.push("**Structure:**");
51
- for (const dir of structure.dirs) {
52
- lines.push(`- \`${dir.name}/\` ${dir.count} files${dir.hint ? ` - ${dir.hint}` : ""}`);
37
+ // Runtime & Stack summary
38
+ const stackParts: string[] = [];
39
+ stackParts.push(project.runtime.charAt(0).toUpperCase() + project.runtime.slice(1));
40
+ if (project.monorepo) {
41
+ stackParts.push(project.monorepo.tool);
53
42
  }
54
-
55
- // Root files (configs, docs)
56
- if (structure.rootFiles.length > 0) {
57
- lines.push(`- root: ${structure.rootFiles.join(", ")}`);
43
+ if (project.frameworks.length > 0) {
44
+ stackParts.push(...project.frameworks.slice(0, 3));
58
45
  }
46
+ lines.push(`**Stack:** ${stackParts.join(", ")}`);
59
47
  lines.push("");
60
48
 
61
- await writeFile(claudeMdPath, lines.join("\n"));
62
-
63
- console.log(chalk.green("Created compact CLAUDE.md"));
64
- console.log(chalk.dim(`${lines.length} lines, ~${lines.join("\n").length} bytes`));
65
- }
66
-
67
- interface DirSummary {
68
- name: string;
69
- count: number;
70
- hint?: string;
71
- }
72
-
73
- interface StructureSummary {
74
- dirs: DirSummary[];
75
- rootFiles: string[];
76
- description?: string;
77
- }
49
+ // Monorepo structure
50
+ if (project.monorepo) {
51
+ lines.push("## Workspaces");
52
+ lines.push("");
78
53
 
79
- async function scanStructure(dir: string): Promise<StructureSummary> {
80
- const entries = await readdir(dir, { withFileTypes: true });
81
- const dirs: DirSummary[] = [];
82
- const rootFiles: string[] = [];
83
- let description: string | undefined;
84
-
85
- // Get description from package.json
86
- const pkgPath = join(dir, "package.json");
87
- if (existsSync(pkgPath)) {
88
- try {
89
- const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
90
- description = pkg.description;
91
- } catch {}
92
- }
54
+ // Group by type
55
+ const grouped = new Map<string, typeof project.monorepo.workspaces>();
56
+ for (const ws of project.monorepo.workspaces) {
57
+ const type = ws.type || "other";
58
+ if (!grouped.has(type)) grouped.set(type, []);
59
+ grouped.get(type)!.push(ws);
60
+ }
93
61
 
94
- for (const entry of entries) {
95
- if (IGNORE.has(entry.name) || entry.name.startsWith(".")) continue;
96
-
97
- if (entry.isDirectory()) {
98
- const count = await countFiles(join(dir, entry.name));
99
- if (count > 0) {
100
- dirs.push({
101
- name: entry.name,
102
- count,
103
- hint: getDirHint(entry.name),
104
- });
62
+ // Output grouped
63
+ const typeOrder = ["app", "server", "service", "package", "cli", "tool", "other"];
64
+ for (const type of typeOrder) {
65
+ const workspaces = grouped.get(type);
66
+ if (!workspaces || workspaces.length === 0) continue;
67
+
68
+ const typeLabel = type === "other" ? "Other" : type.charAt(0).toUpperCase() + type.slice(1) + "s";
69
+ lines.push(`### ${typeLabel}`);
70
+ for (const ws of workspaces) {
71
+ const desc = ws.description ? ` - ${ws.description}` : "";
72
+ lines.push(`- \`${ws.path}\` (${ws.name})${desc}`);
105
73
  }
106
- } else if (isRelevantRootFile(entry.name)) {
107
- rootFiles.push(entry.name);
74
+ lines.push("");
108
75
  }
109
76
  }
110
77
 
111
- // Sort dirs by importance
112
- dirs.sort((a, b) => {
113
- const order = ["src", "app", "lib", "pages", "components", "api", "test", "tests", "scripts", "docs"];
114
- const aIdx = order.indexOf(a.name);
115
- const bIdx = order.indexOf(b.name);
116
- if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx;
117
- if (aIdx !== -1) return -1;
118
- if (bIdx !== -1) return 1;
119
- return a.name.localeCompare(b.name);
120
- });
121
-
122
- return { dirs, rootFiles, description };
123
- }
78
+ // Tooling
79
+ if (project.tooling.length > 0) {
80
+ lines.push("## Tooling");
81
+ lines.push("");
82
+ lines.push(project.tooling.join(", "));
83
+ lines.push("");
84
+ }
124
85
 
125
- async function countFiles(dir: string): Promise<number> {
126
- let count = 0;
127
- try {
128
- const entries = await readdir(dir, { withFileTypes: true });
129
- for (const entry of entries) {
130
- if (IGNORE.has(entry.name) || entry.name.startsWith(".")) continue;
131
- if (entry.isDirectory()) {
132
- count += await countFiles(join(dir, entry.name));
133
- } else {
134
- count++;
135
- }
136
- }
137
- } catch {}
138
- return count;
139
- }
86
+ // Frameworks (if not already shown in stack or there are more)
87
+ if (project.frameworks.length > 3) {
88
+ lines.push("## Frameworks");
89
+ lines.push("");
90
+ lines.push(project.frameworks.join(", "));
91
+ lines.push("");
92
+ }
140
93
 
141
- function isRelevantRootFile(name: string): boolean {
142
- const relevant = [
143
- "package.json", "tsconfig.json", "Cargo.toml", "go.mod", "pyproject.toml",
144
- "Gemfile", "Makefile", "docker-compose.yml", "Dockerfile",
145
- "README.md", "CHANGELOG.md",
146
- ];
147
- return relevant.includes(name);
148
- }
94
+ const content = lines.join("\n");
95
+ await writeFile(claudeMdPath, content);
149
96
 
150
- function getDirHint(name: string): string | undefined {
151
- const hints: Record<string, string> = {
152
- src: "source code",
153
- app: "application",
154
- lib: "library",
155
- pages: "routes",
156
- components: "UI",
157
- api: "endpoints",
158
- test: "tests",
159
- tests: "tests",
160
- scripts: "tooling",
161
- docs: "documentation",
162
- public: "static assets",
163
- assets: "resources",
164
- config: "configuration",
165
- utils: "utilities",
166
- hooks: "React hooks",
167
- services: "business logic",
168
- models: "data models",
169
- controllers: "request handlers",
170
- middleware: "middleware",
171
- types: "TypeScript types",
172
- };
173
- return hints[name];
174
- }
97
+ console.log(chalk.green("Created CLAUDE.md"));
98
+ console.log(chalk.dim(`${lines.length} lines, ${content.length} bytes`));
175
99
 
176
- function findEntryPoints(dir: string, type: ProjectType): string[] {
177
- const entries: string[] = [];
178
-
179
- // Try package.json bin/main first
180
- const pkgPath = join(dir, "package.json");
181
- if (existsSync(pkgPath)) {
182
- try {
183
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
184
- if (pkg.bin) {
185
- const bins = typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin);
186
- for (const bin of bins as string[]) {
187
- if (bin && !entries.includes(bin)) entries.push(bin);
188
- }
189
- }
190
- if (pkg.main && !entries.includes(pkg.main)) entries.push(pkg.main);
191
- if (pkg.module && !entries.includes(pkg.module)) entries.push(pkg.module);
192
- } catch {}
100
+ // Show summary
101
+ console.log("");
102
+ console.log(chalk.cyan("Detected:"));
103
+ console.log(` Runtime: ${project.runtime}`);
104
+ if (project.monorepo) {
105
+ console.log(` Monorepo: ${project.monorepo.tool} (${project.monorepo.workspaces.length} workspaces)`);
193
106
  }
194
-
195
- // Fallback to common patterns
196
- if (entries.length === 0) {
197
- const candidates = [
198
- "src/index.ts", "src/index.tsx", "src/main.ts", "src/main.tsx",
199
- "src/app.ts", "src/app.tsx", "src/server.ts",
200
- "app/page.tsx", "pages/index.tsx",
201
- "index.ts", "index.tsx", "main.ts", "main.py",
202
- "src/lib.rs", "src/main.rs",
203
- "cmd/main.go", "main.go",
204
- "app.rb", "config.ru",
205
- ];
206
-
207
- for (const candidate of candidates) {
208
- if (existsSync(join(dir, candidate))) {
209
- entries.push(candidate);
210
- if (entries.length >= 2) break;
211
- }
212
- }
107
+ if (project.frameworks.length > 0) {
108
+ console.log(` Frameworks: ${project.frameworks.join(", ")}`);
109
+ }
110
+ if (project.tooling.length > 0) {
111
+ console.log(` Tooling: ${project.tooling.join(", ")}`);
213
112
  }
214
-
215
- return entries.slice(0, 3); // Max 3
216
113
  }
@@ -1,76 +1,351 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, readFileSync, readdirSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
4
  export interface ProjectInfo {
5
- type: ProjectType;
6
5
  name: string;
6
+ description?: string;
7
7
  root: string;
8
+ runtime: Runtime;
9
+ monorepo?: MonorepoInfo;
10
+ frameworks: string[];
11
+ tooling: string[];
8
12
  }
9
13
 
10
- export type ProjectType =
11
- | "node"
12
- | "bun"
13
- | "deno"
14
- | "rust"
15
- | "python"
16
- | "go"
17
- | "java"
18
- | "ruby"
19
- | "unknown";
20
-
21
- const PROJECT_MARKERS: Record<ProjectType, string[]> = {
22
- bun: ["bun.lockb", "bunfig.toml"],
23
- node: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
24
- deno: ["deno.json", "deno.jsonc"],
25
- rust: ["Cargo.toml"],
26
- python: ["pyproject.toml", "setup.py", "requirements.txt", "Pipfile"],
27
- go: ["go.mod"],
28
- java: ["pom.xml", "build.gradle", "build.gradle.kts"],
29
- ruby: ["Gemfile"],
30
- unknown: [],
31
- };
14
+ export interface MonorepoInfo {
15
+ tool: string;
16
+ workspaces: WorkspaceInfo[];
17
+ }
18
+
19
+ export interface WorkspaceInfo {
20
+ name: string;
21
+ path: string;
22
+ description?: string;
23
+ type?: string; // "app" | "package" | "service" | etc.
24
+ }
25
+
26
+ export type Runtime = "bun" | "node" | "deno" | "rust" | "python" | "go" | "java" | "ruby" | "unknown";
32
27
 
33
28
  /**
34
- * Detect project type from the given directory
29
+ * Detect project info from the given directory
35
30
  */
36
31
  export async function detectProject(dir: string): Promise<ProjectInfo> {
37
- const root = dir;
38
-
39
- // Auto-detect based on marker files
40
- // Check Bun first (more specific than Node)
41
- for (const [type, markers] of Object.entries(PROJECT_MARKERS)) {
42
- for (const marker of markers) {
43
- if (existsSync(join(root, marker))) {
44
- return {
45
- type: type as ProjectType,
46
- name: detectProjectName(root),
47
- root,
48
- };
32
+ const pkg = readPackageJson(dir);
33
+
34
+ // Check for monorepo first
35
+ const monorepo = detectMonorepo(dir, pkg);
36
+
37
+ // Collect all frameworks and tooling (including from workspaces)
38
+ let frameworks = detectFrameworks(dir, pkg);
39
+ let tooling = detectTooling(dir, pkg);
40
+
41
+ // If monorepo, also scan workspaces for frameworks
42
+ if (monorepo) {
43
+ for (const ws of monorepo.workspaces) {
44
+ const wsPath = join(dir, ws.path);
45
+ const wsPkg = readPackageJson(wsPath);
46
+ if (wsPkg) {
47
+ frameworks = [...frameworks, ...detectFrameworks(wsPath, wsPkg)];
48
+ // Only add key tooling from workspaces, not everything
49
+ const wsTooling = detectFrameworks(wsPath, wsPkg);
50
+ tooling = [...tooling, ...wsTooling.filter(t =>
51
+ ["Tauri", "Electron", "React Native", "Expo"].includes(t)
52
+ )];
49
53
  }
50
54
  }
51
55
  }
52
56
 
53
- return {
54
- type: "unknown",
55
- name: detectProjectName(root),
56
- root,
57
+ const info: ProjectInfo = {
58
+ name: pkg?.name || detectProjectName(dir),
59
+ description: pkg?.description || extractReadmeDescription(dir),
60
+ root: dir,
61
+ runtime: detectRuntime(dir),
62
+ frameworks: [...new Set(frameworks)],
63
+ tooling: [...new Set(tooling)],
57
64
  };
65
+
66
+ if (monorepo) {
67
+ info.monorepo = monorepo;
68
+ }
69
+
70
+ return info;
58
71
  }
59
72
 
60
- function detectProjectName(root: string): string {
61
- // Try to get name from package.json
62
- const pkgPath = join(root, "package.json");
63
- if (existsSync(pkgPath)) {
64
- try {
65
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
66
- if (pkg.name) return pkg.name;
67
- } catch {}
73
+ function readPackageJson(dir: string): any | null {
74
+ const pkgPath = join(dir, "package.json");
75
+ if (!existsSync(pkgPath)) return null;
76
+ try {
77
+ return JSON.parse(readFileSync(pkgPath, "utf-8"));
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ function detectRuntime(dir: string): Runtime {
84
+ // Order matters - more specific first
85
+ if (existsSync(join(dir, "bun.lock")) || existsSync(join(dir, "bunfig.toml"))) return "bun";
86
+ if (existsSync(join(dir, "deno.json")) || existsSync(join(dir, "deno.jsonc"))) return "deno";
87
+ if (existsSync(join(dir, "Cargo.toml"))) return "rust";
88
+ if (existsSync(join(dir, "go.mod"))) return "go";
89
+ if (existsSync(join(dir, "pyproject.toml")) || existsSync(join(dir, "requirements.txt"))) return "python";
90
+ if (existsSync(join(dir, "Gemfile"))) return "ruby";
91
+ if (existsSync(join(dir, "pom.xml")) || existsSync(join(dir, "build.gradle"))) return "java";
92
+ if (existsSync(join(dir, "package.json"))) return "node";
93
+ return "unknown";
94
+ }
95
+
96
+ function detectMonorepo(dir: string, pkg: any): MonorepoInfo | undefined {
97
+ let tool: string | undefined;
98
+ let workspacePatterns: string[] = [];
99
+
100
+ // Detect monorepo tool
101
+ if (existsSync(join(dir, "turbo.json"))) {
102
+ tool = "Turborepo";
103
+ } else if (existsSync(join(dir, "nx.json"))) {
104
+ tool = "Nx";
105
+ } else if (existsSync(join(dir, "lerna.json"))) {
106
+ tool = "Lerna";
107
+ } else if (existsSync(join(dir, "pnpm-workspace.yaml"))) {
108
+ tool = "pnpm workspaces";
109
+ workspacePatterns = parsePnpmWorkspaces(dir);
68
110
  }
69
111
 
112
+ // Get workspace patterns from package.json
113
+ if (pkg?.workspaces) {
114
+ const ws = pkg.workspaces;
115
+ workspacePatterns = Array.isArray(ws) ? ws : ws.packages || [];
116
+
117
+ if (!tool) {
118
+ // Detect workspace tool from lockfile
119
+ if (existsSync(join(dir, "bun.lock"))) tool = "Bun workspaces";
120
+ else if (existsSync(join(dir, "pnpm-lock.yaml"))) tool = "pnpm workspaces";
121
+ else if (existsSync(join(dir, "yarn.lock"))) tool = "Yarn workspaces";
122
+ else tool = "npm workspaces";
123
+ }
124
+ }
125
+
126
+ if (!tool || workspacePatterns.length === 0) return undefined;
127
+
128
+ // Resolve workspaces
129
+ const workspaces = resolveWorkspaces(dir, workspacePatterns);
130
+ if (workspaces.length === 0) return undefined;
131
+
132
+ return { tool, workspaces };
133
+ }
134
+
135
+ function parsePnpmWorkspaces(dir: string): string[] {
136
+ const wsPath = join(dir, "pnpm-workspace.yaml");
137
+ if (!existsSync(wsPath)) return [];
138
+ try {
139
+ const content = readFileSync(wsPath, "utf-8");
140
+ const match = content.match(/packages:\s*\n((?:\s*-\s*.+\n?)+)/);
141
+ if (match) {
142
+ return match[1].split("\n")
143
+ .map(line => line.replace(/^\s*-\s*['"]?|['"]?\s*$/g, ""))
144
+ .filter(Boolean);
145
+ }
146
+ } catch {}
147
+ return [];
148
+ }
149
+
150
+ function resolveWorkspaces(dir: string, patterns: string[]): WorkspaceInfo[] {
151
+ const workspaces: WorkspaceInfo[] = [];
152
+
153
+ for (const pattern of patterns) {
154
+ // Handle glob patterns like "apps/*", "packages/*"
155
+ if (pattern.endsWith("/*")) {
156
+ const baseDir = pattern.slice(0, -2);
157
+ const fullPath = join(dir, baseDir);
158
+ if (existsSync(fullPath)) {
159
+ try {
160
+ const entries = readdirSync(fullPath, { withFileTypes: true });
161
+ for (const entry of entries) {
162
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
163
+ const wsPath = join(fullPath, entry.name);
164
+ const wsPkg = readPackageJson(wsPath);
165
+ workspaces.push({
166
+ name: wsPkg?.name || entry.name,
167
+ path: `${baseDir}/${entry.name}`,
168
+ description: wsPkg?.description,
169
+ type: inferWorkspaceType(baseDir, entry.name, wsPkg),
170
+ });
171
+ }
172
+ }
173
+ } catch {}
174
+ }
175
+ } else {
176
+ // Direct path
177
+ const wsPath = join(dir, pattern);
178
+ if (existsSync(wsPath)) {
179
+ const wsPkg = readPackageJson(wsPath);
180
+ workspaces.push({
181
+ name: wsPkg?.name || pattern.split("/").pop() || pattern,
182
+ path: pattern,
183
+ description: wsPkg?.description,
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ return workspaces;
190
+ }
191
+
192
+ function inferWorkspaceType(baseDir: string, name: string, pkg: any): string | undefined {
193
+ // From directory name
194
+ if (baseDir === "apps" || baseDir === "applications") return "app";
195
+ if (baseDir === "packages" || baseDir === "libs") return "package";
196
+ if (baseDir === "services") return "service";
197
+ if (baseDir === "tools" || baseDir === "tooling") return "tool";
198
+
199
+ // From package.json hints
200
+ if (pkg?.bin) return "cli";
201
+ if (pkg?.main?.includes("server") || pkg?.name?.includes("server")) return "server";
202
+ if (pkg?.name?.includes("client") || pkg?.dependencies?.react) return "app";
203
+
204
+ return undefined;
205
+ }
206
+
207
+ function detectFrameworks(dir: string, pkg: any): string[] {
208
+ const frameworks: string[] = [];
209
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
210
+
211
+ // Frontend
212
+ if (deps?.react) frameworks.push("React");
213
+ if (deps?.vue) frameworks.push("Vue");
214
+ if (deps?.svelte) frameworks.push("Svelte");
215
+ if (deps?.angular || deps?.["@angular/core"]) frameworks.push("Angular");
216
+ if (deps?.solid || deps?.["solid-js"]) frameworks.push("Solid");
217
+
218
+ // Meta-frameworks
219
+ if (deps?.next) frameworks.push("Next.js");
220
+ if (deps?.nuxt) frameworks.push("Nuxt");
221
+ if (deps?.astro) frameworks.push("Astro");
222
+ if (deps?.remix || deps?.["@remix-run/node"]) frameworks.push("Remix");
223
+
224
+ // Backend
225
+ if (deps?.express) frameworks.push("Express");
226
+ if (deps?.fastify) frameworks.push("Fastify");
227
+ if (deps?.hono) frameworks.push("Hono");
228
+ if (deps?.elysia) frameworks.push("Elysia");
229
+ if (deps?.["@nestjs/core"]) frameworks.push("NestJS");
230
+
231
+ // Desktop/Mobile
232
+ if (existsSync(join(dir, "tauri.conf.json")) || existsSync(join(dir, "src-tauri"))) {
233
+ frameworks.push("Tauri");
234
+ }
235
+ if (deps?.electron) frameworks.push("Electron");
236
+ if (deps?.["react-native"]) frameworks.push("React Native");
237
+ if (deps?.expo) frameworks.push("Expo");
238
+
239
+ // AI/ML
240
+ if (deps?.["@anthropic-ai/sdk"] || deps?.["@anthropic-ai/claude-agent-sdk"]) {
241
+ frameworks.push("Claude SDK");
242
+ }
243
+ if (deps?.openai) frameworks.push("OpenAI");
244
+ if (deps?.langchain || deps?.["@langchain/core"]) frameworks.push("LangChain");
245
+
246
+ // Rust detection
247
+ const cargoPath = join(dir, "Cargo.toml");
248
+ if (existsSync(cargoPath)) {
249
+ const cargo = readFileSync(cargoPath, "utf-8");
250
+ if (cargo.includes("tauri")) frameworks.push("Tauri");
251
+ if (cargo.includes("actix")) frameworks.push("Actix");
252
+ if (cargo.includes("axum")) frameworks.push("Axum");
253
+ if (cargo.includes("rocket")) frameworks.push("Rocket");
254
+ }
255
+
256
+ return [...new Set(frameworks)]; // dedupe
257
+ }
258
+
259
+ function detectTooling(dir: string, pkg: any): string[] {
260
+ const tools: string[] = [];
261
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
262
+
263
+ // Build tools
264
+ if (existsSync(join(dir, "turbo.json"))) tools.push("Turborepo");
265
+ if (existsSync(join(dir, "nx.json"))) tools.push("Nx");
266
+ if (deps?.vite) tools.push("Vite");
267
+ if (deps?.webpack) tools.push("Webpack");
268
+ if (deps?.esbuild) tools.push("esbuild");
269
+ if (deps?.rollup) tools.push("Rollup");
270
+
271
+ // Linting/Formatting
272
+ if (deps?.["@biomejs/biome"] || existsSync(join(dir, "biome.json"))) tools.push("Biome");
273
+ if (deps?.eslint || existsSync(join(dir, ".eslintrc.json"))) tools.push("ESLint");
274
+ if (deps?.prettier || existsSync(join(dir, ".prettierrc"))) tools.push("Prettier");
275
+
276
+ // Testing
277
+ if (deps?.vitest) tools.push("Vitest");
278
+ if (deps?.jest) tools.push("Jest");
279
+ if (deps?.playwright || deps?.["@playwright/test"]) tools.push("Playwright");
280
+ if (deps?.cypress) tools.push("Cypress");
281
+
282
+ // Database/ORM
283
+ if (deps?.prisma || deps?.["@prisma/client"]) tools.push("Prisma");
284
+ if (deps?.drizzle || deps?.["drizzle-orm"]) tools.push("Drizzle");
285
+ if (deps?.typeorm) tools.push("TypeORM");
286
+ if (deps?.mongoose) tools.push("Mongoose");
287
+
288
+ // Other
289
+ if (existsSync(join(dir, "docker-compose.yml")) || existsSync(join(dir, "Dockerfile"))) {
290
+ tools.push("Docker");
291
+ }
292
+ if (deps?.tailwindcss || existsSync(join(dir, "tailwind.config.js"))) tools.push("Tailwind");
293
+ if (deps?.typescript || existsSync(join(dir, "tsconfig.json"))) tools.push("TypeScript");
294
+
295
+ return [...new Set(tools)];
296
+ }
297
+
298
+ function extractReadmeDescription(dir: string): string | undefined {
299
+ const readmePath = join(dir, "README.md");
300
+ if (!existsSync(readmePath)) return undefined;
301
+
302
+ try {
303
+ const content = readFileSync(readmePath, "utf-8");
304
+
305
+ // Try to find a tagline/description pattern (often in bold after title)
306
+ // Match patterns like "**The Agentic Development Environment**"
307
+ const taglineMatch = content.match(/\*\*([^*]{10,100})\*\*/);
308
+ if (taglineMatch && !taglineMatch[1].includes("http") && !taglineMatch[1].includes("badge")) {
309
+ return taglineMatch[1].trim();
310
+ }
311
+
312
+ // Try Overview/About section
313
+ const overviewMatch = content.match(/##\s*(?:Overview|About|Description)\s*\n+([^\n#]+)/i);
314
+ if (overviewMatch) {
315
+ return cleanMarkdown(overviewMatch[1]).slice(0, 200);
316
+ }
317
+
318
+ // Fall back to first paragraph
319
+ const lines = content.split("\n");
320
+ for (const line of lines) {
321
+ const trimmed = line.trim();
322
+ // Skip headings, badges, empty lines, HTML
323
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("[") ||
324
+ trimmed.startsWith("!") || trimmed.startsWith("<") ||
325
+ trimmed.startsWith("*") || trimmed.startsWith("-") ||
326
+ trimmed.startsWith("|") || trimmed.includes("badge")) {
327
+ continue;
328
+ }
329
+ return cleanMarkdown(trimmed).slice(0, 200);
330
+ }
331
+ } catch {}
332
+
333
+ return undefined;
334
+ }
335
+
336
+ function cleanMarkdown(text: string): string {
337
+ return text
338
+ .replace(/\*\*([^*]+)\*\*/g, "$1") // bold
339
+ .replace(/\*([^*]+)\*/g, "$1") // italic
340
+ .replace(/`([^`]+)`/g, "$1") // code
341
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links
342
+ .trim();
343
+ }
344
+
345
+ function detectProjectName(root: string): string {
70
346
  // Try Cargo.toml
71
347
  const cargoPath = join(root, "Cargo.toml");
72
348
  if (existsSync(cargoPath)) {
73
- // Simple parse - just look for name =
74
349
  try {
75
350
  const content = readFileSync(cargoPath, "utf-8");
76
351
  const match = content.match(/name\s*=\s*"([^"]+)"/);
package/CLAUDE.md DELETED
@@ -1,111 +0,0 @@
1
- ---
2
- description: Use Bun instead of Node.js, npm, pnpm, or vite.
3
- globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
4
- alwaysApply: false
5
- ---
6
-
7
- Default to using Bun instead of Node.js.
8
-
9
- - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
10
- - Use `bun test` instead of `jest` or `vitest`
11
- - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
12
- - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
13
- - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
14
- - Use `bunx <package> <command>` instead of `npx <package> <command>`
15
- - Bun automatically loads .env, so don't use dotenv.
16
-
17
- ## APIs
18
-
19
- - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
20
- - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
21
- - `Bun.redis` for Redis. Don't use `ioredis`.
22
- - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
23
- - `WebSocket` is built-in. Don't use `ws`.
24
- - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
25
- - Bun.$`ls` instead of execa.
26
-
27
- ## Testing
28
-
29
- Use `bun test` to run tests.
30
-
31
- ```ts#index.test.ts
32
- import { test, expect } from "bun:test";
33
-
34
- test("hello world", () => {
35
- expect(1).toBe(1);
36
- });
37
- ```
38
-
39
- ## Frontend
40
-
41
- Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
42
-
43
- Server:
44
-
45
- ```ts#index.ts
46
- import index from "./index.html"
47
-
48
- Bun.serve({
49
- routes: {
50
- "/": index,
51
- "/api/users/:id": {
52
- GET: (req) => {
53
- return new Response(JSON.stringify({ id: req.params.id }));
54
- },
55
- },
56
- },
57
- // optional websocket support
58
- websocket: {
59
- open: (ws) => {
60
- ws.send("Hello, world!");
61
- },
62
- message: (ws, message) => {
63
- ws.send(message);
64
- },
65
- close: (ws) => {
66
- // handle close
67
- }
68
- },
69
- development: {
70
- hmr: true,
71
- console: true,
72
- }
73
- })
74
- ```
75
-
76
- HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
77
-
78
- ```html#index.html
79
- <html>
80
- <body>
81
- <h1>Hello, world!</h1>
82
- <script type="module" src="./frontend.tsx"></script>
83
- </body>
84
- </html>
85
- ```
86
-
87
- With the following `frontend.tsx`:
88
-
89
- ```tsx#frontend.tsx
90
- import React from "react";
91
- import { createRoot } from "react-dom/client";
92
-
93
- // import .css files directly and it works
94
- import './index.css';
95
-
96
- const root = createRoot(document.body);
97
-
98
- export default function Frontend() {
99
- return <h1>Hello, world!</h1>;
100
- }
101
-
102
- root.render(<Frontend />);
103
- ```
104
-
105
- Then, run index.ts
106
-
107
- ```sh
108
- bun --hot ./index.ts
109
- ```
110
-
111
- For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.