claude-think 0.4.1 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.2] - 2025-02-06
9
+
10
+ ### Added
11
+ - **Smart project detection** - `think project` now detects:
12
+ - Monorepo tools (Turborepo, Nx, Lerna, pnpm/yarn/bun workspaces)
13
+ - Workspace structure with types (apps, packages, services)
14
+ - Frameworks from all workspaces (React, Tauri, Hono, Claude SDK, etc.)
15
+ - Tooling (Biome, Docker, TypeScript, Vite, Prisma, etc.)
16
+ - README tagline/description
17
+ - **Agent/Skill management in TUI**:
18
+ - `n` - create new with auto-generated name (e.g., "swift-falcon")
19
+ - `r` - rename selected item
20
+ - `d` - delete with confirmation
21
+ - Creative name generator for new agents/skills
22
+
23
+ ### Fixed
24
+ - YAML parsing errors no longer crash the TUI (graceful fallback)
25
+ - Long descriptions truncated in agent/skill lists
26
+
27
+ ## [0.4.1] - 2025-02-06
28
+
29
+ ### Added
30
+ - Improved project detection for monorepos
31
+ - Detect runtime (Bun vs Node vs Deno)
32
+ - Detect frameworks from workspace dependencies
33
+
8
34
  ## [0.4.0] - 2025-02-05
9
35
 
10
36
  ### Added
package/README.md CHANGED
@@ -83,7 +83,7 @@ Press `P` in the TUI to switch profiles interactively.
83
83
  | `think edit <file>` | Edit any config file |
84
84
  | `think allow "cmd"` | Pre-approve a command |
85
85
  | `think tree` | Preview project file tree |
86
- | `think project learn` | Generate CLAUDE.md for current project |
86
+ | `think project` | Generate CLAUDE.md for current project |
87
87
  | `think help` | Show all commands |
88
88
 
89
89
  ## TUI
@@ -94,13 +94,14 @@ Run `think` to launch the fullscreen TUI:
94
94
  |-----|--------|
95
95
  | `Tab` | Switch sections |
96
96
  | `↑↓` / `jk` | Navigate / scroll |
97
- | `Enter` | Edit selected item |
98
- | `d` | Delete selected item |
97
+ | `e` | Edit selected item |
98
+ | `n` | New agent/skill |
99
+ | `r` | Rename agent/skill |
100
+ | `d` | Delete item |
99
101
  | `a` | Quick actions (sync, learn, search) |
100
102
  | `p` | Preview CLAUDE.md |
101
103
  | `P` | Switch profile |
102
104
  | `/` | Search all files |
103
- | `e` | Open in $EDITOR |
104
105
  | `?` | Help |
105
106
  | `q` | Quit |
106
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-think",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Personal context manager for Claude - manage your preferences, patterns, and memory",
5
5
  "author": "Amit Feldman",
6
6
  "license": "MIT",
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Generate creative names like Claude Code does
3
+ */
4
+
5
+ const adjectives = [
6
+ "swift", "bold", "calm", "bright", "quick", "sharp", "keen", "wise",
7
+ "agile", "clever", "steady", "nimble", "silent", "vivid", "subtle",
8
+ "curious", "daring", "eager", "gentle", "mighty", "patient", "precise",
9
+ "radiant", "serene", "vigilant", "witty", "zesty", "cosmic", "lunar",
10
+ "stellar", "amber", "azure", "coral", "crimson", "golden", "jade",
11
+ "ruby", "silver", "violet", "rustic", "urban", "ancient", "modern",
12
+ ];
13
+
14
+ const nouns = [
15
+ "falcon", "phoenix", "tiger", "wolf", "hawk", "raven", "fox", "owl",
16
+ "dragon", "lion", "eagle", "bear", "panther", "viper", "cobra", "shark",
17
+ "storm", "thunder", "spark", "flame", "frost", "wave", "wind", "shadow",
18
+ "nova", "comet", "nebula", "quasar", "pulsar", "orbit", "vertex", "prism",
19
+ "cipher", "beacon", "sentinel", "guardian", "pilot", "scout", "ranger",
20
+ "forge", "anvil", "blade", "arrow", "shield", "helm", "crown", "torch",
21
+ ];
22
+
23
+ function randomFrom<T>(arr: T[]): T {
24
+ return arr[Math.floor(Math.random() * arr.length)];
25
+ }
26
+
27
+ /**
28
+ * Generate a unique name like "swift-falcon" or "bold-phoenix"
29
+ */
30
+ export function generateName(): string {
31
+ return `${randomFrom(adjectives)}-${randomFrom(nouns)}`;
32
+ }
33
+
34
+ /**
35
+ * Generate a unique filename, checking for conflicts
36
+ */
37
+ export function generateUniqueFilename(existingNames: string[], extension = ".md"): string {
38
+ let name: string;
39
+ let attempts = 0;
40
+
41
+ do {
42
+ name = generateName();
43
+ attempts++;
44
+ } while (existingNames.includes(name) && attempts < 100);
45
+
46
+ // If we somehow hit 100 collisions, add a number
47
+ if (existingNames.includes(name)) {
48
+ name = `${name}-${Date.now() % 1000}`;
49
+ }
50
+
51
+ return `${name}${extension}`;
52
+ }
@@ -16,14 +16,24 @@ export async function parseMarkdown(filePath: string): Promise<ParsedFile | null
16
16
  return null;
