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.
Files changed (33) hide show
  1. package/dist/chunk-7AGI43F5.js +42 -0
  2. package/dist/context-FN5O5YBI.js +114 -0
  3. package/dist/gateway-EOVQXRON.js +198 -0
  4. package/dist/guards/completion-guard.sh +15 -2
  5. package/dist/guards/dangerous-cmd-guard.sh +2 -2
  6. package/dist/guards/error-pattern-guard.sh +45 -0
  7. package/dist/guards/notify-telegram.sh +34 -0
  8. package/dist/guards/test-protect-guard.sh +2 -2
  9. package/dist/guards/ui-checklist-guard.sh +1 -1
  10. package/dist/index.js +52 -13
  11. package/dist/{init-S7BKM2N2.js → init-W3XGDQ6D.js} +274 -11
  12. package/dist/llm-YRORUH7E.js +9 -0
  13. package/dist/optimize-UKMAGQQE.js +148 -0
  14. package/dist/setup-AO3MW46W.js +252 -0
  15. package/dist/skills/build-validate/SKILL.md +23 -0
  16. package/dist/skills/code-review/SKILL.md +25 -0
  17. package/dist/skills/context-loader/SKILL.md +25 -0
  18. package/dist/skills/debug-assist/SKILL.md +26 -0
  19. package/dist/skills/deploy-checklist/SKILL.md +61 -0
  20. package/dist/skills/dstudio-design-system/SKILL.md +120 -0
  21. package/dist/skills/figma-workflow/SKILL.md +23 -0
  22. package/dist/skills/fix-issue/SKILL.md +43 -0
  23. package/dist/skills/new-rule/SKILL.md +19 -0
  24. package/dist/skills/notify/SKILL.md +26 -0
  25. package/dist/skills/one-shot/SKILL.md +18 -0
  26. package/dist/skills/pr-review-loop/SKILL.md +48 -0
  27. package/dist/skills/project-init/SKILL.md +45 -0
  28. package/dist/skills/research/SKILL.md +17 -0
  29. package/dist/skills/rex-boot/SKILL.md +64 -0
  30. package/dist/skills/spec-interview/SKILL.md +20 -0
  31. package/dist/skills/token-guard/SKILL.md +26 -0
  32. package/package.json +4 -4
  33. 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
- return;
106
- }
107
- try {
108
- execSync(`launchctl unload ${plistPath}`, { stdio: "ignore" });
109
- } catch {
110
- }
111
- try {
112
- unlinkSync(plistPath);
113
- } catch {
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
- ok("LaunchAgent removed");
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,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ detectModel,
4
+ llm
5
+ } from "./chunk-7AGI43F5.js";
6
+ export {
7
+ detectModel,
8
+ llm
9
+ };
@@ -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
+ };