claude-think 0.2.1 → 0.3.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.
@@ -29,7 +29,8 @@
29
29
  "Bash(xargs:*)",
30
30
  "WebSearch",
31
31
  "Bash(__NEW_LINE_db734bce153d7d1e__ echo \"\")",
32
- "Bash(git checkout:*)"
32
+ "Bash(git checkout:*)",
33
+ "Bash(npm i:*)"
33
34
  ]
34
35
  }
35
36
  }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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.3.0] - 2025-02-05
9
+
10
+ ### Added
11
+ - Enhanced TUI with new features:
12
+ - **Status bar** - shows learnings count, pending count, last sync time
13
+ - **Quick actions menu** (press `a`) - sync, add learning, search
14
+ - **Preview panel** (press `p`) - view generated CLAUDE.md with scrolling
15
+ - **Search** (press `/`) - search across all config files
16
+ - **Better help screen** (press `?`) - organized shortcuts and CLI commands
17
+ - New keyboard shortcuts: `a` actions, `p` preview, `/` search, `Ctrl+S` sync
18
+ - Bordered content panels for cleaner visual design
19
+
20
+ ### Changed
21
+ - TUI now uses modals for actions, preview, search, and help
22
+ - Improved navigation hints in footer
23
+
8
24
  ## [0.2.1] - 2025-02-05
9
25
 
10
26
  ### Added
package/README.md CHANGED
@@ -25,7 +25,7 @@ Personal context manager for Claude. Stop repeating yourself.
25
25
 
