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.
Files changed (51) hide show
  1. package/.claude/settings.local.json +19 -0
  2. package/.think.yaml +30 -0
  3. package/CHANGELOG.md +20 -0
  4. package/CLAUDE.md +111 -0
  5. package/README.md +97 -0
  6. package/bun.lock +144 -0
  7. package/package.json +37 -0
  8. package/src/cli/commands/allow.ts +46 -0
  9. package/src/cli/commands/edit.ts +57 -0
  10. package/src/cli/commands/help.ts +64 -0
  11. package/src/cli/commands/init.ts +76 -0
  12. package/src/cli/commands/learn.ts +50 -0
  13. package/src/cli/commands/profile.ts +30 -0
  14. package/src/cli/commands/project.ts +64 -0
  15. package/src/cli/commands/review.ts +130 -0
  16. package/src/cli/commands/setup.ts +220 -0
  17. package/src/cli/commands/status.ts +66 -0
  18. package/src/cli/commands/sync.ts +30 -0
  19. package/src/cli/commands/tree.ts +30 -0
  20. package/src/cli/index.ts +126 -0
  21. package/src/core/banner.ts +25 -0
  22. package/src/core/config.ts +57 -0
  23. package/src/core/dedup.ts +75 -0
  24. package/src/core/file-tree.ts +255 -0
  25. package/src/core/generator.ts +189 -0
  26. package/src/core/parser.ts +65 -0
  27. package/src/core/project-detect.ts +120 -0
  28. package/src/templates/allowed-commands.md +19 -0
  29. package/src/templates/anti-patterns.md +11 -0
  30. package/src/templates/corrections.md +4 -0
  31. package/src/templates/file-tree.md +28 -0
  32. package/src/templates/learnings.md +4 -0
  33. package/src/templates/patterns.md +10 -0
  34. package/src/templates/pending.md +5 -0
  35. package/src/templates/profile.md +11 -0
  36. package/src/templates/settings.md +6 -0
  37. package/src/templates/subagents.md +15 -0
  38. package/src/templates/tools.md +13 -0
  39. package/src/templates/workflows.md +9 -0
  40. package/src/tui/App.tsx +100 -0
  41. package/src/tui/components/Agents.tsx +117 -0
  42. package/src/tui/components/Automation.tsx +88 -0
  43. package/src/tui/components/Help.tsx +56 -0
  44. package/src/tui/components/Memory.tsx +101 -0
  45. package/src/tui/components/Navigation.tsx +63 -0
  46. package/src/tui/components/Permissions.tsx +88 -0
  47. package/src/tui/components/Preferences.tsx +89 -0
  48. package/src/tui/components/Profile.tsx +62 -0
  49. package/src/tui/components/Skills.tsx +118 -0
  50. package/src/tui/index.tsx +7 -0
  51. package/tsconfig.json +29 -0
@@ -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
+ }