rex-claude 1.1.7 → 2.0.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 (57) hide show
  1. package/dist/guards/completion-guard.sh +40 -0
  2. package/dist/guards/dangerous-cmd-guard.sh +50 -0
  3. package/dist/guards/scope-guard.sh +16 -0
  4. package/dist/guards/session-summary.sh +42 -0
  5. package/dist/guards/test-protect-guard.sh +15 -0
  6. package/dist/guards/ui-checklist-guard.sh +44 -0
  7. package/dist/index.js +454 -0
  8. package/dist/init-YMRG5ZXU.js +248 -0
  9. package/dist/optimize-NE47FMOP.js +111 -0
  10. package/package.json +26 -22
  11. package/README.md +0 -163
  12. package/activity/activity.jsonl +0 -443
  13. package/activity/config.lua +0 -3
  14. package/activity/init.lua +0 -49
  15. package/dist/cli.js +0 -504
  16. package/dotfiles/CLAUDE.md +0 -136
  17. package/dotfiles/commands/clean.md +0 -8
  18. package/dotfiles/commands/doc.md +0 -8
  19. package/dotfiles/commands/review.md +0 -15
  20. package/dotfiles/commands/scaffold.md +0 -11
  21. package/dotfiles/commands/test.md +0 -11
  22. package/dotfiles/docs/cloudflare.md +0 -62
  23. package/dotfiles/docs/nextjs.md +0 -79
  24. package/dotfiles/docs/react.md +0 -63
  25. package/dotfiles/docs/tailwind.md +0 -45
  26. package/dotfiles/docs/telegram-bot.md +0 -55
  27. package/dotfiles/rules/api-design.md +0 -63
  28. package/dotfiles/rules/defensive-engineering.md +0 -42
  29. package/dotfiles/rules/docs-first.md +0 -47
  30. package/dotfiles/rules/frontend.md +0 -41
  31. package/dotfiles/rules/git-workflow.md +0 -57
  32. package/dotfiles/rules/never-assume.md +0 -39
  33. package/dotfiles/rules/security.md +0 -46
  34. package/dotfiles/rules/testing.md +0 -33
  35. package/dotfiles/settings.json +0 -80
  36. package/dotfiles/skills/build-validate/SKILL.md +0 -16
  37. package/dotfiles/skills/code-review/SKILL.md +0 -18
  38. package/dotfiles/skills/context-loader/SKILL.md +0 -25
  39. package/dotfiles/skills/debug-assist/SKILL.md +0 -26
  40. package/dotfiles/skills/deploy-checklist/SKILL.md +0 -54
  41. package/dotfiles/skills/dstudio-design-system/SKILL.md +0 -120
  42. package/dotfiles/skills/figma-workflow/SKILL.md +0 -23
  43. package/dotfiles/skills/fix-issue/SKILL.md +0 -43
  44. package/dotfiles/skills/one-shot/SKILL.md +0 -18
  45. package/dotfiles/skills/pr-review-loop/SKILL.md +0 -41
  46. package/dotfiles/skills/project-init/SKILL.md +0 -45
  47. package/dotfiles/skills/research/SKILL.md +0 -17
  48. package/dotfiles/skills/spec-interview/SKILL.md +0 -20
  49. package/dotfiles/skills/token-guard/SKILL.md +0 -26
  50. package/dotfiles/templates/CLAUDE.md.template +0 -39
  51. package/memory/package.json +0 -24
  52. package/memory/src/embed.ts +0 -23
  53. package/memory/src/ingest.ts +0 -257
  54. package/memory/src/search.ts +0 -32
  55. package/memory/src/server.ts +0 -69
  56. package/memory/tsconfig.json +0 -14
  57. package/tmux/.tmux.conf +0 -73
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # REX Guard: Completion Verification
3
+ # Hook: Stop — runs when Claude considers stopping
4
+ # Prevents the "70-80% problem" — Claude declaring done with incomplete work
5
+
6
+ # Check for TODO/placeholder/stub patterns in recently modified files
7
+ MODIFIED_FILES=$(git diff --name-only HEAD 2>/dev/null)
8
+ if [ -z "$MODIFIED_FILES" ]; then
9
+ exit 0
10
+ fi
11
+
12
+ ISSUES=""
13
+
14
+ # Check for incomplete implementation markers
15
+ for file in $MODIFIED_FILES; do
16
+ if [ ! -f "$file" ]; then continue; fi
17
+
18
+ # Skip non-code files
19
+ case "$file" in
20
+ *.md|*.json|*.lock|*.yaml|*.yml|*.txt|*.log) continue ;;
21
+ esac
22
+
23
+ TODOS=$(grep -n "TODO\|FIXME\|HACK\|XXX\|placeholder\|not.implemented\|throw new Error.*TODO" "$file" 2>/dev/null | head -5)
24
+ if [ -n "$TODOS" ]; then
25
+ ISSUES="${ISSUES}\n⚠ Incomplete markers in ${file}:\n${TODOS}\n"
26
+ fi
27
+
28
+ # Check for empty function bodies (common LLM pattern)
29
+ EMPTY_FN=$(grep -n "{\s*}" "$file" 2>/dev/null | grep -v "import\|interface\|type " | head -3)
30
+ if [ -n "$EMPTY_FN" ]; then
31
+ ISSUES="${ISSUES}\n⚠ Empty function bodies in ${file}:\n${EMPTY_FN}\n"
32
+ fi
33
+ done
34
+
35
+ if [ -n "$ISSUES" ]; then
36
+ echo "REX Guard: Found potential incomplete implementation:"
37
+ echo -e "$ISSUES"
38
+ echo ""
39
+ echo "Verify these are intentional before declaring the task done."
40
+ fi
@@ -0,0 +1,50 @@
1
+ #!/bin/bash
2
+ # REX Guard: Dangerous Command Blocker
3
+ # Hook: PreToolUse (matcher: Bash)
4
+ # Prevents destructive commands from running without confirmation
5
+
6
+ # $TOOL_INPUT contains the command about to be executed
7
+ CMD="$TOOL_INPUT"
8
+
9
+ # Patterns that should ALWAYS be blocked or warned
10
+ BLOCKED_PATTERNS=(
11
+ "rm -rf /"
12
+ "rm -rf ~"
13
+ "rm -rf \$HOME"
14
+ "git push --force main"
15
+ "git push --force master"
16
+ "git push -f origin main"
17
+ "git push -f origin master"
18
+ "git reset --hard"
19
+ "git clean -fd"
20
+ "DROP TABLE"
21
+ "DROP DATABASE"
22
+ "truncate "
23
+ "--dangerously-skip"
24
+ "--no-verify"
25
+ )
26
+
27
+ for pattern in "${BLOCKED_PATTERNS[@]}"; do
28
+ if echo "$CMD" | grep -qi "$pattern"; then
29
+ echo '{"decision": "block", "reason": "REX Guard: Dangerous command detected — '"$pattern"'. Use a safer alternative."}'
30
+ exit 0
31
+ fi
32
+ done
33
+
34
+ # Warn on potentially risky commands (don't block, just flag)
35
+ WARN_PATTERNS=(
36
+ "git push --force"
37
+ "rm -rf"
38
+ "chmod 777"
39
+ "curl.*| sh"
40
+ "curl.*| bash"
41
+ "npm publish"
42
+ "npx wrangler deploy"
43
+ )
44
+
45
+ for pattern in "${WARN_PATTERNS[@]}"; do
46
+ if echo "$CMD" | grep -qi "$pattern"; then
47
+ echo "REX Guard: Risky command detected ($pattern). Proceeding with caution."
48
+ exit 0
49
+ fi
50
+ done
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # REX Guard: Scope Creep Detector
3
+ # Hook: PostToolUse (matcher: Edit|Write)
4
+ # Detects when Claude modifies too many files — sign of scope creep
5
+
6
+ # Count files modified in current session (unstaged changes)
7
+ MODIFIED_COUNT=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
8
+ STAGED_COUNT=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
9
+ TOTAL=$((MODIFIED_COUNT + STAGED_COUNT))
10
+
11
+ if [ "$TOTAL" -gt 12 ]; then
12
+ echo "REX Guard: $TOTAL files changed in this session. This may indicate scope creep."
13
+ echo "Consider committing current work and splitting remaining changes into a separate task."
14
+ elif [ "$TOTAL" -gt 8 ]; then
15
+ echo "REX Guard: $TOTAL files modified. Getting large — consider a checkpoint commit."
16
+ fi
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # REX Guard: Session Summary
3
+ # Hook: Stop — auto-saves session state to memory
4
+ # Prevents context loss after compaction by persisting key decisions
5
+
6
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
7
+ PROJECT_NAME=$(basename "$PROJECT_DIR")
8
+ MEMORY_DIR="$HOME/.claude/projects/-$(echo "$PROJECT_DIR" | tr '/' '-')/memory"
9
+ SUMMARY_FILE="$MEMORY_DIR/last-session.md"
10
+
11
+ mkdir -p "$MEMORY_DIR"
12
+
13
+ # Capture current state
14
+ BRANCH=$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || echo "unknown")
15
+ MODIFIED=$(git -C "$PROJECT_DIR" diff --name-only 2>/dev/null | head -20)
16
+ STAGED=$(git -C "$PROJECT_DIR" diff --cached --name-only 2>/dev/null | head -20)
17
+ RECENT_COMMITS=$(git -C "$PROJECT_DIR" log --oneline -5 2>/dev/null)
18
+ UNTRACKED=$(git -C "$PROJECT_DIR" ls-files --others --exclude-standard 2>/dev/null | head -10)
19
+
20
+ cat > "$SUMMARY_FILE" << SUMMARY
21
+ # Last Session Summary
22
+ Updated: $(date -u '+%Y-%m-%d %H:%M UTC')
23
+
24
+ ## Branch: $BRANCH
25
+
26
+ ## Recent commits
27
+ $RECENT_COMMITS
28
+
29
+ ## Modified files (unstaged)
30
+ $MODIFIED
31
+
32
+ ## Staged files
33
+ $STAGED
34
+
35
+ ## Untracked files
36
+ $UNTRACKED
37
+ SUMMARY
38
+
39
+ # Also try to ingest the session transcript (background, non-blocking)
40
+ if command -v npx &>/dev/null; then
41
+ (npx rex-cli ingest 2>/dev/null &)
42
+ fi
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ # REX Guard: Test File Protection
3
+ # Hook: PostToolUse (matcher: Edit|Write)
4
+ # Prevents the #1 LLM anti-pattern: modifying tests to match broken code
5
+
6
+ # $TOOL_INPUT contains the file path and changes
7
+ INPUT="$TOOL_INPUT"
8
+
9
+ # Check if a test file was modified
10
+ if echo "$INPUT" | grep -qE '\.(test|spec)\.(ts|tsx|js|jsx|py)'; then
11
+ # Check if assertions were changed (likely making tests pass by changing expectations)
12
+ if echo "$INPUT" | grep -qE 'expect\(|assert\.|assertEqual|toBe\(|toEqual\(|toMatch\('; then
13
+ echo "REX Guard: Test assertions modified. Remember: fix the CODE, not the tests. Tests define truth."
14
+ fi
15
+ fi
@@ -0,0 +1,44 @@
1
+ #!/bin/bash
2
+ # REX Guard: UI States Checklist
3
+ # Hook: PostToolUse (matcher: Edit|Write)
4
+ # Prevents the "missing states" anti-pattern: LLMs generate happy path only
5
+
6
+ INPUT="$TOOL_INPUT"
7
+
8
+ # Only check UI component files
9
+ if ! echo "$INPUT" | grep -qE '\.(tsx|jsx|vue|svelte)'; then
10
+ exit 0
11
+ fi
12
+
13
+ # Extract file path from tool input
14
+ FILE_PATH=$(echo "$INPUT" | grep -oE '[a-zA-Z0-9_./-]+\.(tsx|jsx|vue|svelte)' | head -1)
15
+ if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
16
+ exit 0
17
+ fi
18
+
19
+ ISSUES=""
20
+
21
+ # Check if component does any data fetching
22
+ if grep -qE 'fetch\(|useQuery|useSWR|axios\.|\.get\(|\.post\(|trpc\.' "$FILE_PATH" 2>/dev/null; then
23
+ # Must have loading state
24
+ if ! grep -qE 'loading|isLoading|isPending|Skeleton|Spinner|Loading' "$FILE_PATH" 2>/dev/null; then
25
+ ISSUES="${ISSUES}\n - Missing loading state (no spinner/skeleton while fetching)"
26
+ fi
27
+
28
+ # Must have error state
29
+ if ! grep -qE 'error|isError|Error|catch|onError' "$FILE_PATH" 2>/dev/null; then
30
+ ISSUES="${ISSUES}\n - Missing error state (no error handling for failed fetch)"
31
+ fi
32
+
33
+ # Must have empty state
34
+ if ! grep -qE 'empty|no.results|no.data|\.length\s*[=!]==?\s*0|isEmpty' "$FILE_PATH" 2>/dev/null; then
35
+ ISSUES="${ISSUES}\n - Missing empty state (no UI for zero results)"
36
+ fi
37
+ fi
38
+
39
+ if [ -n "$ISSUES" ]; then
40
+ echo "REX Guard: UI component may be missing required states:"
41
+ echo -e "$ISSUES"
42
+ echo ""
43
+ echo "Every component that fetches data needs: loading + error + empty states."
44
+ fi
package/dist/index.js ADDED
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../core/dist/index.js
4
+ import { homedir } from "os";
5
+ import { join as join9 } from "path";
6
+ import { readFile, access } from "fs/promises";
7
+ import { join } from "path";
8
+ import { readdir, readFile as readFile2 } from "fs/promises";
9
+ import { join as join2 } from "path";
10
+ import { readFile as readFile3, access as access2 } from "fs/promises";
11
+ import { join as join3, dirname } from "path";
12
+ import { readFile as readFile4 } from "fs/promises";
13
+ import { join as join4 } from "path";
14
+ import { execSync } from "child_process";
15
+ import { readdir as readdir2 } from "fs/promises";
16
+ import { join as join5 } from "path";
17
+ import { readFile as readFile5 } from "fs/promises";
18
+ import { join as join6 } from "path";
19
+ import { access as access3 } from "fs/promises";
20
+ import { join as join7 } from "path";
21
+ import { readdir as readdir3, stat } from "fs/promises";
22
+ import { join as join8 } from "path";
23
+ import { execSync as execSync2 } from "child_process";
24
+ import { platform, release, arch } from "os";
25
+ async function checkConfig(claudeDir) {
26
+ const results = [];
27
+ const claudeMdPath = join(claudeDir, "CLAUDE.md");
28
+ try {
29
+ const content = await readFile(claudeMdPath, "utf-8");
30
+ results.push(
31
+ content.trim().length > 0 ? { name: "CLAUDE.md", status: "pass", message: "Present and non-empty" } : { name: "CLAUDE.md", status: "warn", message: "File exists but is empty" }
32
+ );
33
+ } catch {
34
+ results.push({ name: "CLAUDE.md", status: "fail", message: "Not found" });
35
+ }
36
+ const settingsPath = join(claudeDir, "settings.json");
37
+ try {
38
+ const content = await readFile(settingsPath, "utf-8");
39
+ JSON.parse(content);
40
+ results.push({ name: "settings.json", status: "pass", message: "Valid JSON" });
41
+ } catch (err) {
42
+ const message = err instanceof SyntaxError ? "Invalid JSON" : "Not found";
43
+ results.push({ name: "settings.json", status: "fail", message });
44
+ }
45
+ const vaultPath = join(claudeDir, "vault.md");
46
+ try {
47
+ await access(vaultPath);
48
+ results.push({ name: "vault.md", status: "pass", message: "Present" });
49
+ } catch {
50
+ results.push({ name: "vault.md", status: "warn", message: "Not found (optional)" });
51
+ }
52
+ return { name: "Config", icon: "\u2699", results };
53
+ }
54
+ async function checkRules(claudeDir) {
55
+ const results = [];
56
+ const rulesDir = join2(claudeDir, "rules");
57
+ try {
58
+ const files = await readdir(rulesDir);
59
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
60
+ for (const file of mdFiles) {
61
+ try {
62
+ const content = await readFile2(join2(rulesDir, file), "utf-8");
63
+ results.push(
64
+ content.trim().length > 0 ? { name: file, status: "pass", message: "Present and non-empty" } : { name: file, status: "warn", message: "Empty file" }
65
+ );
66
+ } catch {
67
+ results.push({ name: file, status: "fail", message: "Cannot read" });
68
+ }
69
+ }
70
+ if (mdFiles.length === 0) {
71
+ results.push({ name: "Rules directory", status: "warn", message: "No rule files found" });
72
+ }
73
+ } catch {
74
+ results.push({ name: "Rules directory", status: "fail", message: "Directory not found" });
75
+ }
76
+ return { name: "Rules", icon: "\u{1F4CF}", results };
77
+ }
78
+ async function checkMemory(claudeDir) {
79
+ const results = [];
80
+ const projectsDir = join3(claudeDir, "projects");
81
+ try {
82
+ const { readdir: readdir4 } = await import("fs/promises");
83
+ const entries = await readdir4(projectsDir, { recursive: true });
84
+ const memoryFiles = entries.filter((e) => typeof e === "string" && e.endsWith("MEMORY.md"));
85
+ if (memoryFiles.length === 0) {
86
+ results.push({ name: "MEMORY.md", status: "warn", message: "No memory files found" });
87
+ }
88
+ for (const memFile of memoryFiles) {
89
+ const fullPath = join3(projectsDir, memFile);
90
+ try {
91
+ const content = await readFile3(fullPath, "utf-8");
92
+ if (content.trim().length === 0) {
93
+ results.push({ name: memFile, status: "warn", message: "Empty" });
94
+ continue;
95
+ }
96
+ results.push({ name: memFile, status: "pass", message: "Present and non-empty" });
97
+ const linkRegex = /\[([^\]]+)\]\(([^)]+\.md)\)/g;
98
+ let match;
99
+ while ((match = linkRegex.exec(content)) !== null) {
100
+ const linkedPath = join3(dirname(fullPath), match[2]);
101
+ try {
102
+ await access2(linkedPath);
103
+ results.push({ name: match[2], status: "pass", message: "Linked file exists" });
104
+ } catch {
105
+ results.push({ name: match[2], status: "warn", message: "Linked file missing" });
106
+ }
107
+ }
108
+ } catch {
109
+ results.push({ name: memFile, status: "fail", message: "Cannot read" });
110
+ }
111
+ }
112
+ } catch {
113
+ results.push({ name: "Projects directory", status: "warn", message: "Not found" });
114
+ }
115
+ return { name: "Memory", icon: "\u{1F9E0}", results };
116
+ }
117
+ async function checkMcpServers(claudeDir) {
118
+ const results = [];
119
+ const settingsPath = join4(claudeDir, "settings.json");
120
+ try {
121
+ const content = await readFile4(settingsPath, "utf-8");
122
+ const settings = JSON.parse(content);
123
+ const servers = settings.mcpServers ?? {};
124
+ const serverNames = Object.keys(servers);
125
+ if (serverNames.length === 0) {
126
+ results.push({ name: "MCP Servers", status: "warn", message: "None configured" });
127
+ return { name: "MCP Servers", icon: "\u{1F50C}", results };
128
+ }
129
+ for (const name of serverNames) {
130
+ const server = servers[name];
131
+ const command = server.command;
132
+ if (!command) {
133
+ results.push({ name, status: "warn", message: "No command specified" });
134
+ continue;
135
+ }
136
+ try {
137
+ execSync(`which ${command}`, { stdio: "ignore" });
138
+ results.push({ name, status: "pass", message: `${command} found` });
139
+ } catch {
140
+ results.push({ name, status: "warn", message: `${command} not found in PATH` });
141
+ }
142
+ }
143
+ } catch {
144
+ results.push({ name: "settings.json", status: "fail", message: "Cannot read MCP config" });
145
+ }
146
+ return { name: "MCP Servers", icon: "\u{1F50C}", results };
147
+ }
148
+ async function checkPlugins(claudeDir) {
149
+ const results = [];
150
+ const pluginsDir = join5(claudeDir, "plugins", "cache");
151
+ try {
152
+ const entries = await readdir2(pluginsDir);
153
+ if (entries.length === 0) {
154
+ results.push({ name: "Plugins", status: "warn", message: "No plugins installed" });
155
+ }
156
+ for (const entry of entries) {
157
+ results.push({ name: entry, status: "pass", message: "Installed" });
158
+ }
159
+ } catch {
160
+ results.push({ name: "Plugins directory", status: "warn", message: "Not found" });
161
+ }
162
+ return { name: "Plugins", icon: "\u{1F9E9}", results };
163
+ }
164
+ var EXPECTED_HOOKS = ["UserPromptSubmit", "PreToolUse", "Stop", "SessionStart", "SessionEnd"];
165
+ async function checkHooks(claudeDir) {
166
+ const results = [];
167
+ const settingsPath = join6(claudeDir, "settings.json");
168
+ try {
169
+ const content = await readFile5(settingsPath, "utf-8");
170
+ const settings = JSON.parse(content);
171
+ const hooks = settings.hooks ?? {};
172
+ for (const hookName of EXPECTED_HOOKS) {
173
+ if (hooks[hookName] && Array.isArray(hooks[hookName]) && hooks[hookName].length > 0) {
174
+ results.push({ name: hookName, status: "pass", message: `${hooks[hookName].length} handler(s)` });
175
+ } else {
176
+ results.push({ name: hookName, status: "warn", message: "Not configured" });
177
+ }
178
+ }
179
+ const extraHooks = Object.keys(hooks).filter((h) => !EXPECTED_HOOKS.includes(h));
180
+ for (const hook of extraHooks) {
181
+ results.push({ name: hook, status: "pass", message: "Custom hook" });
182
+ }
183
+ } catch {
184
+ results.push({ name: "Hooks config", status: "fail", message: "Cannot read settings.json" });
185
+ }
186
+ return { name: "Hooks", icon: "\u{1FA9D}", results };
187
+ }
188
+ var EXPECTED_GUARDS = [
189
+ "completion-guard.sh",
190
+ "dangerous-cmd-guard.sh",
191
+ "test-protect-guard.sh",
192
+ "session-summary.sh",
193
+ "ui-checklist-guard.sh",
194
+ "scope-guard.sh"
195
+ ];
196
+ async function checkGuards(claudeDir) {
197
+ const results = [];
198
+ const guardsDir = join7(claudeDir, "rex-guards");
199
+ for (const guard of EXPECTED_GUARDS) {
200
+ const guardPath = join7(guardsDir, guard);
201
+ try {
202
+ await access3(guardPath);
203
+ results.push({ name: guard, status: "pass", message: "Installed" });
204
+ } catch {
205
+ results.push({ name: guard, status: "warn", message: "Not installed \u2014 run rex init" });
206
+ }
207
+ }
208
+ return { name: "Guards", icon: "\u{1F6E1}", results };
209
+ }
210
+ async function checkDocsCache(claudeDir) {
211
+ const results = [];
212
+ const docsDir = join8(claudeDir, "docs");
213
+ try {
214
+ const files = await readdir3(docsDir);
215
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
216
+ if (mdFiles.length === 0) {
217
+ results.push({ name: "Docs cache", status: "warn", message: "No cached docs" });
218
+ }
219
+ for (const file of mdFiles) {
220
+ const fileStat = await stat(join8(docsDir, file));
221
+ const sizeKb = Math.round(fileStat.size / 1024);
222
+ results.push({ name: file, status: "pass", message: `${sizeKb}KB` });
223
+ }
224
+ } catch {
225
+ results.push({ name: "Docs directory", status: "warn", message: "Not found" });
226
+ }
227
+ return { name: "Docs Cache", icon: "\u{1F4DA}", results };
228
+ }
229
+ function getVersion(command) {
230
+ try {
231
+ return execSync2(command, { encoding: "utf-8", timeout: 5e3 }).trim();
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+ async function checkEnvironment() {
237
+ const results = [];
238
+ const claudeVersion = getVersion("claude --version 2>/dev/null");
239
+ results.push(
240
+ claudeVersion ? { name: "Claude Code", status: "pass", message: claudeVersion } : { name: "Claude Code", status: "fail", message: "Not found" }
241
+ );
242
+ const nodeVersion = getVersion("node --version");
243
+ results.push(
244
+ nodeVersion ? { name: "Node.js", status: "pass", message: nodeVersion } : { name: "Node.js", status: "fail", message: "Not found" }
245
+ );
246
+ const gitVersion = getVersion("git --version");
247
+ results.push(
248
+ gitVersion ? { name: "Git", status: "pass", message: gitVersion } : { name: "Git", status: "fail", message: "Not found" }
249
+ );
250
+ results.push({
251
+ name: "OS",
252
+ status: "pass",
253
+ message: `${platform()} ${release()} (${arch()})`
254
+ });
255
+ const shell = process.env.SHELL ?? "unknown";
256
+ results.push({ name: "Shell", status: "pass", message: shell });
257
+ return { name: "Environment", icon: "\u{1F4BB}", results };
258
+ }
259
+ async function runAllChecks(claudeDir) {
260
+ const dir = claudeDir ?? join9(homedir(), ".claude");
261
+ const groups = await Promise.all([
262
+ checkConfig(dir),
263
+ checkRules(dir),
264
+ checkMemory(dir),
265
+ checkMcpServers(dir),
266
+ checkPlugins(dir),
267
+ checkHooks(dir),
268
+ checkGuards(dir),
269
+ checkDocsCache(dir),
270
+ checkEnvironment()
271
+ ]);
272
+ const allResults = groups.flatMap((g) => g.results);
273
+ const failCount = allResults.filter((r) => r.status === "fail").length;
274
+ const warnCount = allResults.filter((r) => r.status === "warn").length;
275
+ let status = "healthy";
276
+ if (failCount > 0) status = "broken";
277
+ else if (warnCount > 2) status = "degraded";
278
+ return {
279
+ groups,
280
+ status,
281
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
282
+ version: "0.1.0"
283
+ };
284
+ }
285
+
286
+ // src/index.ts
287
+ import { existsSync } from "fs";
288
+ import { join as join10 } from "path";
289
+ var COLORS = {
290
+ reset: "\x1B[0m",
291
+ green: "\x1B[32m",
292
+ yellow: "\x1B[33m",
293
+ red: "\x1B[31m",
294
+ dim: "\x1B[2m",
295
+ bold: "\x1B[1m",
296
+ cyan: "\x1B[36m"
297
+ };
298
+ function statusIcon(status) {
299
+ switch (status) {
300
+ case "pass":
301
+ return `${COLORS.green}\u2713${COLORS.reset}`;
302
+ case "fail":
303
+ return `${COLORS.red}\u2717${COLORS.reset}`;
304
+ case "warn":
305
+ return `${COLORS.yellow}!${COLORS.reset}`;
306
+ default:
307
+ return " ";
308
+ }
309
+ }
310
+ function formatGroup(group) {
311
+ const passed = group.results.filter((r) => r.status === "pass").length;
312
+ const total = group.results.length;
313
+ let out = `
314
+ ${COLORS.bold}${group.icon} ${group.name}${COLORS.reset}`;
315
+ out += ` ${COLORS.dim}${passed}/${total}${COLORS.reset}
316
+ `;
317
+ for (const result of group.results) {
318
+ out += ` ${statusIcon(result.status)} ${result.name} ${COLORS.dim}\u2014 ${result.message}${COLORS.reset}
319
+ `;
320
+ }
321
+ return out;
322
+ }
323
+ function formatReport(report) {
324
+ const line = "\u2550".repeat(45);
325
+ const thinLine = "\u2500".repeat(45);
326
+ let statusColor = COLORS.green;
327
+ if (report.status === "degraded") statusColor = COLORS.yellow;
328
+ if (report.status === "broken") statusColor = COLORS.red;
329
+ let out = `
330
+ ${line}
331
+ `;
332
+ out += `${COLORS.bold} REX DOCTOR \u2014 Health Check${COLORS.reset}
333
+ `;
334
+ out += `${line}
335
+ `;
336
+ for (const group of report.groups) {
337
+ out += formatGroup(group);
338
+ }
339
+ const allResults = report.groups.flatMap((g) => g.results);
340
+ const passed = allResults.filter((r) => r.status === "pass").length;
341
+ const total = allResults.length;
342
+ out += `
343
+ ${thinLine}
344
+ `;
345
+ out += ` Summary: ${COLORS.bold}${passed}/${total}${COLORS.reset} checks passed
346
+ `;
347
+ out += ` Status: ${statusColor}${COLORS.bold}${report.status.toUpperCase()}${COLORS.reset}
348
+ `;
349
+ out += `${line}
350
+ `;
351
+ return out;
352
+ }
353
+ async function main() {
354
+ const command = process.argv[2] ?? "help";
355
+ switch (command) {
356
+ case "doctor": {
357
+ const report = await runAllChecks();
358
+ console.log(formatReport(report));
359
+ process.exit(report.status === "broken" ? 1 : 0);
360
+ break;
361
+ }
362
+ case "status": {
363
+ const report = await runAllChecks();
364
+ const allResults = report.groups.flatMap((g) => g.results);
365
+ const passed = allResults.filter((r) => r.status === "pass").length;
366
+ const total = allResults.length;
367
+ const dot = report.status === "healthy" ? `${COLORS.green}\u25CF${COLORS.reset}` : report.status === "degraded" ? `${COLORS.yellow}\u25CF${COLORS.reset}` : `${COLORS.red}\u25CB${COLORS.reset}`;
368
+ console.log(`REX ${dot} ${report.status.toUpperCase()} \u2014 ${passed}/${total} checks passed`);
369
+ break;
370
+ }
371
+ case "init": {
372
+ const { init } = await import("./init-YMRG5ZXU.js");
373
+ await init();
374
+ break;
375
+ }
376
+ case "ingest": {
377
+ try {
378
+ const { execSync: execSync3 } = await import("child_process");
379
+ const memDir = findMemoryPackage();
380
+ if (!memDir) {
381
+ console.log(`${COLORS.yellow}Memory package not found.${COLORS.reset} This feature requires @rex/memory.`);
382
+ console.log(`Run from the REX monorepo or install @rex/memory separately.`);
383
+ process.exit(1);
384
+ }
385
+ console.log(`${COLORS.cyan}Ingesting sessions...${COLORS.reset}`);
386
+ execSync3("npx tsx src/ingest.ts", { cwd: memDir, stdio: "inherit" });
387
+ } catch {
388
+ process.exit(1);
389
+ }
390
+ break;
391
+ }
392
+ case "search": {
393
+ const query = process.argv.slice(3).join(" ");
394
+ if (!query) {
395
+ console.error("Usage: rex search <query>");
396
+ process.exit(1);
397
+ }
398
+ try {
399
+ const memDir = findMemoryPackage();
400
+ if (!memDir) {
401
+ console.log(`${COLORS.yellow}Memory package not found.${COLORS.reset} This feature requires @rex/memory + Ollama.`);
402
+ process.exit(1);
403
+ }
404
+ const { execSync: execSync3 } = await import("child_process");
405
+ execSync3(`npx tsx src/cli-search.ts ${query.split(" ").map((w) => JSON.stringify(w)).join(" ")}`, { cwd: memDir, stdio: "inherit" });
406
+ } catch {
407
+ process.exit(1);
408
+ }
409
+ break;
410
+ }
411
+ case "optimize": {
412
+ const { optimize } = await import("./optimize-NE47FMOP.js");
413
+ await optimize();
414
+ break;
415
+ }
416
+ case "--version":
417
+ case "-v":
418
+ console.log("rex-cli v0.1.0");
419
+ break;
420
+ case "help":
421
+ default:
422
+ console.log(`
423
+ ${COLORS.bold}REX${COLORS.reset} \u2014 Claude Code sous steroides
424
+
425
+ ${COLORS.bold}Commands:${COLORS.reset}
426
+ rex init Setup REX (guards, hooks, MCP)
427
+ rex doctor Full health check (9 categories)
428
+ rex status Quick one-line status
429
+
430
+ ${COLORS.bold}Memory (requires Ollama):${COLORS.reset}
431
+ rex ingest Sync session history to vector DB
432
+ rex search Semantic search across past sessions
433
+ rex optimize Analyze CLAUDE.md with local LLM
434
+
435
+ ${COLORS.bold}Info:${COLORS.reset}
436
+ rex help Show this help
437
+ rex --version Show version
438
+
439
+ ${COLORS.dim}After install: rex init \u2014 everything else is automatic.${COLORS.reset}
440
+ `);
441
+ }
442
+ }
443
+ function findMemoryPackage() {
444
+ const thisDir = new URL(".", import.meta.url).pathname;
445
+ const candidates = [
446
+ join10(thisDir, "..", "..", "memory"),
447
+ join10(process.env.HOME || "~", ".rex-memory")
448
+ ];
449
+ for (const c of candidates) {
450
+ if (existsSync(join10(c, "src", "ingest.ts"))) return c;
451
+ }
452
+ return null;
453
+ }
454
+ main().catch(console.error);