26
26
  ```bash
27
27
  # With bun (recommended)
28
- bun install -g claude-think
28
+ bun add -g claude-think
29
29
 
30
30
  # With npm
31
31
  npm install -g claude-think
@@ -47,8 +47,8 @@ claude
47
47
  ## How it works
48
48
 
49
49
  1. Your preferences live in `~/.think/` (markdown files)
50
- 2. `think sync` generates a Claude plugin at `~/.claude/plugins/think/`
51
- 3. Claude Code auto-loads the plugin, so your context is always there
50
+ 2. `think sync` generates `~/.claude/CLAUDE.md`
51
+ 3. Claude Code auto-loads CLAUDE.md at session start
52
52
 
53
53
  ## Commands
54
54
 
@@ -65,6 +65,7 @@ claude
65
65
  | `think edit <file>` | Edit any config file |
66
66
  | `think allow "cmd"` | Pre-approve a command |
67
67
  | `think tree` | Preview project file tree |
68
+ | `think project learn` | Generate CLAUDE.md for current project |
68
69
  | `think help` | Show all commands |
69
70
 
70
71
  ## What you can configure
package/bun.lock CHANGED
@@ -10,6 +10,7 @@
10
10
  "commander": "^14.0.3",
11
11
  "gray-matter": "^4.0.3",
12
12
  "ink": "^6.6.0",
13
+ "ink-text-input": "^6.0.0",
13
14
  "react": "^19.2.4",
14
15
  "string-similarity": "^4.0.4",
15
16
  },
@@ -86,6 +87,8 @@
86
87
 
87
88
  "ink": ["ink@6.6.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ=="],
88
89
 
90
+ "ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
91
+
89
92
  "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
90
93
 
91
94
  "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-think",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Personal context manager for Claude - manage your preferences, patterns, and memory",
5
5
  "author": "Amit Feldman",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "commander": "^14.0.3",
33
33
  "gray-matter": "^4.0.3",
34
34
  "ink": "^6.6.0",
35
+ "ink-text-input": "^6.0.0",
35
36
  "react": "^19.2.4",
36
37
  "string-similarity": "^4.0.4"
37
38
  }
package/src/tui/App.tsx CHANGED
@@ -9,9 +9,14 @@ import { Skills } from "./components/Skills";
9
9
  import { Agents } from "./components/Agents";
10
10
  import { Automation } from "./components/Automation";
11
11
  import { Help } from "./components/Help";
12
+ import { StatusBar } from "./components/StatusBar";
13
+ import { QuickActions } from "./components/QuickActions";
14
+ import { Preview } from "./components/Preview";
15
+ import { Search } from "./components/Search";
12
16
  import { existsSync } from "fs";
13
17
  import { CONFIG } from "../core/config";
14
18
 
19
+ // Note: "help" is handled as a modal, not a navigation section
15
20
  type Section =
16
21
  | "profile"
17
22
  | "preferences"
@@ -19,13 +24,15 @@ type Section =
19
24
  | "permissions"
20
25
  | "skills"
21
26
  | "agents"
22
- | "automation"
23
- | "help";
27
+ | "automation";
28
+
29
+ type Modal = "none" | "help" | "actions" | "preview" | "search";
24
30
 
25
31
  export function App() {
26
32
  const { exit } = useApp();
27
33
  const [section, setSection] = useState<Section>("profile");
28
- const [showHelp, setShowHelp] = useState(false);
34
+ const [modal, setModal] = useState<Modal>("none");
35
+ const [statusMessage, setStatusMessage] = useState<string | undefined>();
29
36
  const [initialized, setInitialized] = useState(false);
30
37
 
31
38
  useEffect(() => {
@@ -33,25 +40,88 @@ export function App() {
33
40
  }, []);
34
41
 
35
42
  useInput((input, key) => {
43
+ // Global shortcuts (when no modal open)
44
+ if (modal !== "none") return;
45
+
36
46
  if (input === "q" || (key.ctrl && input === "c")) {
37
47
  exit();
38
48
  }
39
49
  if (input === "?") {
40
- setShowHelp(!showHelp);
50
+ setModal("help");
51
+ }
52
+ if (key.ctrl && input === "s") {
53
+ setModal("actions");
54
+ }
55
+ if (input === "a") {
56
+ setModal("actions");
57
+ }
58
+ if (input === "p") {
59
+ setModal("preview");
60
+ }
61
+ if (input === "/" || (key.ctrl && input === "f")) {
62
+ setModal("search");
41
63
  }
42
64
  });
43
65
 
66
+ function handleMessage(msg: string) {
67
+ setStatusMessage(msg);
68
+ setTimeout(() => setStatusMessage(undefined), 3000);
69
+ }
70
+
44
71
  if (!initialized) {
45
72
  return (
46
73
  <Box flexDirection="column" padding={1}>
47
- <Text color="red">~/.think not found.</Text>
48
- <Text>Run `think init` first, then launch the TUI.</Text>
74
+ <Box
75
+ borderStyle="round"
76
+ borderColor="red"
77
+ paddingX={2}
78
+ paddingY={1}
79
+ flexDirection="column"
80
+ >
81
+ <Text color="red" bold>
82
+ ~/.think not found
83
+ </Text>
84
+ <Box marginTop={1}>
85
+ <Text color="gray">Run `think init` first, then launch the TUI.</Text>
86
+ </Box>
87
+ </Box>
88
+ </Box>
89
+ );
90
+ }
91
+
92
+ // Render modals
93
+ if (modal === "help") {
94
+ return <Help onClose={() => setModal("none")} />;
95
+ }
96
+
97
+ if (modal === "actions") {
98
+ return (
99
+ <Box flexDirection="column" padding={1}>
100
+ <Header />
101
+ <QuickActions
102
+ onMessage={handleMessage}
103
+ onClose={() => setModal("none")}
104
+ />
105
+ </Box>
106
+ );
107
+ }
108
+
109
+ if (modal === "preview") {
110
+ return (
111
+ <Box flexDirection="column" padding={1}>
112
+ <Header />
113
+ <Preview onClose={() => setModal("none")} />
49
114
  </Box>
50
115
  );
51
116
  }
52
117
 
53
- if (showHelp) {
54
- return <Help onClose={() => setShowHelp(false)} />;
118
+ if (modal === "search") {
119
+ return (
120
+ <Box flexDirection="column" padding={1}>
121
+ <Header />
122
+ <Search onClose={() => setModal("none")} />
123
+ </Box>
124
+ );
55
125
  }
56
126
 
57
127
  const renderSection = () => {
@@ -77,24 +147,41 @@ export function App() {
77
147
 
78
148
  return (
79
149
  <Box flexDirection="column">
80
- <Box marginBottom={1}>
81
- <Text color="green" bold>
82
- ▀█▀ █░█ █ █▄░█ █▄▀
83
- </Text>
84
- <Text color="gray"> Personal Context for Claude</Text>
85
- </Box>
150
+ <Header />
86
151
 
87
152
  <Navigation currentSection={section} onSectionChange={setSection} />
88
153
 
89
- <Box flexDirection="column" marginTop={1}>
154
+ <Box
155
+ flexDirection="column"
156
+ marginTop={1}
157
+ borderStyle="single"
158
+ borderColor="gray"
159
+ padding={1}
160
+ minHeight={15}
161
+ >
90
162
  {renderSection()}
91
163
  </Box>
92
164
 
165
+ <Box marginTop={1}>
166
+ <StatusBar message={statusMessage} />
167
+ </Box>
168
+
93
169
  <Box marginTop={1}>
94
170
  <Text color="gray">
95
- Tab: switch sections | ?: help | q: quit
171
+ Tab: sections | a: actions | p: preview | /: search | ?: help | q: quit
96
172
  </Text>
97
173
  </Box>
98
174
  </Box>
99
175
  );
100
176
  }
177
+
178
+ function Header() {
179
+ return (
180
+ <Box marginBottom={1}>
181
+ <Text color="green" bold>
182
+ ▀█▀ █░█ █ █▄░█ █▄▀
183
+ </Text>
184
+ <Text color="gray"> Personal Context for Claude</Text>
185
+ </Box>
186
+ );
187
+ }
@@ -16,40 +16,70 @@ export function Help({ onClose }: HelpProps) {
16
16
  <Box flexDirection="column" padding={1}>
17
17
  <Box marginBottom={1}>
18
18
  <Text bold color="green">
19
- Keyboard Shortcuts
19
+ ▀█▀ █░█ █ █▄░█ █▄▀
20
20
  </Text>
21
+ <Text color="gray"> Help</Text>
21
22
  </Box>
22
23
 
23
- <Box flexDirection="column" paddingLeft={1}>
24
- <Text key="nav-header" bold color="cyan">Navigation</Text>
25
- <Text key="nav-1"> Tab / Shift+Tab Switch sections</Text>
26
- <Text key="nav-2"> 1-7 Jump to section</Text>
27
- <Text key="nav-3"> ← / → Switch sub-sections</Text>
28
- <Text key="nav-4"> ↑ / ↓ Select item in list</Text>
29
- <Text key="nav-spacer"> </Text>
30
-
31
- <Text key="act-header" bold color="cyan">Actions</Text>
32
- <Text key="act-1"> e Edit in $EDITOR</Text>
33
- <Text key="act-2"> n Create new (skills/agents)</Text>
34
- <Text key="act-3"> Enter Select / Open</Text>
35
- <Text key="act-spacer"> </Text>
36
-
37
- <Text key="gen-header" bold color="cyan">General</Text>
38
- <Text key="gen-1"> ? Toggle help</Text>
39
- <Text key="gen-2"> q / Ctrl+C Quit</Text>
40
- <Text key="gen-spacer"> </Text>
41
-
42
- <Text key="cli-header" bold color="cyan">CLI Commands</Text>
43
- <Text key="cli-1"> think init Initialize ~/.think</Text>
44
- <Text key="cli-2"> think sync Sync to Claude plugin</Text>
45
- <Text key="cli-3"> think status Show status</Text>
46
- <Text key="cli-4"> think learn Add a learning</Text>
47
- <Text key="cli-5"> think review Review pending learnings</Text>
48
- <Text key="cli-6"> think allow Add allowed command</Text>
24
+ <Box
25
+ flexDirection="column"
26
+ borderStyle="single"
27
+ borderColor="gray"
28
+ padding={1}
29
+ >
30
+ <Box flexDirection="row">
31
+ <Box flexDirection="column" marginRight={4}>
32
+ <Text bold color="cyan">Navigation</Text>
33
+ <Text><Text color="green">Tab</Text> Switch sections</Text>
34
+ <Text><Text color="green">1-7</Text> Jump to section</Text>
35
+ <Text><Text color="green">←/→</Text> Sub-sections</Text>
36
+ <Text><Text color="green">↑/↓</Text> Select item</Text>
37
+ </Box>
38
+
39
+ <Box flexDirection="column" marginRight={4}>
40
+ <Text bold color="cyan">Quick Actions</Text>
41
+ <Text><Text color="green">a</Text> Actions menu</Text>
42
+ <Text><Text color="green">p</Text> Preview CLAUDE.md</Text>
43
+ <Text><Text color="green">/</Text> Search</Text>
44
+ <Text><Text color="green">Ctrl+S</Text> Sync</Text>
45
+ </Box>
46
+
47
+ <Box flexDirection="column">
48
+ <Text bold color="cyan">Editing</Text>
49
+ <Text><Text color="green">e</Text> Edit in $EDITOR</Text>
50
+ <Text><Text color="green">n</Text> Create new item</Text>
51
+ <Text><Text color="green">Enter</Text> Select/Open</Text>
52
+ <Text><Text color="green">Esc</Text> Close modal</Text>
53
+ </Box>
54
+ </Box>
55
+
56
+ <Box marginTop={1} flexDirection="column">
57
+ <Text bold color="cyan">CLI Commands</Text>
58
+ <Box flexDirection="row">
59
+ <Box flexDirection="column" marginRight={2}>
60
+ <Text color="gray">think init</Text>
61
+ <Text color="gray">think setup</Text>
62
+ <Text color="gray">think sync</Text>
63
+ <Text color="gray">think status</Text>
64
+ </Box>
65
+ <Box flexDirection="column" marginRight={2}>
66
+ <Text color="gray">think learn "..."</Text>
67
+ <Text color="gray">think review</Text>
68
+ <Text color="gray">think profile</Text>
69
+ <Text color="gray">think edit &lt;file&gt;</Text>
70
+ </Box>
71
+ <Box flexDirection="column">
72
+ <Text color="gray">think allow "cmd"</Text>
73
+ <Text color="gray">think tree</Text>
74
+ <Text color="gray">think project learn</Text>
75
+ <Text color="gray">think help</Text>
76
+ </Box>
77
+ </Box>
78
+ </Box>
49
79
  </Box>
50
80
 
51
81
  <Box marginTop={1}>
52
- <Text color="gray">Press ? or Escape to close</Text>
82
+ <Text color="gray">Press ? or Esc to close</Text>
53
83
  </Box>
54
84
  </Box>
55
85
  );
@@ -0,0 +1,86 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { readFile } from "fs/promises";
4
+ import { existsSync } from "fs";
5
+ import { CONFIG } from "../../core/config";
6
+
7
+ interface PreviewProps {
8
+ onClose: () => void;
9
+ }
10
+
11
+ export function Preview({ onClose }: PreviewProps) {
12
+ const [content, setContent] = useState<string[]>([]);
13
+ const [scroll, setScroll] = useState(0);
14
+ const [loading, setLoading] = useState(true);
15
+ const maxLines = 20;
16
+
17
+ useEffect(() => {
18
+ loadPreview();
19
+ }, []);
20
+
21
+ async function loadPreview() {
22
+ if (existsSync(CONFIG.claudeMdPath)) {
23
+ const text = await readFile(CONFIG.claudeMdPath, "utf-8");
24
+ setContent(text.split("\n"));
25
+ }
26
+ setLoading(false);
27
+ }
28
+
29
+ useInput((input, key) => {
30
+ if (key.escape || input === "q") {
31
+ onClose();
32
+ }
33
+ if (key.upArrow || input === "k") {
34
+ setScroll((s) => Math.max(0, s - 1));
35
+ }
36
+ if (key.downArrow || input === "j") {
37
+ setScroll((s) => Math.min(content.length - maxLines, s + 1));
38
+ }
39
+ if (key.pageUp) {
40
+ setScroll((s) => Math.max(0, s - maxLines));
41
+ }
42
+ if (key.pageDown) {
43
+ setScroll((s) => Math.min(content.length - maxLines, s + maxLines));
44
+ }
45
+ });
46
+
47
+ if (loading) {
48
+ return <Text color="gray">Loading preview...</Text>;
49
+ }
50
+
51
+ const visibleLines = content.slice(scroll, scroll + maxLines);
52
+ const scrollPercent = content.length > maxLines
53
+ ? Math.round((scroll / (content.length - maxLines)) * 100)
54
+ : 100;
55
+
56
+ return (
57
+ <Box flexDirection="column" borderStyle="single" borderColor="cyan" padding={1}>
58
+ <Box marginBottom={1}>
59
+ <Text bold color="cyan">CLAUDE.md Preview</Text>
60
+ <Text color="gray"> ({content.length} lines)</Text>
61
+ <Box flexGrow={1} />
62
+ <Text color="gray">{scrollPercent}%</Text>
63
+ </Box>
64
+
65
+ <Box flexDirection="column" height={maxLines}>
66
+ {visibleLines.map((line, i) => {
67
+ let color: string | undefined;
68
+ if (line.startsWith("# ")) color = "green";
69
+ else if (line.startsWith("## ")) color = "yellow";
70
+ else if (line.startsWith("- ")) color = "white";
71
+ else if (line.startsWith("**")) color = "cyan";
72
+
73
+ return (
74
+ <Text key={scroll + i} color={color} dimColor={!color}>
75
+ {line || " "}
76
+ </Text>
77
+ );
78
+ })}
79
+ </Box>
80
+
81
+ <Box marginTop={1}>
82
+ <Text color="gray">↑↓/jk: scroll | PgUp/PgDn: page | Esc: close</Text>
83
+ </Box>
84
+ </Box>
85
+ );
86
+ }
@@ -0,0 +1,155 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import { generatePlugin } from "../../core/generator";
5
+ import { addLearning } from "../../core/dedup";
6
+ import { readFile, writeFile } from "fs/promises";
7
+ import { existsSync } from "fs";
8
+ import { thinkPath, CONFIG } from "../../core/config";
9
+
10
+ interface QuickActionsProps {
11
+ onMessage: (msg: string) => void;
12
+ onClose: () => void;
13
+ }
14
+
15
+ type Action = "menu" | "sync" | "learn" | "search";
16
+
17
+ export function QuickActions({ onMessage, onClose }: QuickActionsProps) {
18
+ const [action, setAction] = useState<Action>("menu");
19
+ const [input, setInput] = useState("");
20
+ const [selected, setSelected] = useState(0);
21
+
22
+ const menuItems = [
23
+ { key: "s", label: "Sync", desc: "Regenerate CLAUDE.md" },
24
+ { key: "l", label: "Learn", desc: "Add a new learning" },
25
+ { key: "f", label: "Search", desc: "Search all files" },
26
+ ];
27
+
28
+ useInput((key, mod) => {
29
+ if (mod.escape || key === "q") {
30
+ if (action === "menu") {
31
+ onClose();
32
+ } else {
33
+ setAction("menu");
34
+ setInput("");
35
+ }
36
+ return;
37
+ }
38
+
39
+ if (action === "menu") {
40
+ if (mod.upArrow || key === "k") {
41
+ setSelected((s) => (s - 1 + menuItems.length) % menuItems.length);
42
+ }
43
+ if (mod.downArrow || key === "j") {
44
+ setSelected((s) => (s + 1) % menuItems.length);
45
+ }
46
+ if (mod.return) {
47
+ const item = menuItems[selected]!;
48
+ if (item.key === "s") handleSync();
49
+ else if (item.key === "l") setAction("learn");
50
+ else if (item.key === "f") setAction("search");
51
+ }
52
+ // Quick keys
53
+ if (key === "s") handleSync();
54
+ if (key === "l") setAction("learn");
55
+ if (key === "f") setAction("search");
56
+ }
57
+ });
58
+
59
+ async function handleSync() {
60
+ onMessage("Syncing...");
61
+ try {
62
+ await generatePlugin();
63
+ onMessage("Synced successfully!");
64
+ } catch (e) {
65
+ onMessage("Sync failed!");
66
+ }
67
+ setTimeout(onClose, 1500);
68
+ }
69
+
70
+ async function handleLearn() {
71
+ if (!input.trim()) return;
72
+
73
+ const learningsPath = thinkPath(CONFIG.files.learnings);
74
+ let content = "";
75
+ if (existsSync(learningsPath)) {
76
+ content = await readFile(learningsPath, "utf-8");
77
+ }
78
+
79
+ const result = addLearning(content, input.trim());
80
+ if (result.added) {
81
+ await writeFile(learningsPath, result.newContent);
82
+ onMessage(`Added: "${input.trim()}"`);
83
+ await generatePlugin();
84
+ } else {
85
+ onMessage(`Similar exists: "${result.similar}"`);
86
+ }
87
+ setInput("");
88
+ setTimeout(onClose, 1500);
89
+ }
90
+
91
+ if (action === "menu") {
92
+ return (
93
+ <Box flexDirection="column" borderStyle="round" borderColor="green" padding={1}>
94
+ <Text bold color="green">Quick Actions</Text>
95
+ <Box flexDirection="column" marginTop={1}>
96
+ {menuItems.map((item, i) => (
97
+ <Box key={item.key}>
98
+ <Text color={i === selected ? "green" : "gray"}>
99
+ {i === selected ? "▸ " : " "}
100
+ </Text>
101
+ <Text color={i === selected ? "white" : "gray"} bold={i === selected}>
102
+ [{item.key}] {item.label}
103
+ </Text>
104
+ <Text color="gray"> - {item.desc}</Text>
105
+ </Box>
106
+ ))}
107
+ </Box>
108
+ <Box marginTop={1}>
109
+ <Text color="gray">↑↓: navigate | Enter: select | Esc: close</Text>
110
+ </Box>
111
+ </Box>
112
+ );
113
+ }
114
+
115
+ if (action === "learn") {
116
+ return (
117
+ <Box flexDirection="column" borderStyle="round" borderColor="yellow" padding={1}>
118
+ <Text bold color="yellow">Add Learning</Text>
119
+ <Box marginTop={1}>
120
+ <Text color="gray">▸ </Text>
121
+ <TextInput
122
+ value={input}
123
+ onChange={setInput}
124
+ onSubmit={handleLearn}
125
+ placeholder="Type your learning..."
126
+ />
127
+ </Box>
128
+ <Box marginTop={1}>
129
+ <Text color="gray">Enter: add | Esc: cancel</Text>
130
+ </Box>
131
+ </Box>
132
+ );
133
+ }
134
+
135
+ if (action === "search") {
136
+ return (
137
+ <Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
138
+ <Text bold color="cyan">Search</Text>
139
+ <Box marginTop={1}>
140
+ <Text color="gray">▸ </Text>
141
+ <TextInput
142
+ value={input}
143
+ onChange={setInput}
144
+ placeholder="Search term..."
145
+ />
146
+ </Box>
147
+ <Box marginTop={1}>
148
+ <Text color="gray">Enter: search | Esc: cancel</Text>
149
+ </Box>
150
+ </Box>
151
+ );
152
+ }
153
+
154
+ return null;
155
+ }
@@ -0,0 +1,129 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import { readFile } from "fs/promises";
5
+ import { existsSync } from "fs";
6
+ import { thinkPath, CONFIG } from "../../core/config";
7
+
8
+ interface SearchProps {
9
+ onClose: () => void;
10
+ }
11
+
12
+ interface SearchResult {
13
+ file: string;
14
+ line: number;
15
+ content: string;
16
+ }
17
+
18
+ const searchFiles = [
19
+ { name: "profile", path: CONFIG.files.profile },
20
+ { name: "tools", path: CONFIG.files.tools },
21
+ { name: "patterns", path: CONFIG.files.patterns },
22
+ { name: "anti-patterns", path: CONFIG.files.antiPatterns },
23
+ { name: "learnings", path: CONFIG.files.learnings },
24
+ { name: "corrections", path: CONFIG.files.corrections },
25
+ { name: "subagents", path: CONFIG.files.subagents },
26
+ { name: "workflows", path: CONFIG.files.workflows },
27
+ ];
28
+
29
+ export function Search({ onClose }: SearchProps) {
30
+ const [query, setQuery] = useState("");
31
+ const [results, setResults] = useState<SearchResult[]>([]);
32
+ const [selected, setSelected] = useState(0);
33
+ const [searching, setSearching] = useState(false);
34
+
35
+ useInput((input, key) => {
36
+ if (key.escape) {
37
+ onClose();
38
+ }
39
+ if (key.upArrow || (key.ctrl && input === "p")) {
40
+ setSelected((s) => Math.max(0, s - 1));
41
+ }
42
+ if (key.downArrow || (key.ctrl && input === "n")) {
43
+ setSelected((s) => Math.min(results.length - 1, s + 1));
44
+ }
45
+ });
46
+
47
+ async function handleSearch(q: string) {
48
+ setQuery(q);
49
+ if (q.length < 2) {
50
+ setResults([]);
51
+ return;
52
+ }
53
+
54
+ setSearching(true);
55
+ const found: SearchResult[] = [];
56
+ const lowerQuery = q.toLowerCase();
57
+
58
+ for (const file of searchFiles) {
59
+ const fullPath = thinkPath(file.path);
60
+ if (!existsSync(fullPath)) continue;
61
+
62
+ const content = await readFile(fullPath, "utf-8");
63
+ const lines = content.split("\n");
64
+
65
+ lines.forEach((line, i) => {
66
+ if (line.toLowerCase().includes(lowerQuery)) {
67
+ found.push({
68
+ file: file.name,
69
+ line: i + 1,
70
+ content: line.trim(),
71
+ });
72
+ }
73
+ });
74
+ }
75
+
76
+ setResults(found.slice(0, 15));
77
+ setSelected(0);
78
+ setSearching(false);
79
+ }
80
+
81
+ return (
82
+ <Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
83
+ <Box marginBottom={1}>
84
+ <Text bold color="cyan">Search</Text>
85
+ {results.length > 0 && (
86
+ <Text color="gray"> ({results.length} results)</Text>
87
+ )}
88
+ </Box>
89
+
90
+ <Box marginBottom={1}>
91
+ <Text color="cyan">▸ </Text>
92
+ <TextInput
93
+ value={query}
94
+ onChange={handleSearch}
95
+ placeholder="Type to search..."
96
+ />
97
+ </Box>
98
+
99
+ {searching && <Text color="gray">Searching...</Text>}
100
+
101
+ {results.length > 0 && (
102
+ <Box flexDirection="column" marginTop={1}>
103
+ {results.map((r, i) => (
104
+ <Box key={`${r.file}-${r.line}`}>
105
+ <Text color={i === selected ? "cyan" : "gray"}>
106
+ {i === selected ? "▸ " : " "}
107
+ </Text>
108
+ <Text color="yellow">{r.file}</Text>
109
+ <Text color="gray">:{r.line} </Text>
110
+ <Text color={i === selected ? "white" : "gray"}>
111
+ {r.content.length > 50
112
+ ? r.content.substring(0, 50) + "..."
113
+ : r.content}
114
+ </Text>
115
+ </Box>
116
+ ))}
117
+ </Box>
118
+ )}
119
+
120
+ {query.length >= 2 && results.length === 0 && !searching && (
121
+ <Text color="gray">No results found</Text>
122
+ )}
123
+
124
+ <Box marginTop={1}>
125
+ <Text color="gray">↑↓: navigate | Esc: close</Text>
126
+ </Box>
127
+ </Box>
128
+ );
129
+ }
@@ -0,0 +1,80 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text } from "ink";
3
+ import { existsSync, statSync } from "fs";
4
+ import { readFile } from "fs/promises";
5
+ import { CONFIG, thinkPath } from "../../core/config";
6
+ import { extractLearnings } from "../../core/dedup";
7
+
8
+ interface StatusBarProps {
9
+ message?: string;
10
+ }
11
+
12
+ export function StatusBar({ message }: StatusBarProps) {
13
+ const [pendingCount, setPendingCount] = useState(0);
14
+ const [learningsCount, setLearningsCount] = useState(0);
15
+ const [lastSync, setLastSync] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ loadStatus();
19
+ const interval = setInterval(loadStatus, 5000);
20
+ return () => clearInterval(interval);
21
+ }, []);
22
+
23
+ async function loadStatus() {
24
+ // Count pending
25
+ const pendingPath = thinkPath(CONFIG.files.pending);
26
+ if (existsSync(pendingPath)) {
27
+ const content = await readFile(pendingPath, "utf-8");
28
+ setPendingCount(extractLearnings(content).length);
29
+ }
30
+
31
+ // Count learnings
32
+ const learningsPath = thinkPath(CONFIG.files.learnings);
33
+ if (existsSync(learningsPath)) {
34
+ const content = await readFile(learningsPath, "utf-8");
35
+ setLearningsCount(extractLearnings(content).length);
36
+ }
37
+
38
+ // Last sync time
39
+ if (existsSync(CONFIG.claudeMdPath)) {
40
+ const stats = statSync(CONFIG.claudeMdPath);
41
+ const ago = formatTimeAgo(stats.mtime);
42
+ setLastSync(ago);
43
+ }
44
+ }
45
+
46
+ return (
47
+ <Box borderStyle="single" borderColor="gray" paddingX={1}>
48
+ <Box flexGrow={1}>
49
+ {message ? (
50
+ <Text color="yellow">{message}</Text>
51
+ ) : (
52
+ <Text color="gray">Ready</Text>
53
+ )}
54
+ </Box>
55
+ <Box marginLeft={2}>
56
+ <Text color="green">{learningsCount}</Text>
57
+ <Text color="gray"> learnings</Text>
58
+ </Box>
59
+ {pendingCount > 0 && (
60
+ <Box marginLeft={2}>
61
+ <Text color="yellow">{pendingCount}</Text>
62
+ <Text color="gray"> pending</Text>
63
+ </Box>
64
+ )}
65
+ {lastSync && (
66
+ <Box marginLeft={2}>
67
+ <Text color="gray">synced {lastSync}</Text>
68
+ </Box>
69
+ )}
70
+ </Box>
71
+ );
72
+ }
73
+
74
+ function formatTimeAgo(date: Date): string {
75
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
76
+ if (seconds < 60) return "just now";
77
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
78
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
79
+ return `${Math.floor(seconds / 86400)}d ago`;
80
+ }