17
17
  }
18
18
 
19
- const raw = await readFile(filePath, "utf-8");
20
- const { data, content } = matter(raw);
19
+ try {
20
+ const raw = await readFile(filePath, "utf-8");
21
+ const { data, content } = matter(raw);
21
22
 
22
- return {
23
- frontmatter: data,
24
- content: content.trim(),
25
- raw,
26
- };
23
+ return {
24
+ frontmatter: data,
25
+ content: content.trim(),
26
+ raw,
27
+ };
28
+ } catch {
29
+ // If YAML parsing fails, return file as content-only
30
+ const raw = await readFile(filePath, "utf-8");
31
+ return {
32
+ frontmatter: {},
33
+ content: raw.trim(),
34
+ raw,
35
+ };
36
+ }
27
37
  }
28
38
 
29
39
  /**
@@ -1,26 +1,33 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
- import { readdir } from "fs/promises";
3
+ import TextInput from "ink-text-input";
4
+ import { readdir, rename, unlink } from "fs/promises";
4
5
  import { existsSync } from "fs";
5
6
  import { thinkPath, CONFIG } from "../../core/config";
6
7
  import { parseMarkdown } from "../../core/parser";
8
+ import { generateUniqueFilename } from "../../core/names";
7
9
  import { spawn } from "child_process";
8
- import { join } from "path";
10
+ import { join, basename } from "path";
9
11
 
10
12
  interface AgentInfo {
11
13
  name: string;
12
14
  description: string;
13
15
  path: string;
16
+ filename: string;
14
17
  }
15
18
 
16
19
  interface AgentsProps {
17
20
  height?: number;
18
21
  }
19
22
 
23
+ type Mode = "list" | "rename" | "confirmDelete";
24
+
20
25
  export function Agents({ height = 15 }: AgentsProps) {
21
26
  const [agents, setAgents] = useState<AgentInfo[]>([]);
22
27
  const [selectedIndex, setSelectedIndex] = useState(0);
23
28
  const [loading, setLoading] = useState(true);
29
+ const [mode, setMode] = useState<Mode>("list");
30
+ const [renameValue, setRenameValue] = useState("");
24
31
 
25
32
  useEffect(() => {
26
33
  loadAgents();
@@ -42,19 +49,84 @@ export function Agents({ height = 15 }: AgentsProps) {
42
49
  const agentInfos: AgentInfo[] = [];
43
50
  for (const file of agentFiles) {
44
51
  const path = join(agentsDir, file);
45
- const parsed = await parseMarkdown(path);
46
- agentInfos.push({
47
- name: (parsed?.frontmatter.name as string) || file.replace(".md", ""),
48
- description: (parsed?.frontmatter.description as string) || "",
49
- path,
50
- });
52
+ try {
53
+ const parsed = await parseMarkdown(path);
54
+ agentInfos.push({
55
+ name: (parsed?.frontmatter?.name as string) || file.replace(".md", ""),
56
+ description: (parsed?.frontmatter?.description as string) || "",
57
+ path,
58
+ filename: file,
59
+ });
60
+ } catch {
61
+ agentInfos.push({
62
+ name: file.replace(".md", ""),
63
+ description: "",
64
+ path,
65
+ filename: file,
66
+ });
67
+ }
51
68
  }
52
69
 
53
70
  setAgents(agentInfos);
54
71
  setLoading(false);
55
72
  }
56
73
 
74
+ async function handleRename() {
75
+ const agent = agents[selectedIndex];
76
+ if (!agent || !renameValue.trim()) return;
77
+
78
+ const newFilename = renameValue.trim().endsWith(".md")
79
+ ? renameValue.trim()
80
+ : `${renameValue.trim()}.md`;
81
+ const newPath = join(thinkPath(CONFIG.dirs.agents), newFilename);
82
+
83
+ try {
84
+ await rename(agent.path, newPath);
85
+ setMode("list");
86
+ setRenameValue("");
87
+ await loadAgents();
88
+ } catch {
89
+ // Failed to rename
90
+ }
91
+ }
92
+
93
+ async function handleDelete() {
94
+ const agent = agents[selectedIndex];
95
+ if (!agent) return;
96
+
97
+ try {
98
+ await unlink(agent.path);
99
+ setMode("list");
100
+ await loadAgents();
101
+ if (selectedIndex >= agents.length - 1) {
102
+ setSelectedIndex(Math.max(0, agents.length - 2));
103
+ }
104
+ } catch {
105
+ // Failed to delete
106
+ }
107
+ }
108
+
57
109
  useInput((input, key) => {
110
+ // Rename mode
111
+ if (mode === "rename") {
112
+ if (key.escape) {
113
+ setMode("list");
114
+ setRenameValue("");
115
+ }
116
+ return;
117
+ }
118
+
119
+ // Confirm delete mode
120
+ if (mode === "confirmDelete") {
121
+ if (input === "y" || input === "Y") {
122
+ handleDelete();
123
+ } else {
124
+ setMode("list");
125
+ }
126
+ return;
127
+ }
128
+
129
+ // List mode
58
130
  if (key.upArrow || input === "k") {
59
131
  setSelectedIndex((i) => Math.max(0, i - 1));
60
132
  }
@@ -71,19 +143,72 @@ export function Agents({ height = 15 }: AgentsProps) {
71
143
  }
72
144
  if (input === "n") {
73
145
  const editor = process.env.EDITOR || "vi";
74
- const newPath = thinkPath(CONFIG.dirs.agents, "new-agent.md");
146
+ const existingNames = agents.map((a) => a.filename.replace(".md", ""));
147
+ const newFilename = generateUniqueFilename(existingNames);
148
+ const newPath = thinkPath(CONFIG.dirs.agents, newFilename);
75
149
  spawn(editor, [newPath], {
76
150
  stdio: "inherit",
77
151
  }).on("exit", () => {
78
152
  loadAgents();
79
153
  });
80
154
  }
155
+ if (input === "r" && agents[selectedIndex]) {
156
+ setRenameValue(agents[selectedIndex].filename.replace(".md", ""));
157
+ setMode("rename");
158
+ }
159
+ if (input === "d" && agents[selectedIndex]) {
160
+ setMode("confirmDelete");
161
+ }
81
162
  });
82
163
 
83
164
  if (loading) {
84
165
  return <Text color="gray">Loading...</Text>;
85
166
  }
86
167
 
168
+ // Rename mode
169
+ if (mode === "rename") {
170
+ return (
171
+ <Box flexDirection="column">
172
+ <Box marginBottom={1}>
173
+ <Text bold color="green">Rename Agent</Text>
174
+ </Box>
175
+ <Box>
176
+ <Text color="cyan">New name: </Text>
177
+ <TextInput
178
+ value={renameValue}
179
+ onChange={setRenameValue}
180
+ onSubmit={handleRename}
181
+ placeholder="agent-name"
182
+ />
183
+ </Box>
184
+ <Box marginTop={1}>
185
+ <Text color="gray">Enter: save | Esc: cancel</Text>
186
+ </Box>
187
+ </Box>
188
+ );
189
+ }
190
+
191
+ // Confirm delete mode
192
+ if (mode === "confirmDelete") {
193
+ const agent = agents[selectedIndex];
194
+ return (
195
+ <Box flexDirection="column">
196
+ <Box marginBottom={1}>
197
+ <Text bold color="red">Delete Agent</Text>
198
+ </Box>
199
+ <Box>
200
+ <Text>Delete </Text>
201
+ <Text color="cyan" bold>"{agent?.name}"</Text>
202
+ <Text>?</Text>
203
+ </Box>
204
+ <Box marginTop={1}>
205
+ <Text color="red" bold>y</Text>
206
+ <Text color="gray">: delete | any key: cancel</Text>
207
+ </Box>
208
+ </Box>
209
+ );
210
+ }
211
+
87
212
  return (
88
213
  <Box flexDirection="column">
89
214
  <Box marginBottom={1}>
@@ -106,7 +231,7 @@ export function Agents({ height = 15 }: AgentsProps) {
106
231
  {agent.name}
107
232
  </Text>
108
233
  {agent.description && (
109
- <Text color="gray"> - {agent.description}</Text>
234
+ <Text color="gray"> - {agent.description.slice(0, 50)}{agent.description.length > 50 ? "..." : ""}</Text>
110
235
  )}
111
236
  </Box>
112
237
  ))}
@@ -114,7 +239,7 @@ export function Agents({ height = 15 }: AgentsProps) {
114
239
  )}
115
240
 
116
241
  <Box marginTop={1}>
117
- <Text color="gray">↑/↓: select | e: edit | n: new</Text>
242
+ <Text color="gray">↑↓: select | e: edit | n: new | r: rename | d: delete</Text>
118
243
  </Box>
119
244
  </Box>
120
245
  );
@@ -1,9 +1,11 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
- import { readdir } from "fs/promises";
3
+ import TextInput from "ink-text-input";
4
+ import { readdir, rename, unlink } from "fs/promises";
4
5
  import { existsSync } from "fs";
5
6
  import { thinkPath, CONFIG } from "../../core/config";
6
7
  import { parseMarkdown } from "../../core/parser";
8
+ import { generateUniqueFilename } from "../../core/names";
7
9
  import { spawn } from "child_process";
8
10
  import { join } from "path";
9
11
 
@@ -11,16 +13,21 @@ interface SkillInfo {
11
13
  name: string;
12
14
  description: string;
13
15
  path: string;
16
+ filename: string;
14
17
  }
15
18
 
16
19
  interface SkillsProps {
17
20
  height?: number;
18
21
  }
19
22
 
23
+ type Mode = "list" | "rename" | "confirmDelete";
24
+
20
25
  export function Skills({ height = 15 }: SkillsProps) {
21
26
  const [skills, setSkills] = useState<SkillInfo[]>([]);
22
27
  const [selectedIndex, setSelectedIndex] = useState(0);
23
28
  const [loading, setLoading] = useState(true);
29
+ const [mode, setMode] = useState<Mode>("list");
30
+ const [renameValue, setRenameValue] = useState("");
24
31
 
25
32
  useEffect(() => {
26
33
  loadSkills();
@@ -42,19 +49,84 @@ export function Skills({ height = 15 }: SkillsProps) {
42
49
  const skillInfos: SkillInfo[] = [];
43
50
  for (const file of skillFiles) {
44
51
  const path = join(skillsDir, file);
45
- const parsed = await parseMarkdown(path);
46
- skillInfos.push({
47
- name: (parsed?.frontmatter.name as string) || file.replace(".md", ""),
48
- description: (parsed?.frontmatter.description as string) || "",
49
- path,
50
- });
52
+ try {
53
+ const parsed = await parseMarkdown(path);
54
+ skillInfos.push({
55
+ name: (parsed?.frontmatter?.name as string) || file.replace(".md", ""),
56
+ description: (parsed?.frontmatter?.description as string) || "",
57
+ path,
58
+ filename: file,
59
+ });
60
+ } catch {
61
+ skillInfos.push({
62
+ name: file.replace(".md", ""),
63
+ description: "",
64
+ path,
65
+ filename: file,
66
+ });
67
+ }
51
68
  }
52
69
 
53
70
  setSkills(skillInfos);
54
71
  setLoading(false);
55
72
  }
56
73
 
74
+ async function handleRename() {
75
+ const skill = skills[selectedIndex];
76
+ if (!skill || !renameValue.trim()) return;
77
+
78
+ const newFilename = renameValue.trim().endsWith(".md")
79
+ ? renameValue.trim()
80
+ : `${renameValue.trim()}.md`;
81
+ const newPath = join(thinkPath(CONFIG.dirs.skills), newFilename);
82
+
83
+ try {
84
+ await rename(skill.path, newPath);
85
+ setMode("list");
86
+ setRenameValue("");
87
+ await loadSkills();
88
+ } catch {
89
+ // Failed to rename
90
+ }
91
+ }
92
+
93
+ async function handleDelete() {
94
+ const skill = skills[selectedIndex];
95
+ if (!skill) return;
96
+
97
+ try {
98
+ await unlink(skill.path);
99
+ setMode("list");
100
+ await loadSkills();
101
+ if (selectedIndex >= skills.length - 1) {
102
+ setSelectedIndex(Math.max(0, skills.length - 2));
103
+ }
104
+ } catch {
105
+ // Failed to delete
106
+ }
107
+ }
108
+
57
109
  useInput((input, key) => {
110
+ // Rename mode
111
+ if (mode === "rename") {
112
+ if (key.escape) {
113
+ setMode("list");
114
+ setRenameValue("");
115
+ }
116
+ return;
117
+ }
118
+
119
+ // Confirm delete mode
120
+ if (mode === "confirmDelete") {
121
+ if (input === "y" || input === "Y") {
122
+ handleDelete();
123
+ } else {
124
+ setMode("list");
125
+ }
126
+ return;
127
+ }
128
+
129
+ // List mode
58
130
  if (key.upArrow || input === "k") {
59
131
  setSelectedIndex((i) => Math.max(0, i - 1));
60
132
  }
@@ -70,21 +142,73 @@ export function Skills({ height = 15 }: SkillsProps) {
70
142
  });
71
143
  }
72
144
  if (input === "n") {
73
- // Create new skill
74
145
  const editor = process.env.EDITOR || "vi";
75
- const newPath = thinkPath(CONFIG.dirs.skills, "new-skill.md");
146
+ const existingNames = skills.map((s) => s.filename.replace(".md", ""));
147
+ const newFilename = generateUniqueFilename(existingNames);
148
+ const newPath = thinkPath(CONFIG.dirs.skills, newFilename);
76
149
  spawn(editor, [newPath], {
77
150
  stdio: "inherit",
78
151
  }).on("exit", () => {
79
152
  loadSkills();
80
153
  });
81
154
  }
155
+ if (input === "r" && skills[selectedIndex]) {
156
+ setRenameValue(skills[selectedIndex].filename.replace(".md", ""));
157
+ setMode("rename");
158
+ }
159
+ if (input === "d" && skills[selectedIndex]) {
160
+ setMode("confirmDelete");
161
+ }
82
162
  });
83
163
 
84
164
  if (loading) {
85
165
  return <Text color="gray">Loading...</Text>;
86
166
  }
87
167
 
168
+ // Rename mode
169
+ if (mode === "rename") {
170
+ return (
171
+ <Box flexDirection="column">
172
+ <Box marginBottom={1}>
173
+ <Text bold color="green">Rename Skill</Text>
174
+ </Box>
175
+ <Box>
176
+ <Text color="cyan">New name: </Text>
177
+ <TextInput
178
+ value={renameValue}
179
+ onChange={setRenameValue}
180
+ onSubmit={handleRename}
181
+ placeholder="skill-name"
182
+ />
183
+ </Box>
184
+ <Box marginTop={1}>
185
+ <Text color="gray">Enter: save | Esc: cancel</Text>
186
+ </Box>
187
+ </Box>
188
+ );
189
+ }
190
+
191
+ // Confirm delete mode
192
+ if (mode === "confirmDelete") {
193
+ const skill = skills[selectedIndex];
194
+ return (
195
+ <Box flexDirection="column">
196
+ <Box marginBottom={1}>
197
+ <Text bold color="red">Delete Skill</Text>
198
+ </Box>
199
+ <Box>
200
+ <Text>Delete </Text>
201
+ <Text color="cyan" bold>"{skill?.name}"</Text>
202
+ <Text>?</Text>
203
+ </Box>
204
+ <Box marginTop={1}>
205
+ <Text color="red" bold>y</Text>
206
+ <Text color="gray">: delete | any key: cancel</Text>
207
+ </Box>
208
+ </Box>
209
+ );
210
+ }
211
+
88
212
  return (
89
213
  <Box flexDirection="column">
90
214
  <Box marginBottom={1}>
@@ -107,7 +231,7 @@ export function Skills({ height = 15 }: SkillsProps) {
107
231
  {skill.name}
108
232
  </Text>
109
233
  {skill.description && (
110
- <Text color="gray"> - {skill.description}</Text>
234
+ <Text color="gray"> - {skill.description.slice(0, 50)}{skill.description.length > 50 ? "..." : ""}</Text>
111
235
  )}
112
236
  </Box>
113
237
  ))}
@@ -115,7 +239,7 @@ export function Skills({ height = 15 }: SkillsProps) {
115
239
  )}
116
240
 
117
241
  <Box marginTop={1}>
118
- <Text color="gray">↑/↓: select | e: edit | n: new</Text>
242
+ <Text color="gray">↑↓: select | e: edit | n: new | r: rename | d: delete</Text>
119
243
  </Box>
120
244
  </Box>
121
245
  );