rex-claude 2.1.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-S7BKM2N2.js → init-W3XGDQ6D.js} +274 -11
- 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";
|
|
@@ -41,6 +41,201 @@ function ensureDir(dir) {
|
|
|
41
41
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
42
42
|
}
|
|
43
43
|
var PLIST_LABEL = "com.dstudio.rex";
|
|
44
|
+
var INGEST_PLIST_LABEL = "com.dstudio.rex-ingest";
|
|
45
|
+
var GATEWAY_PLIST_LABEL = "com.dstudio.rex-gateway";
|
|
46
|
+
function installIngestAgent() {
|
|
47
|
+
if (process.platform !== "darwin") {
|
|
48
|
+
info("Auto-ingest only supported on macOS");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const launchAgentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
52
|
+
ensureDir(launchAgentsDir);
|
|
53
|
+
const plistPath = join(launchAgentsDir, `${INGEST_PLIST_LABEL}.plist`);
|
|
54
|
+
let rexBin = "";
|
|
55
|
+
try {
|
|
56
|
+
rexBin = execSync("which rex", { encoding: "utf-8" }).trim();
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
if (!rexBin) {
|
|
60
|
+
info("rex binary not in PATH \u2014 skipping ingest LaunchAgent");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (existsSync(plistPath)) {
|
|
64
|
+
skip("Ingest LaunchAgent already installed (auto-ingest every hour)");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
68
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
69
|
+
<plist version="1.0">
|
|
70
|
+
<dict>
|
|
71
|
+
<key>Label</key>
|
|
72
|
+
<string>${INGEST_PLIST_LABEL}</string>
|
|
73
|
+
<key>ProgramArguments</key>
|
|
74
|
+
<array>
|
|
75
|
+
<string>${rexBin}</string>
|
|
76
|
+
<string>ingest</string>
|
|
77
|
+
</array>
|
|
78
|
+
<key>RunAtLoad</key>
|
|
79
|
+
<true/>
|
|
80
|
+
<key>StartInterval</key>
|
|
81
|
+
<integer>3600</integer>
|
|
82
|
+
<key>StandardOutPath</key>
|
|
83
|
+
<string>${join(homedir(), ".claude", "rex-ingest.log")}</string>
|
|
84
|
+
<key>StandardErrorPath</key>
|
|
85
|
+
<string>${join(homedir(), ".claude", "rex-ingest.log")}</string>
|
|
86
|
+
<key>EnvironmentVariables</key>
|
|
87
|
+
<dict>
|
|
88
|
+
<key>PATH</key>
|
|
89
|
+
<string>/usr/local/bin:/usr/bin:/bin:${dirname(rexBin)}</string>
|
|
90
|
+
</dict>
|
|
91
|
+
</dict>
|
|
92
|
+
</plist>
|
|
93
|
+
`;
|
|
94
|
+
writeFileSync(plistPath, plist);
|
|
95
|
+
try {
|
|
96
|
+
execSync(`launchctl load ${plistPath}`, { stdio: "ignore" });
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
ok("Ingest LaunchAgent installed \u2014 auto-ingest every hour");
|
|
100
|
+
}
|
|
101
|
+
function uninstallIngestAgent() {
|
|
102
|
+
if (process.platform !== "darwin") return;
|
|
103
|
+
const plistPath = join(homedir(), "Library", "LaunchAgents", `${INGEST_PLIST_LABEL}.plist`);
|
|
104
|
+
if (!existsSync(plistPath)) {
|
|
105
|
+
info("Ingest LaunchAgent not installed");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
execSync(`launchctl unload ${plistPath}`, { stdio: "ignore" });
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
unlinkSync(plistPath);
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
ok("Ingest LaunchAgent removed");
|
|
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
|
+
}
|
|
213
|
+
function installApp() {
|
|
214
|
+
if (process.platform !== "darwin") return;
|
|
215
|
+
const thisDir = new URL(".", import.meta.url).pathname;
|
|
216
|
+
const buildApp = join(thisDir, "..", "..", "app", "src-tauri", "target", "release", "bundle", "macos", "REX.app");
|
|
217
|
+
const installedApp = "/Applications/REX.app";
|
|
218
|
+
if (existsSync(installedApp)) {
|
|
219
|
+
skip("REX.app already in /Applications");
|
|
220
|
+
} else if (existsSync(buildApp)) {
|
|
221
|
+
try {
|
|
222
|
+
execSync(`cp -R "${buildApp}" "${installedApp}"`, { stdio: "ignore" });
|
|
223
|
+
ok("REX.app installed to /Applications");
|
|
224
|
+
} catch {
|
|
225
|
+
info("Could not copy REX.app to /Applications (try manually)");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
info("REX.app not built \u2014 run `pnpm tauri build` in packages/app first");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
execSync(`osascript -e 'tell application "System Events" to make login item at end with properties {path:"${installedApp}", hidden:false}'`, { stdio: "ignore" });
|
|
234
|
+
ok("REX.app added to Login Items (auto-start on login)");
|
|
235
|
+
} catch {
|
|
236
|
+
skip("REX.app already in Login Items");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
44
239
|
function installStartup() {
|
|
45
240
|
if (process.platform !== "darwin") {
|
|
46
241
|
info("Startup auto-launch only supported on macOS");
|
|
@@ -102,17 +297,19 @@ function uninstallStartup() {
|
|
|
102
297
|
const plistPath = join(homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
103
298
|
if (!existsSync(plistPath)) {
|
|
104
299
|
info("LaunchAgent not installed");
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
300
|
+
} else {
|
|
301
|
+
try {
|
|
302
|
+
execSync(`launchctl unload ${plistPath}`, { stdio: "ignore" });
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
unlinkSync(plistPath);
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
ok("LaunchAgent removed");
|
|
114
310
|
}
|
|
115
|
-
|
|
311
|
+
uninstallIngestAgent();
|
|
312
|
+
uninstallGatewayAgent();
|
|
116
313
|
}
|
|
117
314
|
async function init() {
|
|
118
315
|
const claudeDir = join(homedir(), ".claude");
|
|
@@ -245,6 +442,18 @@ fi
|
|
|
245
442
|
event: "PostToolUse",
|
|
246
443
|
desc: "Scope creep detector",
|
|
247
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
|
|
248
457
|
}
|
|
249
458
|
];
|
|
250
459
|
let guardsInstalled = 0;
|
|
@@ -280,6 +489,9 @@ fi
|
|
|
280
489
|
skip("All REX guards already installed");
|
|
281
490
|
}
|
|
282
491
|
installStartup();
|
|
492
|
+
installIngestAgent();
|
|
493
|
+
installGatewayAgent();
|
|
494
|
+
installApp();
|
|
283
495
|
let ollamaOk = false;
|
|
284
496
|
try {
|
|
285
497
|
const res = await fetch("http://localhost:11434/api/tags");
|
|
@@ -302,6 +514,52 @@ fi
|
|
|
302
514
|
} else {
|
|
303
515
|
info("Ollama not running \u2014 needed for memory/RAG. Install: https://ollama.ai");
|
|
304
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
|
+
}
|
|
305
563
|
writeJson(settingsPath, settings);
|
|
306
564
|
console.log(`
|
|
307
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}`);
|
|
@@ -321,6 +579,11 @@ ${COLORS.bold} REX initialized!${COLORS.reset}`);
|
|
|
321
579
|
}
|
|
322
580
|
export {
|
|
323
581
|
init,
|
|
582
|
+
installApp,
|
|
583
|
+
installGatewayAgent,
|
|
584
|
+
installIngestAgent,
|
|
324
585
|
installStartup,
|
|
586
|
+
uninstallGatewayAgent,
|
|
587
|
+
uninstallIngestAgent,
|
|
325
588
|
uninstallStartup
|
|
326
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
|
+
};
|