rex-claude 2.2.0 → 3.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.
- package/dist/chunk-7AGI43F5.js +42 -0
- package/dist/context-FN5O5YBI.js +114 -0
- package/dist/gateway-EOVQXRON.js +198 -0
- package/dist/guards/completion-guard.sh +15 -2
- package/dist/guards/dangerous-cmd-guard.sh +2 -2
- package/dist/guards/error-pattern-guard.sh +45 -0
- package/dist/guards/notify-telegram.sh +34 -0
- package/dist/guards/test-protect-guard.sh +2 -2
- package/dist/guards/ui-checklist-guard.sh +1 -1
- package/dist/index.js +52 -13
- package/dist/{init-NXU37FCV.js → init-W3XGDQ6D.js} +159 -1
- package/dist/llm-YRORUH7E.js +9 -0
- package/dist/optimize-UKMAGQQE.js +148 -0
- package/dist/setup-AO3MW46W.js +252 -0
- package/dist/skills/build-validate/SKILL.md +23 -0
- package/dist/skills/code-review/SKILL.md +25 -0
- package/dist/skills/context-loader/SKILL.md +25 -0
- package/dist/skills/debug-assist/SKILL.md +26 -0
- package/dist/skills/deploy-checklist/SKILL.md +61 -0
- package/dist/skills/dstudio-design-system/SKILL.md +120 -0
- package/dist/skills/figma-workflow/SKILL.md +23 -0
- package/dist/skills/fix-issue/SKILL.md +43 -0
- package/dist/skills/new-rule/SKILL.md +19 -0
- package/dist/skills/notify/SKILL.md +26 -0
- package/dist/skills/one-shot/SKILL.md +18 -0
- package/dist/skills/pr-review-loop/SKILL.md +48 -0
- package/dist/skills/project-init/SKILL.md +45 -0
- package/dist/skills/research/SKILL.md +17 -0
- package/dist/skills/rex-boot/SKILL.md +64 -0
- package/dist/skills/spec-interview/SKILL.md +20 -0
- package/dist/skills/token-guard/SKILL.md +26 -0
- package/package.json +4 -4
- package/dist/optimize-NE47FMOP.js +0 -111
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/init.ts
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync } from "fs";
|
|
5
5
|
import { join, dirname } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { execSync } from "child_process";
|
|
@@ -42,6 +42,7 @@ function ensureDir(dir) {
|
|
|
42
42
|
}
|
|
43
43
|
var PLIST_LABEL = "com.dstudio.rex";
|
|
44
44
|
var INGEST_PLIST_LABEL = "com.dstudio.rex-ingest";
|
|
45
|
+
var GATEWAY_PLIST_LABEL = "com.dstudio.rex-gateway";
|
|
45
46
|
function installIngestAgent() {
|
|
46
47
|
if (process.platform !== "darwin") {
|
|
47
48
|
info("Auto-ingest only supported on macOS");
|
|
@@ -114,6 +115,101 @@ function uninstallIngestAgent() {
|
|
|
114
115
|
}
|
|
115
116
|
ok("Ingest LaunchAgent removed");
|
|
116
117
|
}
|
|
118
|
+
function installGatewayAgent() {
|
|
119
|
+
if (process.platform !== "darwin") {
|
|
120
|
+
info("Gateway LaunchAgent only supported on macOS");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
124
|
+
let hasTelegram = false;
|
|
125
|
+
try {
|
|
126
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
127
|
+
hasTelegram = !!(settings.env?.REX_TELEGRAM_BOT_TOKEN && settings.env?.REX_TELEGRAM_CHAT_ID);
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
if (!hasTelegram) {
|
|
131
|
+
info("Telegram not configured \u2014 skipping gateway LaunchAgent (run rex setup first)");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const launchAgentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
135
|
+
ensureDir(launchAgentsDir);
|
|
136
|
+
const plistPath = join(launchAgentsDir, `${GATEWAY_PLIST_LABEL}.plist`);
|
|
137
|
+
let rexBin = "";
|
|
138
|
+
try {
|
|
139
|
+
rexBin = execSync("which rex", { encoding: "utf-8" }).trim();
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
if (!rexBin) {
|
|
143
|
+
info("rex binary not in PATH \u2014 skipping gateway LaunchAgent");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (existsSync(plistPath)) {
|
|
147
|
+
skip("Gateway LaunchAgent already installed (Telegram bot always-on)");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let botToken = "";
|
|
151
|
+
let chatIdVal = "";
|
|
152
|
+
try {
|
|
153
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
154
|
+
botToken = settings.env?.REX_TELEGRAM_BOT_TOKEN || "";
|
|
155
|
+
chatIdVal = settings.env?.REX_TELEGRAM_CHAT_ID || "";
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
159
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
160
|
+
<plist version="1.0">
|
|
161
|
+
<dict>
|
|
162
|
+
<key>Label</key>
|
|
163
|
+
<string>${GATEWAY_PLIST_LABEL}</string>
|
|
164
|
+
<key>ProgramArguments</key>
|
|
165
|
+
<array>
|
|
166
|
+
<string>${rexBin}</string>
|
|
167
|
+
<string>gateway</string>
|
|
168
|
+
</array>
|
|
169
|
+
<key>RunAtLoad</key>
|
|
170
|
+
<true/>
|
|
171
|
+
<key>KeepAlive</key>
|
|
172
|
+
<true/>
|
|
173
|
+
<key>StandardOutPath</key>
|
|
174
|
+
<string>${join(homedir(), ".claude", "rex-gateway.log")}</string>
|
|
175
|
+
<key>StandardErrorPath</key>
|
|
176
|
+
<string>${join(homedir(), ".claude", "rex-gateway.log")}</string>
|
|
177
|
+
<key>EnvironmentVariables</key>
|
|
178
|
+
<dict>
|
|
179
|
+
<key>PATH</key>
|
|
180
|
+
<string>/usr/local/bin:/usr/bin:/bin:${dirname(rexBin)}</string>
|
|
181
|
+
<key>REX_TELEGRAM_BOT_TOKEN</key>
|
|
182
|
+
<string>${botToken}</string>
|
|
183
|
+
<key>REX_TELEGRAM_CHAT_ID</key>
|
|
184
|
+
<string>${chatIdVal}</string>
|
|
185
|
+
</dict>
|
|
186
|
+
</dict>
|
|
187
|
+
</plist>
|
|
188
|
+
`;
|
|
189
|
+
writeFileSync(plistPath, plist);
|
|
190
|
+
try {
|
|
191
|
+
execSync(`launchctl load ${plistPath}`, { stdio: "ignore" });
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
ok("Gateway LaunchAgent installed \u2014 Telegram bot always-on (auto-restart)");
|
|
195
|
+
}
|
|
196
|
+
function uninstallGatewayAgent() {
|
|
197
|
+
if (process.platform !== "darwin") return;
|
|
198
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", `${GATEWAY_PLIST_LABEL}.plist`);
|
|
199
|
+
if (!existsSync(plistPath)) {
|
|
200
|
+
info("Gateway LaunchAgent not installed");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
execSync(`launchctl unload ${plistPath}`, { stdio: "ignore" });
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
unlinkSync(plistPath);
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
ok("Gateway LaunchAgent removed");
|
|
212
|
+
}
|
|
117
213
|
function installApp() {
|
|
118
214
|
if (process.platform !== "darwin") return;
|
|
119
215
|
const thisDir = new URL(".", import.meta.url).pathname;
|
|
@@ -213,6 +309,7 @@ function uninstallStartup() {
|
|
|
213
309
|
ok("LaunchAgent removed");
|
|
214
310
|
}
|
|
215
311
|
uninstallIngestAgent();
|
|
312
|
+
uninstallGatewayAgent();
|
|
216
313
|
}
|
|
217
314
|
async function init() {
|
|
218
315
|
const claudeDir = join(homedir(), ".claude");
|
|
@@ -345,6 +442,18 @@ fi
|
|
|
345
442
|
event: "PostToolUse",
|
|
346
443
|
desc: "Scope creep detector",
|
|
347
444
|
matcher: "Edit|Write"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
file: "error-pattern-guard.sh",
|
|
448
|
+
event: "PostToolUse",
|
|
449
|
+
desc: "Recurring error pattern detector",
|
|
450
|
+
matcher: "Bash"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
file: "notify-telegram.sh",
|
|
454
|
+
event: "Stop",
|
|
455
|
+
desc: "Telegram notification on task completion",
|
|
456
|
+
matcher: void 0
|
|
348
457
|
}
|
|
349
458
|
];
|
|
350
459
|
let guardsInstalled = 0;
|
|
@@ -381,6 +490,7 @@ fi
|
|
|
381
490
|
}
|
|
382
491
|
installStartup();
|
|
383
492
|
installIngestAgent();
|
|
493
|
+
installGatewayAgent();
|
|
384
494
|
installApp();
|
|
385
495
|
let ollamaOk = false;
|
|
386
496
|
try {
|
|
@@ -404,6 +514,52 @@ fi
|
|
|
404
514
|
} else {
|
|
405
515
|
info("Ollama not running \u2014 needed for memory/RAG. Install: https://ollama.ai");
|
|
406
516
|
}
|
|
517
|
+
const bundledSkillsDir = join(thisDir, "..", "skills");
|
|
518
|
+
if (existsSync(bundledSkillsDir)) {
|
|
519
|
+
const skillsTargetDir = join(claudeDir, "skills");
|
|
520
|
+
ensureDir(skillsTargetDir);
|
|
521
|
+
let skillsSynced = 0;
|
|
522
|
+
try {
|
|
523
|
+
const skillDirs = readdirSync(bundledSkillsDir).filter((d) => {
|
|
524
|
+
try {
|
|
525
|
+
return statSync(join(bundledSkillsDir, d)).isDirectory();
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
for (const skill of skillDirs) {
|
|
531
|
+
const srcSkill = join(bundledSkillsDir, skill, "SKILL.md");
|
|
532
|
+
const destSkillDir = join(skillsTargetDir, skill);
|
|
533
|
+
const destSkill = join(destSkillDir, "SKILL.md");
|
|
534
|
+
if (existsSync(srcSkill)) {
|
|
535
|
+
const srcContent = readFileSync(srcSkill, "utf-8");
|
|
536
|
+
const destContent = existsSync(destSkill) ? readFileSync(destSkill, "utf-8") : "";
|
|
537
|
+
if (srcContent !== destContent) {
|
|
538
|
+
ensureDir(destSkillDir);
|
|
539
|
+
writeFileSync(destSkill, srcContent);
|
|
540
|
+
skillsSynced++;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
if (skillsSynced > 0) {
|
|
547
|
+
ok(`${skillsSynced} skills synced from rex-claude`);
|
|
548
|
+
} else {
|
|
549
|
+
skip("All skills up to date");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const memoryMonorepoDir = join(thisDir, "..", "..", "memory");
|
|
553
|
+
const memoryTargetDir = join(homedir(), ".rex-memory");
|
|
554
|
+
if (existsSync(join(memoryMonorepoDir, "package.json")) && !existsSync(join(memoryTargetDir, "package.json"))) {
|
|
555
|
+
try {
|
|
556
|
+
execSync(`cp -R "${memoryMonorepoDir}" "${memoryTargetDir}"`, { stdio: "ignore" });
|
|
557
|
+
execSync("npm install --production 2>/dev/null", { cwd: memoryTargetDir, stdio: "ignore" });
|
|
558
|
+
ok("@rex/memory synced to ~/.rex-memory/");
|
|
559
|
+
} catch {
|
|
560
|
+
info("Could not sync @rex/memory \u2014 install manually");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
407
563
|
writeJson(settingsPath, settings);
|
|
408
564
|
console.log(`
|
|
409
565
|
${COLORS.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${COLORS.reset}`);
|
|
@@ -424,8 +580,10 @@ ${COLORS.bold} REX initialized!${COLORS.reset}`);
|
|
|
424
580
|
export {
|
|
425
581
|
init,
|
|
426
582
|
installApp,
|
|
583
|
+
installGatewayAgent,
|
|
427
584
|
installIngestAgent,
|
|
428
585
|
installStartup,
|
|
586
|
+
uninstallGatewayAgent,
|
|
429
587
|
uninstallIngestAgent,
|
|
430
588
|
uninstallStartup
|
|
431
589
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
detectModel,
|
|
4
|
+
llm
|
|
5
|
+
} from "./chunk-7AGI43F5.js";
|
|
6
|
+
|
|
7
|
+
// src/optimize.ts
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
var COLORS = {
|
|
12
|
+
reset: "\x1B[0m",
|
|
13
|
+
green: "\x1B[32m",
|
|
14
|
+
yellow: "\x1B[33m",
|
|
15
|
+
red: "\x1B[31m",
|
|
16
|
+
bold: "\x1B[1m",
|
|
17
|
+
dim: "\x1B[2m",
|
|
18
|
+
cyan: "\x1B[36m"
|
|
19
|
+
};
|
|
20
|
+
var OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
|
|
21
|
+
function collectImports(content, baseDir) {
|
|
22
|
+
const imports = [];
|
|
23
|
+
const importRegex = /@import\s+["']([^"']+)["']/g;
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
26
|
+
const importPath = join(baseDir, match[1]);
|
|
27
|
+
if (existsSync(importPath)) {
|
|
28
|
+
imports.push(`
|
|
29
|
+
--- ${match[1]} ---
|
|
30
|
+
${readFileSync(importPath, "utf-8")}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const rulesDir = join(baseDir, "rules");
|
|
34
|
+
if (existsSync(rulesDir)) {
|
|
35
|
+
try {
|
|
36
|
+
const rules = readdirSync(rulesDir).filter((f) => f.endsWith(".md"));
|
|
37
|
+
for (const rule of rules) {
|
|
38
|
+
imports.push(`
|
|
39
|
+
--- rules/${rule} ---
|
|
40
|
+
${readFileSync(join(rulesDir, rule), "utf-8")}`);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return imports.join("\n");
|
|
46
|
+
}
|
|
47
|
+
async function optimize(apply = false) {
|
|
48
|
+
const line = "\u2550".repeat(45);
|
|
49
|
+
console.log(`
|
|
50
|
+
${line}`);
|
|
51
|
+
console.log(`${COLORS.bold} REX OPTIMIZE${apply ? " --apply" : ""}${COLORS.reset}`);
|
|
52
|
+
console.log(`${line}
|
|
53
|
+
`);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${OLLAMA_URL}/api/tags`);
|
|
56
|
+
if (!res.ok) throw new Error();
|
|
57
|
+
} catch {
|
|
58
|
+
console.error(`${COLORS.red}Ollama not running.${COLORS.reset} Start it: ollama serve`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const cwd = process.cwd();
|
|
62
|
+
const projectClaudeMd = join(cwd, "CLAUDE.md");
|
|
63
|
+
const globalClaudeMd = join(homedir(), ".claude", "CLAUDE.md");
|
|
64
|
+
const target = existsSync(projectClaudeMd) ? projectClaudeMd : globalClaudeMd;
|
|
65
|
+
if (!existsSync(target)) {
|
|
66
|
+
console.error(`${COLORS.red}No CLAUDE.md found.${COLORS.reset}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const content = readFileSync(target, "utf-8");
|
|
70
|
+
const baseDir = join(target, "..");
|
|
71
|
+
const importedContent = collectImports(content, baseDir);
|
|
72
|
+
const fullContent = content + importedContent;
|
|
73
|
+
const lines = content.split("\n").length;
|
|
74
|
+
const chars = fullContent.length;
|
|
75
|
+
const tokens = Math.ceil(chars / 4);
|
|
76
|
+
console.log(` ${COLORS.cyan}Target:${COLORS.reset} ${target}`);
|
|
77
|
+
console.log(` ${COLORS.cyan}Size:${COLORS.reset} ${lines} lines, ~${tokens} tokens (with imports)`);
|
|
78
|
+
console.log();
|
|
79
|
+
const model = await detectModel();
|
|
80
|
+
console.log(` ${COLORS.dim}Analyzing with ${model}...${COLORS.reset}`);
|
|
81
|
+
if (!apply) {
|
|
82
|
+
const analysis = await llm(
|
|
83
|
+
`Analyze this CLAUDE.md file and all its imported rules. Provide specific suggestions to reduce token count while keeping all important instructions. Focus on:
|
|
84
|
+
1. Redundant or duplicate instructions (across main file AND imported rules)
|
|
85
|
+
2. Overly verbose sections that could be shortened
|
|
86
|
+
3. Content that could be moved to separate @import files
|
|
87
|
+
4. Dead or outdated references
|
|
88
|
+
5. Contradictions between files
|
|
89
|
+
|
|
90
|
+
CLAUDE.md + imports:
|
|
91
|
+
---
|
|
92
|
+
${fullContent.slice(0, 8e3)}
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
Provide a concise analysis with specific, actionable suggestions. Format each suggestion as:
|
|
96
|
+
- [SECTION] What to change and why (estimated savings: N tokens)`,
|
|
97
|
+
"You are a technical editor that optimizes AI instruction files. Be direct and specific. Output only the analysis, no preamble.",
|
|
98
|
+
model
|
|
99
|
+
);
|
|
100
|
+
console.log(`
|
|
101
|
+
${COLORS.bold} Analysis:${COLORS.reset}
|
|
102
|
+
`);
|
|
103
|
+
for (const l of analysis.split("\n")) {
|
|
104
|
+
console.log(` ${l}`);
|
|
105
|
+
}
|
|
106
|
+
console.log(`
|
|
107
|
+
${COLORS.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${COLORS.reset}`);
|
|
108
|
+
console.log(`
|
|
109
|
+
${COLORS.dim}Run ${COLORS.cyan}rex optimize --apply${COLORS.reset}${COLORS.dim} to auto-apply suggestions${COLORS.reset}`);
|
|
110
|
+
} else {
|
|
111
|
+
const backupPath = target + ".bak";
|
|
112
|
+
writeFileSync(backupPath, content);
|
|
113
|
+
console.log(` ${COLORS.green}\u2713${COLORS.reset} Backup saved to ${backupPath}`);
|
|
114
|
+
const optimized = await llm(
|
|
115
|
+
`Rewrite this CLAUDE.md to be more concise while keeping ALL important instructions. Rules:
|
|
116
|
+
- Remove redundancy and duplication
|
|
117
|
+
- Shorten verbose explanations to bullet points
|
|
118
|
+
- Keep all actionable rules, security requirements, and workflow steps
|
|
119
|
+
- Preserve @import references
|
|
120
|
+
- Use tables instead of verbose lists where possible
|
|
121
|
+
- Remove filler words and unnecessary context
|
|
122
|
+
- Output ONLY the rewritten CLAUDE.md content, nothing else
|
|
123
|
+
|
|
124
|
+
Original:
|
|
125
|
+
---
|
|
126
|
+
${content.slice(0, 8e3)}
|
|
127
|
+
---`,
|
|
128
|
+
"You rewrite AI instruction files to be maximally concise. Output only the rewritten file, no commentary.",
|
|
129
|
+
model
|
|
130
|
+
);
|
|
131
|
+
writeFileSync(target, optimized);
|
|
132
|
+
const oldTokens = Math.ceil(content.length / 4);
|
|
133
|
+
const newTokens = Math.ceil(optimized.length / 4);
|
|
134
|
+
const saved = oldTokens - newTokens;
|
|
135
|
+
const pct = Math.round(saved / oldTokens * 100);
|
|
136
|
+
console.log(` ${COLORS.green}\u2713${COLORS.reset} CLAUDE.md rewritten`);
|
|
137
|
+
console.log(`
|
|
138
|
+
${COLORS.bold}Before:${COLORS.reset} ~${oldTokens} tokens`);
|
|
139
|
+
console.log(` ${COLORS.bold}After:${COLORS.reset} ~${newTokens} tokens`);
|
|
140
|
+
console.log(` ${COLORS.bold}Saved:${COLORS.reset} ~${saved} tokens (${pct}%)`);
|
|
141
|
+
console.log(`
|
|
142
|
+
${COLORS.dim}Review changes: diff ${backupPath} ${target}${COLORS.reset}`);
|
|
143
|
+
}
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
export {
|
|
147
|
+
optimize
|
|
148
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/setup.ts
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { platform, totalmem, homedir } from "os";
|
|
6
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { createInterface } from "readline";
|
|
9
|
+
var COLORS = {
|
|
10
|
+
reset: "\x1B[0m",
|
|
11
|
+
green: "\x1B[32m",
|
|
12
|
+
yellow: "\x1B[33m",
|
|
13
|
+
red: "\x1B[31m",
|
|
14
|
+
bold: "\x1B[1m",
|
|
15
|
+
dim: "\x1B[2m",
|
|
16
|
+
cyan: "\x1B[36m"
|
|
17
|
+
};
|
|
18
|
+
function ok(msg) {
|
|
19
|
+
console.log(` ${COLORS.green}\u2713${COLORS.reset} ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
function info(msg) {
|
|
22
|
+
console.log(` ${COLORS.cyan}\u2139${COLORS.reset} ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
function fail(msg) {
|
|
25
|
+
console.log(` ${COLORS.red}\u2717${COLORS.reset} ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
var OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
|
|
28
|
+
async function isOllamaInstalled() {
|
|
29
|
+
try {
|
|
30
|
+
execSync("which ollama", { stdio: "ignore" });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function isOllamaRunning() {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`${OLLAMA_URL}/api/tags`);
|
|
39
|
+
return res.ok;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function getInstalledModels() {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`${OLLAMA_URL}/api/tags`);
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
return data.models.map((m) => m.name);
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function pullModel(model) {
|
|
54
|
+
console.log(` ${COLORS.dim}Pulling ${model}...${COLORS.reset}`);
|
|
55
|
+
try {
|
|
56
|
+
execSync(`ollama pull ${model}`, { stdio: "inherit" });
|
|
57
|
+
ok(`${model} installed`);
|
|
58
|
+
} catch {
|
|
59
|
+
fail(`Failed to pull ${model}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function testEmbed() {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${OLLAMA_URL}/api/embed`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify({ model: "nomic-embed-text", input: "test embedding" })
|
|
68
|
+
});
|
|
69
|
+
return res.ok;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function testGenerate(model) {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(`${OLLAMA_URL}/api/generate`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ model, prompt: 'Say "ok" in one word.', stream: false })
|
|
80
|
+
});
|
|
81
|
+
return res.ok;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function prompt(question) {
|
|
87
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
rl.question(` ${COLORS.cyan}?${COLORS.reset} ${question} `, (answer) => {
|
|
90
|
+
rl.close();
|
|
91
|
+
resolve(answer.trim());
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async function setupTelegram() {
|
|
96
|
+
console.log(`
|
|
97
|
+
${COLORS.bold}Telegram Gateway${COLORS.reset}`);
|
|
98
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
99
|
+
let settings = {};
|
|
100
|
+
try {
|
|
101
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
if (!settings.env) settings.env = {};
|
|
105
|
+
const existingToken = settings.env.REX_TELEGRAM_BOT_TOKEN;
|
|
106
|
+
const existingChat = settings.env.REX_TELEGRAM_CHAT_ID;
|
|
107
|
+
if (existingToken && existingChat) {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(`https://api.telegram.org/bot${existingToken}/sendMessage`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify({ chat_id: existingChat, text: "\u{1F514} REX Setup \u2014 Telegram gateway verified", parse_mode: "Markdown" })
|
|
113
|
+
});
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
ok("Telegram gateway already configured and working");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
info("Existing Telegram config found but not working \u2014 reconfiguring");
|
|
121
|
+
}
|
|
122
|
+
const botToken = await prompt("Telegram Bot Token (from @BotFather):");
|
|
123
|
+
if (!botToken) {
|
|
124
|
+
info("Skipped Telegram setup");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
129
|
+
const data = await res.json();
|
|
130
|
+
if (!data.ok) {
|
|
131
|
+
fail("Invalid bot token");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
ok(`Bot: @${data.result?.username}`);
|
|
135
|
+
} catch {
|
|
136
|
+
fail("Could not validate bot token");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log(`
|
|
140
|
+
${COLORS.dim}Send /start to your bot on Telegram, then press Enter...${COLORS.reset}`);
|
|
141
|
+
await prompt("Press Enter when done");
|
|
142
|
+
let chatId = "";
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/getUpdates`);
|
|
145
|
+
const data = await res.json();
|
|
146
|
+
const msg = data.result?.find((u) => u.message);
|
|
147
|
+
if (msg?.message) {
|
|
148
|
+
chatId = String(msg.message.chat.id);
|
|
149
|
+
ok(`Chat ID: ${chatId} (from @${msg.message.from?.username ?? "?"})`);
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
if (!chatId) {
|
|
154
|
+
chatId = await prompt("Chat ID (could not auto-detect):");
|
|
155
|
+
}
|
|
156
|
+
if (!chatId) {
|
|
157
|
+
fail("No chat ID \u2014 Telegram setup aborted");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
settings.env.REX_TELEGRAM_BOT_TOKEN = botToken;
|
|
161
|
+
settings.env.REX_TELEGRAM_CHAT_ID = chatId;
|
|
162
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
163
|
+
ok("Telegram credentials saved to settings.json");
|
|
164
|
+
try {
|
|
165
|
+
await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
body: JSON.stringify({ chat_id: chatId, text: "\u2705 *REX Setup Complete*\nTelegram gateway is active.", parse_mode: "Markdown" })
|
|
169
|
+
});
|
|
170
|
+
ok("Test message sent \u2014 check Telegram!");
|
|
171
|
+
} catch {
|
|
172
|
+
fail("Could not send test message");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function setup() {
|
|
176
|
+
const line = "\u2550".repeat(45);
|
|
177
|
+
console.log(`
|
|
178
|
+
${line}`);
|
|
179
|
+
console.log(`${COLORS.bold} REX SETUP \u2014 Full Configuration${COLORS.reset}`);
|
|
180
|
+
console.log(`${line}
|
|
181
|
+
`);
|
|
182
|
+
const ramGB = Math.round(totalmem() / 1024 ** 3);
|
|
183
|
+
const os = platform();
|
|
184
|
+
info(`System: ${os}, ${ramGB}GB RAM`);
|
|
185
|
+
if (!await isOllamaInstalled()) {
|
|
186
|
+
fail("Ollama not installed");
|
|
187
|
+
console.log(`
|
|
188
|
+
Install: ${COLORS.cyan}https://ollama.com/download${COLORS.reset}`);
|
|
189
|
+
if (os === "darwin") {
|
|
190
|
+
info("Opening download page...");
|
|
191
|
+
try {
|
|
192
|
+
execSync("open https://ollama.com/download", { stdio: "ignore" });
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
ok("Ollama installed");
|
|
199
|
+
if (!await isOllamaRunning()) {
|
|
200
|
+
info("Starting Ollama...");
|
|
201
|
+
try {
|
|
202
|
+
execSync("ollama serve &", { stdio: "ignore" });
|
|
203
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
204
|
+
if (await isOllamaRunning()) {
|
|
205
|
+
ok("Ollama started");
|
|
206
|
+
} else {
|
|
207
|
+
fail("Could not start Ollama \u2014 start manually: ollama serve");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
fail("Could not start Ollama \u2014 start manually: ollama serve");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
ok("Ollama running");
|
|
216
|
+
}
|
|
217
|
+
const models = await getInstalledModels();
|
|
218
|
+
if (models.some((m) => m.includes("nomic-embed-text"))) {
|
|
219
|
+
ok("nomic-embed-text already installed");
|
|
220
|
+
} else {
|
|
221
|
+
pullModel("nomic-embed-text");
|
|
222
|
+
}
|
|
223
|
+
const reasoningModel = ramGB >= 16 ? "qwen3.5:9b" : "qwen3.5:4b";
|
|
224
|
+
info(`Selected reasoning model: ${reasoningModel} (${ramGB}GB RAM)`);
|
|
225
|
+
if (models.some((m) => m.includes(reasoningModel.split(":")[0]))) {
|
|
226
|
+
ok(`${reasoningModel} already installed`);
|
|
227
|
+
} else {
|
|
228
|
+
pullModel(reasoningModel);
|
|
229
|
+
}
|
|
230
|
+
console.log(`
|
|
231
|
+
${COLORS.dim}Testing...${COLORS.reset}`);
|
|
232
|
+
const embedOk = await testEmbed();
|
|
233
|
+
const genOk = await testGenerate(reasoningModel);
|
|
234
|
+
if (embedOk) ok("Embedding test passed");
|
|
235
|
+
else fail("Embedding test failed");
|
|
236
|
+
if (genOk) ok("Generation test passed");
|
|
237
|
+
else fail("Generation test failed");
|
|
238
|
+
await setupTelegram();
|
|
239
|
+
console.log(`
|
|
240
|
+
${COLORS.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${COLORS.reset}`);
|
|
241
|
+
if (embedOk && genOk) {
|
|
242
|
+
console.log(`
|
|
243
|
+
${COLORS.green}${COLORS.bold}Setup complete!${COLORS.reset} REX is fully configured.`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(`
|
|
246
|
+
${COLORS.yellow}Setup partial.${COLORS.reset} Some tests failed \u2014 check Ollama logs.`);
|
|
247
|
+
}
|
|
248
|
+
console.log();
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
setup
|
|
252
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-validate
|
|
3
|
+
description: Verify code changes compile, pass tests, and work correctly. Runs build, lint, tests, dev server checks. Reports without modifying code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build Validation
|
|
7
|
+
|
|
8
|
+
Verify the current project state:
|
|
9
|
+
|
|
10
|
+
1. Run build command (`npm run build` or equivalent)
|
|
11
|
+
2. Run linter if configured
|
|
12
|
+
3. Run test suite if exists
|
|
13
|
+
4. Start dev server, verify it loads (curl for 200)
|
|
14
|
+
5. For UI changes, take a screenshot
|
|
15
|
+
|
|
16
|
+
Report each step as PASS/FAIL with details. NEVER modify code — only report.
|
|
17
|
+
|
|
18
|
+
## Auto-Learn
|
|
19
|
+
|
|
20
|
+
If any step FAILS, call `rex_learn` MCP tool:
|
|
21
|
+
- category: `"lesson"`
|
|
22
|
+
- fact: the error message + root cause + fix applied (e.g. "Next.js build fails with X when Y — fix: Z")
|
|
23
|
+
- This builds a knowledge base of project-specific build issues
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: Thorough code review of staged/changed files. Checks logic errors, security, performance, missing states, TypeScript strictness.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Code Review
|
|
7
|
+
|
|
8
|
+
Review all changed files (`git diff`):
|
|
9
|
+
|
|
10
|
+
1. Logic errors and edge cases
|
|
11
|
+
2. Security vulnerabilities (OWASP top 10)
|
|
12
|
+
3. Performance (N+1 queries, unbounded loops, missing indexes)
|
|
13
|
+
4. Missing error handling, loading/empty/error states
|
|
14
|
+
5. TypeScript strictness (no `any`, no `@ts-ignore` without justification)
|
|
15
|
+
6. Consistency with existing codebase patterns
|
|
16
|
+
|
|
17
|
+
Rate findings: **critical** / **warning** / **suggestion**. Provide fix snippets.
|
|
18
|
+
Never suggest modifying tests to pass. Focus bugs > security > style.
|
|
19
|
+
|
|
20
|
+
## Auto-Learn
|
|
21
|
+
|
|
22
|
+
After completing the review, call `rex_learn` MCP tool for each notable finding:
|
|
23
|
+
- category: `"pattern"` for codebase patterns, `"lesson"` for bugs/anti-patterns
|
|
24
|
+
- fact: concise description of the pattern found and why it matters
|
|
25
|
+
- Only learn findings rated **critical** or recurring patterns — skip trivial suggestions
|