claude-think 0.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.
- package/.claude/settings.local.json +19 -0
- package/.think.yaml +30 -0
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +111 -0
- package/README.md +97 -0
- package/bun.lock +144 -0
- package/package.json +37 -0
- package/src/cli/commands/allow.ts +46 -0
- package/src/cli/commands/edit.ts +57 -0
- package/src/cli/commands/help.ts +64 -0
- package/src/cli/commands/init.ts +76 -0
- package/src/cli/commands/learn.ts +50 -0
- package/src/cli/commands/profile.ts +30 -0
- package/src/cli/commands/project.ts +64 -0
- package/src/cli/commands/review.ts +130 -0
- package/src/cli/commands/setup.ts +220 -0
- package/src/cli/commands/status.ts +66 -0
- package/src/cli/commands/sync.ts +30 -0
- package/src/cli/commands/tree.ts +30 -0
- package/src/cli/index.ts +126 -0
- package/src/core/banner.ts +25 -0
- package/src/core/config.ts +57 -0
- package/src/core/dedup.ts +75 -0
- package/src/core/file-tree.ts +255 -0
- package/src/core/generator.ts +189 -0
- package/src/core/parser.ts +65 -0
- package/src/core/project-detect.ts +120 -0
- package/src/templates/allowed-commands.md +19 -0
- package/src/templates/anti-patterns.md +11 -0
- package/src/templates/corrections.md +4 -0
- package/src/templates/file-tree.md +28 -0
- package/src/templates/learnings.md +4 -0
- package/src/templates/patterns.md +10 -0
- package/src/templates/pending.md +5 -0
- package/src/templates/profile.md +11 -0
- package/src/templates/settings.md +6 -0
- package/src/templates/subagents.md +15 -0
- package/src/templates/tools.md +13 -0
- package/src/templates/workflows.md +9 -0
- package/src/tui/App.tsx +100 -0
- package/src/tui/components/Agents.tsx +117 -0
- package/src/tui/components/Automation.tsx +88 -0
- package/src/tui/components/Help.tsx +56 -0
- package/src/tui/components/Memory.tsx +101 -0
- package/src/tui/components/Navigation.tsx +63 -0
- package/src/tui/components/Permissions.tsx +88 -0
- package/src/tui/components/Preferences.tsx +89 -0
- package/src/tui/components/Profile.tsx +62 -0
- package/src/tui/components/Skills.tsx +118 -0
- package/src/tui/index.tsx +7 -0
- package/tsconfig.json +29 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initCommand } from "./commands/init";
|
|
4
|
+
import { syncCommand } from "./commands/sync";
|
|
5
|
+
import { learnCommand } from "./commands/learn";
|
|
6
|
+
import { statusCommand } from "./commands/status";
|
|
7
|
+
import { profileCommand } from "./commands/profile";
|
|
8
|
+
import { editCommand } from "./commands/edit";
|
|
9
|
+
import { allowCommand } from "./commands/allow";
|
|
10
|
+
import { reviewCommand } from "./commands/review";
|
|
11
|
+
import { treeCommand } from "./commands/tree";
|
|
12
|
+
import { projectInitCommand } from "./commands/project";
|
|
13
|
+
import { helpCommand } from "./commands/help";
|
|
14
|
+
import { setupCommand } from "./commands/setup";
|
|
15
|
+
import { printBanner } from "../core/banner";
|
|
16
|
+
import { launchTui } from "../tui";
|
|
17
|
+
|
|
18
|
+
const program = new Command();
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.name("think")
|
|
22
|
+
.description("Personal context manager for Claude")
|
|
23
|
+
.version("0.1.0");
|
|
24
|
+
|
|
25
|
+
// Initialize ~/.think
|
|
26
|
+
program
|
|
27
|
+
.command("init")
|
|
28
|
+
.description("Initialize ~/.think with starter templates")
|
|
29
|
+
.option("-f, --force", "Reinitialize even if already exists")
|
|
30
|
+
.action(initCommand);
|
|
31
|
+
|
|
32
|
+
// Sync to Claude plugin
|
|
33
|
+
program
|
|
34
|
+
.command("sync")
|
|
35
|
+
.description("Regenerate Claude plugin from ~/.think")
|
|
36
|
+
.action(syncCommand);
|
|
37
|
+
|
|
38
|
+
// Add a learning
|
|
39
|
+
program
|
|
40
|
+
.command("learn <learning>")
|
|
41
|
+
.description("Add a new learning")
|
|
42
|
+
.option("--no-sync", "Don't auto-sync after adding")
|
|
43
|
+
.action(learnCommand);
|
|
44
|
+
|
|
45
|
+
// Show status
|
|
46
|
+
program
|
|
47
|
+
.command("status")
|
|
48
|
+
.description("Show current think status")
|
|
49
|
+
.action(statusCommand);
|
|
50
|
+
|
|
51
|
+
// Edit profile
|
|
52
|
+
program
|
|
53
|
+
.command("profile")
|
|
54
|
+
.description("Open profile.md in $EDITOR")
|
|
55
|
+
.action(profileCommand);
|
|
56
|
+
|
|
57
|
+
// Edit any file
|
|
58
|
+
program
|
|
59
|
+
.command("edit <file>")
|
|
60
|
+
.description("Open a ~/.think file in $EDITOR")
|
|
61
|
+
.action(editCommand);
|
|
62
|
+
|
|
63
|
+
// Allow a command
|
|
64
|
+
program
|
|
65
|
+
.command("allow <command>")
|
|
66
|
+
.description("Add a command to the allowed list")
|
|
67
|
+
.option("--no-sync", "Don't auto-sync after adding")
|
|
68
|
+
.action(allowCommand);
|
|
69
|
+
|
|
70
|
+
// Review pending learnings
|
|
71
|
+
program
|
|
72
|
+
.command("review")
|
|
73
|
+
.description("Review pending learnings from Claude")
|
|
74
|
+
.action(reviewCommand);
|
|
75
|
+
|
|
76
|
+
// File tree preview
|
|
77
|
+
program
|
|
78
|
+
.command("tree")
|
|
79
|
+
.description("Preview file tree for current directory")
|
|
80
|
+
.action(treeCommand);
|
|
81
|
+
|
|
82
|
+
// Project commands
|
|
83
|
+
const projectCmd = program
|
|
84
|
+
.command("project")
|
|
85
|
+
.description("Project-specific commands");
|
|
86
|
+
|
|
87
|
+
projectCmd
|
|
88
|
+
.command("init")
|
|
89
|
+
.description("Initialize .think.yaml for current project")
|
|
90
|
+
.option("-f, --force", "Overwrite existing config")
|
|
91
|
+
.action(projectInitCommand);
|
|
92
|
+
|
|
93
|
+
// Help command
|
|
94
|
+
program
|
|
95
|
+
.command("help")
|
|
96
|
+
.description("Show help and command reference")
|
|
97
|
+
.action(helpCommand);
|
|
98
|
+
|
|
99
|
+
// Setup wizard
|
|
100
|
+
program
|
|
101
|
+
.command("setup")
|
|
102
|
+
.description("Interactive profile setup wizard")
|
|
103
|
+
.action(setupCommand);
|
|
104
|
+
|
|
105
|
+
// Default action (no subcommand) - launch TUI
|
|
106
|
+
program.action(async () => {
|
|
107
|
+
// Check if we have a TTY (required for TUI)
|
|
108
|
+
if (!process.stdin.isTTY) {
|
|
109
|
+
printBanner();
|
|
110
|
+
console.log("TUI requires an interactive terminal.\n");
|
|
111
|
+
console.log("Available commands:");
|
|
112
|
+
console.log(" think init Initialize ~/.think");
|
|
113
|
+
console.log(" think sync Sync to Claude plugin");
|
|
114
|
+
console.log(" think status Show status");
|
|
115
|
+
console.log(" think learn Add a learning");
|
|
116
|
+
console.log(" think review Review pending learnings");
|
|
117
|
+
console.log(" think profile Edit profile");
|
|
118
|
+
console.log(" think edit Edit any file");
|
|
119
|
+
console.log(" think allow Allow a command");
|
|
120
|
+
console.log();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
launchTui();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
program.parse();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
// Clean flush banner
|
|
4
|
+
const BANNER = `
|
|
5
|
+
╭────────────────────────────────────────────╮
|
|
6
|
+
│ │
|
|
7
|
+
│ ████████╗██╗ ██╗██╗███╗ ██╗██╗ ██╗ │
|
|
8
|
+
│ ╚══██╔══╝██║ ██║██║████╗ ██║██║ ██╔╝ │
|
|
9
|
+
│ ██║ ███████║██║██╔██╗ ██║█████╔╝ │
|
|
10
|
+
│ ██║ ██╔══██║██║██║╚██╗██║██╔═██╗ │
|
|
11
|
+
│ ██║ ██║ ██║██║██║ ╚████║██║ ██╗ │
|
|
12
|
+
│ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ │
|
|
13
|
+
│ │
|
|
14
|
+
│ Personal Context for Claude │
|
|
15
|
+
│ │
|
|
16
|
+
╰────────────────────────────────────────────╯
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
export function printBanner(): void {
|
|
20
|
+
console.log(chalk.green(BANNER));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function printCompactBanner(): void {
|
|
24
|
+
console.log(chalk.green.bold("\n THINK") + chalk.dim(" Personal Context for Claude\n"));
|
|
25
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
export const CONFIG = {
|
|
5
|
+
// Source directory (user's preferences)
|
|
6
|
+
thinkDir: join(homedir(), ".think"),
|
|
7
|
+
|
|
8
|
+
// Output directory (generated Claude plugin)
|
|
9
|
+
pluginDir: join(homedir(), ".claude", "plugins", "think"),
|
|
10
|
+
|
|
11
|
+
// Subdirectories in ~/.think
|
|
12
|
+
dirs: {
|
|
13
|
+
preferences: "preferences",
|
|
14
|
+
permissions: "permissions",
|
|
15
|
+
skills: "skills",
|
|
16
|
+
agents: "agents",
|
|
17
|
+
memory: "memory",
|
|
18
|
+
automation: "automation",
|
|
19
|
+
templates: "templates",
|
|
20
|
+
projects: "projects",
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// Core files
|
|
24
|
+
files: {
|
|
25
|
+
profile: "profile.md",
|
|
26
|
+
tools: "preferences/tools.md",
|
|
27
|
+
patterns: "preferences/patterns.md",
|
|
28
|
+
antiPatterns: "preferences/anti-patterns.md",
|
|
29
|
+
allowedCommands: "permissions/allowed-commands.md",
|
|
30
|
+
settings: "permissions/settings.md",
|
|
31
|
+
learnings: "memory/learnings.md",
|
|
32
|
+
corrections: "memory/corrections.md",
|
|
33
|
+
pending: "memory/pending.md",
|
|
34
|
+
subagents: "automation/subagents.md",
|
|
35
|
+
workflows: "automation/workflows.md",
|
|
36
|
+
fileTree: "templates/file-tree.md",
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Plugin output files
|
|
40
|
+
plugin: {
|
|
41
|
+
manifest: "plugin.json",
|
|
42
|
+
claudeMd: "CLAUDE.md",
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Project config file name
|
|
46
|
+
projectConfig: ".think.yaml",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
// Helper to get full path within ~/.think
|
|
50
|
+
export function thinkPath(...segments: string[]): string {
|
|
51
|
+
return join(CONFIG.thinkDir, ...segments);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Helper to get full path within plugin output
|
|
55
|
+
export function pluginPath(...segments: string[]): string {
|
|
56
|
+
return join(CONFIG.pluginDir, ...segments);
|
|
57
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { compareTwoStrings } from "string-similarity";
|
|
2
|
+
|
|
3
|
+
const SIMILARITY_THRESHOLD = 0.7;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize text for comparison
|
|
7
|
+
*/
|
|
8
|
+
function normalize(text: string): string {
|
|
9
|
+
return text
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^\w\s]/g, "") // Remove punctuation
|
|
12
|
+
.replace(/\s+/g, " ") // Collapse whitespace
|
|
13
|
+
.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if a new learning is similar to any existing learnings
|
|
18
|
+
* Returns the similar learning if found, null otherwise
|
|
19
|
+
*/
|
|
20
|
+
export function findSimilar(
|
|
21
|
+
newLearning: string,
|
|
22
|
+
existingLearnings: string[]
|
|
23
|
+
): string | null {
|
|
24
|
+
const normalizedNew = normalize(newLearning);
|
|
25
|
+
|
|
26
|
+
for (const existing of existingLearnings) {
|
|
27
|
+
const normalizedExisting = normalize(existing);
|
|
28
|
+
const similarity = compareTwoStrings(normalizedNew, normalizedExisting);
|
|
29
|
+
|
|
30
|
+
if (similarity >= SIMILARITY_THRESHOLD) {
|
|
31
|
+
return existing;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract individual learnings from markdown content
|
|
40
|
+
* Assumes learnings are bullet points starting with -
|
|
41
|
+
*/
|
|
42
|
+
export function extractLearnings(content: string): string[] {
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
const learnings: string[] = [];
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (trimmed.startsWith("- ")) {
|
|
49
|
+
learnings.push(trimmed.slice(2).trim());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return learnings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add a learning to content if not duplicate
|
|
58
|
+
* Returns { added: boolean, similar?: string, newContent: string }
|
|
59
|
+
*/
|
|
60
|
+
export function addLearning(
|
|
61
|
+
content: string,
|
|
62
|
+
newLearning: string
|
|
63
|
+
): { added: boolean; similar?: string; newContent: string } {
|
|
64
|
+
const existingLearnings = extractLearnings(content);
|
|
65
|
+
const similar = findSimilar(newLearning, existingLearnings);
|
|
66
|
+
|
|
67
|
+
if (similar) {
|
|
68
|
+
return { added: false, similar, newContent: content };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const newLine = `- ${newLearning}`;
|
|
72
|
+
const newContent = content.trim() ? `${content.trim()}\n${newLine}` : newLine;
|
|
73
|
+
|
|
74
|
+
return { added: true, newContent };
|
|
75
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { readdir, stat, readFile } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join, relative, basename } from "path";
|
|
4
|
+
import { parseMarkdown } from "./parser";
|
|
5
|
+
import { thinkPath, CONFIG } from "./config";
|
|
6
|
+
|
|
7
|
+
interface FileTreeConfig {
|
|
8
|
+
ignorePatterns: string[];
|
|
9
|
+
maxDepth: number;
|
|
10
|
+
annotations: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TreeNode {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
type: "file" | "directory";
|
|
17
|
+
annotation?: string;
|
|
18
|
+
children?: TreeNode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_IGNORE = [
|
|
22
|
+
"node_modules",
|
|
23
|
+
".git",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
".next",
|
|
27
|
+
"__pycache__",
|
|
28
|
+
".venv",
|
|
29
|
+
"venv",
|
|
30
|
+
"target",
|
|
31
|
+
".cache",
|
|
32
|
+
"coverage",
|
|
33
|
+
".turbo",
|
|
34
|
+
".DS_Store",
|
|
35
|
+
"*.pyc",
|
|
36
|
+
"*.pyo",
|
|
37
|
+
".env",
|
|
38
|
+
".env.*",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const DEFAULT_ANNOTATIONS: Record<string, string> = {
|
|
42
|
+
"package.json": "project manifest",
|
|
43
|
+
"tsconfig.json": "TypeScript config",
|
|
44
|
+
"Cargo.toml": "Rust manifest",
|
|
45
|
+
"pyproject.toml": "Python config",
|
|
46
|
+
"go.mod": "Go module",
|
|
47
|
+
"Gemfile": "Ruby dependencies",
|
|
48
|
+
"README.md": "documentation",
|
|
49
|
+
"CLAUDE.md": "Claude context",
|
|
50
|
+
".env.example": "environment template",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load file tree configuration from ~/.think/templates/file-tree.md
|
|
55
|
+
*/
|
|
56
|
+
async function loadConfig(): Promise<FileTreeConfig> {
|
|
57
|
+
const configPath = thinkPath(CONFIG.files.fileTree);
|
|
58
|
+
|
|
59
|
+
if (!existsSync(configPath)) {
|
|
60
|
+
return {
|
|
61
|
+
ignorePatterns: DEFAULT_IGNORE,
|
|
62
|
+
maxDepth: 4,
|
|
63
|
+
annotations: DEFAULT_ANNOTATIONS,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parsed = await parseMarkdown(configPath);
|
|
68
|
+
if (!parsed) {
|
|
69
|
+
return {
|
|
70
|
+
ignorePatterns: DEFAULT_IGNORE,
|
|
71
|
+
maxDepth: 4,
|
|
72
|
+
annotations: DEFAULT_ANNOTATIONS,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const content = parsed.content;
|
|
77
|
+
const ignorePatterns: string[] = [...DEFAULT_IGNORE];
|
|
78
|
+
const annotations: Record<string, string> = { ...DEFAULT_ANNOTATIONS };
|
|
79
|
+
let maxDepth = 4;
|
|
80
|
+
|
|
81
|
+
// Parse ignore patterns section
|
|
82
|
+
const ignoreMatch = content.match(/## Ignore Patterns[\s\S]*?(?=##|$)/);
|
|
83
|
+
if (ignoreMatch) {
|
|
84
|
+
const lines = ignoreMatch[0].split("\n");
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const trimmed = line.trim();
|
|
87
|
+
if (trimmed.startsWith("- ")) {
|
|
88
|
+
ignorePatterns.push(trimmed.slice(2).trim());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse max depth
|
|
94
|
+
const depthMatch = content.match(/## Max Depth\s*\n\s*(\d+)/);
|
|
95
|
+
if (depthMatch) {
|
|
96
|
+
maxDepth = parseInt(depthMatch[1], 10);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Parse annotations
|
|
100
|
+
const annotMatch = content.match(/## Annotations[\s\S]*?(?=##|$)/);
|
|
101
|
+
if (annotMatch) {
|
|
102
|
+
const lines = annotMatch[0].split("\n");
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (trimmed.startsWith("- ")) {
|
|
106
|
+
const [pattern, desc] = trimmed.slice(2).split(":").map((s) => s.trim());
|
|
107
|
+
if (pattern && desc) {
|
|
108
|
+
annotations[pattern] = desc;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { ignorePatterns, maxDepth, annotations };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a path should be ignored
|
|
119
|
+
*/
|
|
120
|
+
function shouldIgnore(name: string, ignorePatterns: string[]): boolean {
|
|
121
|
+
for (const pattern of ignorePatterns) {
|
|
122
|
+
if (pattern.includes("*")) {
|
|
123
|
+
// Simple glob matching
|
|
124
|
+
const regex = new RegExp(
|
|
125
|
+
"^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
126
|
+
);
|
|
127
|
+
if (regex.test(name)) return true;
|
|
128
|
+
} else if (name === pattern) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get annotation for a file
|
|
137
|
+
*/
|
|
138
|
+
function getAnnotation(
|
|
139
|
+
name: string,
|
|
140
|
+
relativePath: string,
|
|
141
|
+
annotations: Record<string, string>
|
|
142
|
+
): string | undefined {
|
|
143
|
+
// Check exact match first
|
|
144
|
+
if (annotations[name]) return annotations[name];
|
|
145
|
+
|
|
146
|
+
// Check path patterns
|
|
147
|
+
for (const [pattern, desc] of Object.entries(annotations)) {
|
|
148
|
+
if (pattern.includes("/") || pattern.includes("*")) {
|
|
149
|
+
const regex = new RegExp(
|
|
150
|
+
"^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
|
|
151
|
+
);
|
|
152
|
+
if (regex.test(relativePath) || regex.test(name)) {
|
|
153
|
+
return desc;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build file tree recursively
|
|
163
|
+
*/
|
|
164
|
+
async function buildTree(
|
|
165
|
+
dir: string,
|
|
166
|
+
rootDir: string,
|
|
167
|
+
config: FileTreeConfig,
|
|
168
|
+
depth: number = 0
|
|
169
|
+
): Promise<TreeNode[]> {
|
|
170
|
+
if (depth >= config.maxDepth) return [];
|
|
171
|
+
|
|
172
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
173
|
+
const nodes: TreeNode[] = [];
|
|
174
|
+
|
|
175
|
+
// Sort: directories first, then files, alphabetically
|
|
176
|
+
const sorted = entries.sort((a, b) => {
|
|
177
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
178
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
179
|
+
return a.name.localeCompare(b.name);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
for (const entry of sorted) {
|
|
183
|
+
if (shouldIgnore(entry.name, config.ignorePatterns)) continue;
|
|
184
|
+
|
|
185
|
+
const fullPath = join(dir, entry.name);
|
|
186
|
+
const relativePath = relative(rootDir, fullPath);
|
|
187
|
+
|
|
188
|
+
if (entry.isDirectory()) {
|
|
189
|
+
const children = await buildTree(fullPath, rootDir, config, depth + 1);
|
|
190
|
+
// Only include directory if it has visible children
|
|
191
|
+
if (children.length > 0) {
|
|
192
|
+
nodes.push({
|
|
193
|
+
name: entry.name,
|
|
194
|
+
path: relativePath,
|
|
195
|
+
type: "directory",
|
|
196
|
+
children,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
nodes.push({
|
|
201
|
+
name: entry.name,
|
|
202
|
+
path: relativePath,
|
|
203
|
+
type: "file",
|
|
204
|
+
annotation: getAnnotation(entry.name, relativePath, config.annotations),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return nodes;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Render tree to string
|
|
214
|
+
*/
|
|
215
|
+
function renderTree(nodes: TreeNode[], prefix: string = ""): string {
|
|
216
|
+
const lines: string[] = [];
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
219
|
+
const node = nodes[i];
|
|
220
|
+
const isLast = i === nodes.length - 1;
|
|
221
|
+
const connector = isLast ? "└── " : "├── ";
|
|
222
|
+
const childPrefix = isLast ? " " : "│ ";
|
|
223
|
+
|
|
224
|
+
let line = prefix + connector + node.name;
|
|
225
|
+
if (node.annotation) {
|
|
226
|
+
line += ` # ${node.annotation}`;
|
|
227
|
+
}
|
|
228
|
+
lines.push(line);
|
|
229
|
+
|
|
230
|
+
if (node.children) {
|
|
231
|
+
lines.push(renderTree(node.children, prefix + childPrefix));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Generate file tree for a project directory
|
|
240
|
+
*/
|
|
241
|
+
export async function generateFileTree(projectDir: string): Promise<string> {
|
|
242
|
+
const config = await loadConfig();
|
|
243
|
+
const nodes = await buildTree(projectDir, projectDir, config);
|
|
244
|
+
const projectName = basename(projectDir);
|
|
245
|
+
|
|
246
|
+
return `${projectName}/\n${renderTree(nodes)}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Generate file tree and return as markdown
|
|
251
|
+
*/
|
|
252
|
+
export async function generateFileTreeMarkdown(projectDir: string): Promise<string> {
|
|
253
|
+
const tree = await generateFileTree(projectDir);
|
|
254
|
+
return `\`\`\`\n${tree}\n\`\`\``;
|
|
255
|
+
}
|