openclaw-smartmeter 0.2.2 → 0.4.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/CHANGELOG.md +201 -0
- package/DEPLOYMENT_HANDOFF.md +923 -0
- package/HANDOFF_BRIEF.md +619 -0
- package/README.md +66 -2
- package/SKILL.md +654 -0
- package/canvas-template/analysis.public.json +77 -16
- package/canvas-template/app.js +1040 -440
- package/canvas-template/index.html +343 -115
- package/canvas-template/styles.css +1433 -563
- package/package.json +19 -3
- package/src/analyzer/aggregator.js +4 -2
- package/src/analyzer/config-manager.js +92 -0
- package/src/analyzer/openrouter-client.js +121 -0
- package/src/analyzer/recommender.js +129 -0
- package/src/canvas/api-server.js +191 -9
- package/src/canvas/deployer.js +1 -1
- package/src/cli/commands.js +345 -39
- package/src/cli/index.js +29 -0
- package/src/generator/config-builder.js +7 -2
- package/src/generator/validator.js +7 -7
package/src/cli/commands.js
CHANGED
|
@@ -90,19 +90,21 @@ export async function cmdAnalyze(opts = {}) {
|
|
|
90
90
|
// Start API server in background
|
|
91
91
|
console.log("✓ Starting API server...");
|
|
92
92
|
const apiServer = await startApiServer({ port: apiPort });
|
|
93
|
+
const actualApiPort = apiServer.server.address().port;
|
|
93
94
|
|
|
94
95
|
// Start static file server in background
|
|
95
96
|
console.log("✓ Starting dashboard server...");
|
|
96
97
|
const staticServer = await startStaticFileServer(deployer.canvasDir, port);
|
|
98
|
+
const actualPort = staticServer.port;
|
|
97
99
|
|
|
98
|
-
const url = `http://localhost:${
|
|
100
|
+
const url = `http://localhost:${actualPort}`;
|
|
99
101
|
|
|
100
102
|
console.log(`
|
|
101
103
|
✅ Dashboard is live!
|
|
102
104
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
103
105
|
|
|
104
106
|
🌐 Dashboard URL: ${url}
|
|
105
|
-
📡 API Server: http://localhost:${
|
|
107
|
+
📡 API Server: http://localhost:${actualApiPort}
|
|
106
108
|
|
|
107
109
|
💡 Features:
|
|
108
110
|
• View cost savings and recommendations
|
|
@@ -117,7 +119,7 @@ Opening in your browser...
|
|
|
117
119
|
|
|
118
120
|
// Open browser
|
|
119
121
|
try {
|
|
120
|
-
await deployer.openDashboard(
|
|
122
|
+
await deployer.openDashboard(actualPort);
|
|
121
123
|
console.log("✓ Browser opened\n");
|
|
122
124
|
} catch (err) {
|
|
123
125
|
console.log(`⚠ Could not open browser automatically`);
|
|
@@ -143,13 +145,18 @@ Opening in your browser...
|
|
|
143
145
|
process.on("SIGINT", shutdownHandler);
|
|
144
146
|
process.on("SIGTERM", shutdownHandler);
|
|
145
147
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
+
// Keep process alive - wait indefinitely
|
|
149
|
+
// This prevents the CLI from exiting and killing the servers
|
|
150
|
+
await new Promise(() => {}); // Never resolves, keeps process alive
|
|
148
151
|
|
|
149
152
|
} catch (err) {
|
|
150
|
-
console.error(`\n
|
|
151
|
-
console.log(
|
|
152
|
-
console.log(`
|
|
153
|
+
console.error(`\n❌ Could not start dashboard: ${err.message}\n`);
|
|
154
|
+
console.log(`💡 Tips:`);
|
|
155
|
+
console.log(` • Make sure no other SmartMeter instances are running`);
|
|
156
|
+
console.log(` • Try specifying different ports: --port 8081 --api-port 3002`);
|
|
157
|
+
console.log(` • Check for processes using ports: lsof -i :${opts.port || 8080} -i :${opts.apiPort || 3001}`);
|
|
158
|
+
console.log(`\nYou can still view your analysis by running:`);
|
|
159
|
+
console.log(` smartmeter status\n`);
|
|
153
160
|
console.log(`\nOr start the dashboard manually:`);
|
|
154
161
|
console.log(` smartmeter serve\n`);
|
|
155
162
|
}
|
|
@@ -530,6 +537,224 @@ smartmeter rollback
|
|
|
530
537
|
return { analysis, config };
|
|
531
538
|
}
|
|
532
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Compare two config versions or current vs optimized.
|
|
542
|
+
*/
|
|
543
|
+
export async function cmdDiff(opts = {}) {
|
|
544
|
+
const configPath = opts.configPath || CONFIG_PATH;
|
|
545
|
+
const backupDir = opts.backupDir || OPENCLAW_DIR;
|
|
546
|
+
|
|
547
|
+
// Get current config
|
|
548
|
+
const currentConfig = await readCurrentConfig(configPath);
|
|
549
|
+
|
|
550
|
+
let compareConfig;
|
|
551
|
+
let compareLabel;
|
|
552
|
+
|
|
553
|
+
if (opts.version) {
|
|
554
|
+
// Compare with specific backup version
|
|
555
|
+
const backupPath = join(backupDir, opts.version);
|
|
556
|
+
try {
|
|
557
|
+
const data = await readFile(backupPath, "utf8");
|
|
558
|
+
compareConfig = JSON.parse(data);
|
|
559
|
+
compareLabel = opts.version;
|
|
560
|
+
} catch {
|
|
561
|
+
console.log(`Backup not found: ${opts.version}`);
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
// Compare current with optimized
|
|
566
|
+
const analysis = await runPipeline(opts);
|
|
567
|
+
if (!analysis) {
|
|
568
|
+
console.log("No session data found.");
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
const { config } = generateConfig(analysis, currentConfig);
|
|
572
|
+
compareConfig = config;
|
|
573
|
+
compareLabel = "optimized";
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
console.log(`\n╔════════════════════════════════════════════════════════╗`);
|
|
577
|
+
console.log(`║ SmartMeter Config Diff ║`);
|
|
578
|
+
console.log(`╚════════════════════════════════════════════════════════╝\n`);
|
|
579
|
+
console.log(`Comparing: current ↔ ${compareLabel}\n`);
|
|
580
|
+
|
|
581
|
+
const diffs = diffObjects(currentConfig, compareConfig);
|
|
582
|
+
if (diffs.length === 0) {
|
|
583
|
+
console.log(" No differences found.");
|
|
584
|
+
} else {
|
|
585
|
+
for (const d of diffs) {
|
|
586
|
+
console.log(` ${d.path}:`);
|
|
587
|
+
console.log(` - ${JSON.stringify(d.old)}`);
|
|
588
|
+
console.log(` + ${JSON.stringify(d.new)}\n`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return diffs;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Show cost breakdown by model and category.
|
|
597
|
+
*/
|
|
598
|
+
export async function cmdCosts(opts = {}) {
|
|
599
|
+
const storageDir = opts.storageDir || SMARTMETER_DIR;
|
|
600
|
+
let analysis = await readAnalysis(storageDir);
|
|
601
|
+
|
|
602
|
+
if (!analysis) {
|
|
603
|
+
// Try running pipeline
|
|
604
|
+
analysis = await runPipeline(opts);
|
|
605
|
+
if (!analysis) {
|
|
606
|
+
console.log("No analysis found. Run `smartmeter analyze` first.");
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
console.log(`\n╔════════════════════════════════════════════════════════╗`);
|
|
612
|
+
console.log(`║ SmartMeter Cost Breakdown ║`);
|
|
613
|
+
console.log(`╚════════════════════════════════════════════════════════╝`);
|
|
614
|
+
|
|
615
|
+
console.log(`\n📊 By Model:\n`);
|
|
616
|
+
const models = Object.entries(analysis.models || {});
|
|
617
|
+
models.sort((a, b) => b[1].cost - a[1].cost);
|
|
618
|
+
for (const [name, m] of models) {
|
|
619
|
+
const pct = analysis.summary.totalCost > 0
|
|
620
|
+
? ((m.cost / analysis.summary.totalCost) * 100).toFixed(1)
|
|
621
|
+
: "0.0";
|
|
622
|
+
console.log(` ${name}`);
|
|
623
|
+
console.log(` Tasks: ${m.count} | Cost: $${m.cost.toFixed(2)} | Avg: $${m.avgCostPerTask.toFixed(4)}/task | Share: ${pct}%`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log(`\n📂 By Category:\n`);
|
|
627
|
+
const cats = Object.entries(analysis.categories || {});
|
|
628
|
+
for (const [name, c] of cats) {
|
|
629
|
+
let catCost = 0;
|
|
630
|
+
for (const mb of Object.values(c.modelBreakdown)) {
|
|
631
|
+
catCost += mb.totalCost || 0;
|
|
632
|
+
}
|
|
633
|
+
console.log(` ${name}: ${c.count} tasks, $${catCost.toFixed(2)} total`);
|
|
634
|
+
for (const [model, mb] of Object.entries(c.modelBreakdown)) {
|
|
635
|
+
console.log(` └─ ${model}: ${mb.count} tasks, $${mb.avgCost.toFixed(4)}/task`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
console.log(`\n💰 Summary:`);
|
|
640
|
+
console.log(` Total cost: $${analysis.summary.totalCost.toFixed(2)}`);
|
|
641
|
+
console.log(` Monthly proj: $${analysis.summary.currentMonthlyCost.toFixed(2)}`);
|
|
642
|
+
console.log(` Cache savings: $${(analysis.caching?.estimatedCacheSavings || 0).toFixed(2)}\n`);
|
|
643
|
+
|
|
644
|
+
return analysis;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Get or set SmartMeter configuration values.
|
|
649
|
+
*/
|
|
650
|
+
export async function cmdConfig(opts = {}) {
|
|
651
|
+
const { getConfig, saveConfig } = await import("../analyzer/config-manager.js");
|
|
652
|
+
const config = await getConfig();
|
|
653
|
+
|
|
654
|
+
if (opts.key && opts.value !== undefined) {
|
|
655
|
+
// Set mode
|
|
656
|
+
config[opts.key] = opts.value;
|
|
657
|
+
config.lastUpdated = new Date().toISOString();
|
|
658
|
+
await saveConfig(config);
|
|
659
|
+
console.log(`✓ Set ${opts.key} = ${opts.value}`);
|
|
660
|
+
return config;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (opts.key) {
|
|
664
|
+
// Get specific key
|
|
665
|
+
const val = config[opts.key];
|
|
666
|
+
if (val !== undefined) {
|
|
667
|
+
console.log(`${opts.key} = ${JSON.stringify(val)}`);
|
|
668
|
+
} else {
|
|
669
|
+
console.log(`Key '${opts.key}' not set.`);
|
|
670
|
+
}
|
|
671
|
+
return val;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Show all config
|
|
675
|
+
console.log(`\n╔════════════════════════════════════════════════════════╗`);
|
|
676
|
+
console.log(`║ SmartMeter Configuration ║`);
|
|
677
|
+
console.log(`╚════════════════════════════════════════════════════════╝\n`);
|
|
678
|
+
|
|
679
|
+
for (const [key, value] of Object.entries(config)) {
|
|
680
|
+
if (key === "openRouterApiKey" && value) {
|
|
681
|
+
console.log(` ${key} = ${value.substring(0, 9)}...${value.substring(value.length - 4)}`);
|
|
682
|
+
} else {
|
|
683
|
+
console.log(` ${key} = ${JSON.stringify(value)}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
console.log();
|
|
687
|
+
|
|
688
|
+
return config;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Show config backup/version history.
|
|
693
|
+
*/
|
|
694
|
+
export async function cmdHistory(opts = {}) {
|
|
695
|
+
const backupDir = opts.backupDir || OPENCLAW_DIR;
|
|
696
|
+
|
|
697
|
+
let entries;
|
|
698
|
+
try {
|
|
699
|
+
entries = await readdir(backupDir);
|
|
700
|
+
} catch {
|
|
701
|
+
console.log("No backup directory found.");
|
|
702
|
+
return [];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const backups = entries
|
|
706
|
+
.filter((f) => f.startsWith("openclaw.json.backup-"))
|
|
707
|
+
.sort()
|
|
708
|
+
.reverse();
|
|
709
|
+
|
|
710
|
+
console.log(`\n╔════════════════════════════════════════════════════════╗`);
|
|
711
|
+
console.log(`║ SmartMeter Config History ║`);
|
|
712
|
+
console.log(`╚════════════════════════════════════════════════════════╝\n`);
|
|
713
|
+
|
|
714
|
+
if (backups.length === 0) {
|
|
715
|
+
console.log(" No config backups found. Backups are created when you run `smartmeter apply`.\n");
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
console.log(` Found ${backups.length} backup(s):\n`);
|
|
720
|
+
for (let i = 0; i < backups.length; i++) {
|
|
721
|
+
const ts = backups[i].replace("openclaw.json.backup-", "").replace(/-/g, function(m, offset) {
|
|
722
|
+
// First 10 chars are date, rest is time
|
|
723
|
+
return offset < 10 ? "-" : ":";
|
|
724
|
+
});
|
|
725
|
+
const label = i === 0 ? " (latest)" : "";
|
|
726
|
+
console.log(` ${i + 1}. ${backups[i]}${label}`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
console.log(`\n💡 To rollback to the latest: smartmeter rollback`);
|
|
730
|
+
console.log(`💡 To compare with a backup: smartmeter diff --version <backup-filename>\n`);
|
|
731
|
+
|
|
732
|
+
return backups;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Simple deep diff between two objects. Returns array of { path, old, new }.
|
|
737
|
+
*/
|
|
738
|
+
function diffObjects(obj1, obj2, prefix = "") {
|
|
739
|
+
const diffs = [];
|
|
740
|
+
const allKeys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]);
|
|
741
|
+
|
|
742
|
+
for (const key of allKeys) {
|
|
743
|
+
if (key === "_smartmeter") continue; // Skip metadata
|
|
744
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
745
|
+
const v1 = (obj1 || {})[key];
|
|
746
|
+
const v2 = (obj2 || {})[key];
|
|
747
|
+
|
|
748
|
+
if (typeof v1 === "object" && typeof v2 === "object" && v1 !== null && v2 !== null && !Array.isArray(v1) && !Array.isArray(v2)) {
|
|
749
|
+
diffs.push(...diffObjects(v1, v2, path));
|
|
750
|
+
} else if (JSON.stringify(v1) !== JSON.stringify(v2)) {
|
|
751
|
+
diffs.push({ path, old: v1, new: v2 });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return diffs;
|
|
756
|
+
}
|
|
757
|
+
|
|
533
758
|
export async function cmdServe(opts = {}) {
|
|
534
759
|
const port = opts.port || 8080;
|
|
535
760
|
const apiPort = opts.apiPort || 3001;
|
|
@@ -562,22 +787,25 @@ export async function cmdServe(opts = {}) {
|
|
|
562
787
|
console.log("⚠ No analysis data. Run 'smartmeter analyze' first.");
|
|
563
788
|
}
|
|
564
789
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
790
|
+
try {
|
|
791
|
+
// Start API server
|
|
792
|
+
console.log("\n🚀 Starting API server...");
|
|
793
|
+
const apiServer = await startApiServer({ port: apiPort });
|
|
794
|
+
const actualApiPort = apiServer.server.address().port;
|
|
795
|
+
|
|
796
|
+
// Start static file server (using Node.js)
|
|
797
|
+
console.log(`\n🚀 Starting dashboard server on port ${port}...`);
|
|
798
|
+
const staticServer = await startStaticFileServer(deployer.canvasDir, port);
|
|
799
|
+
const actualPort = staticServer.port;
|
|
800
|
+
|
|
801
|
+
const url = `http://localhost:${actualPort}`;
|
|
802
|
+
console.log(`
|
|
575
803
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
576
804
|
✅ SmartMeter is ready!
|
|
577
805
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
578
806
|
|
|
579
807
|
🌐 Dashboard: ${url}
|
|
580
|
-
📡 API Server: http://localhost:${
|
|
808
|
+
📡 API Server: http://localhost:${actualApiPort}
|
|
581
809
|
|
|
582
810
|
💡 Features enabled:
|
|
583
811
|
• Live dashboard updates (auto-refresh every 5s)
|
|
@@ -589,30 +817,74 @@ Press Ctrl+C to stop all servers
|
|
|
589
817
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
590
818
|
`);
|
|
591
819
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
820
|
+
if (shouldOpen) {
|
|
821
|
+
console.log("🌐 Opening dashboard in browser...");
|
|
822
|
+
try {
|
|
823
|
+
await deployer.openDashboard(actualPort);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
console.log(` Open manually: ${url}`);
|
|
826
|
+
}
|
|
598
827
|
}
|
|
599
|
-
}
|
|
600
828
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
829
|
+
// Handle shutdown
|
|
830
|
+
process.on("SIGINT", async () => {
|
|
831
|
+
console.log("\n\n🛑 Shutting down servers...");
|
|
832
|
+
await staticServer.stop();
|
|
833
|
+
await apiServer.stop();
|
|
834
|
+
console.log("✓ Servers stopped");
|
|
835
|
+
process.exit(0);
|
|
836
|
+
});
|
|
609
837
|
|
|
610
|
-
|
|
838
|
+
return { url, apiPort: actualApiPort, deployer, apiServer, staticServer };
|
|
839
|
+
} catch (err) {
|
|
840
|
+
console.error(`\n❌ Failed to start servers: ${err.message}\n`);
|
|
841
|
+
console.log(`💡 Tips:`);
|
|
842
|
+
console.log(` • Make sure no other SmartMeter instances are running`);
|
|
843
|
+
console.log(` • Try specifying different ports: --port 8081 --api-port 3002`);
|
|
844
|
+
console.log(` • Check for processes using these ports: lsof -i :${port} -i :${apiPort}\n`);
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
611
847
|
}
|
|
612
848
|
|
|
613
849
|
/**
|
|
614
850
|
* Start a simple static file server
|
|
615
851
|
*/
|
|
852
|
+
/**
|
|
853
|
+
* Check if a port is available
|
|
854
|
+
* @param {number} port - Port to check
|
|
855
|
+
* @returns {Promise<boolean>} - True if port is available
|
|
856
|
+
*/
|
|
857
|
+
async function isPortAvailable(port) {
|
|
858
|
+
const { createServer } = await import("node:http");
|
|
859
|
+
return new Promise((resolve) => {
|
|
860
|
+
const server = createServer();
|
|
861
|
+
server.once('error', (err) => {
|
|
862
|
+
resolve(false);
|
|
863
|
+
});
|
|
864
|
+
server.once('listening', () => {
|
|
865
|
+
server.close();
|
|
866
|
+
resolve(true);
|
|
867
|
+
});
|
|
868
|
+
server.listen(port);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Find an available port in a range
|
|
874
|
+
* @param {number} startPort - Starting port number
|
|
875
|
+
* @param {number} maxAttempts - Maximum number of ports to try (default: 10)
|
|
876
|
+
* @returns {Promise<number|null>} - Available port or null if none found
|
|
877
|
+
*/
|
|
878
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
879
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
880
|
+
const port = startPort + i;
|
|
881
|
+
if (await isPortAvailable(port)) {
|
|
882
|
+
return port;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
|
|
616
888
|
async function startStaticFileServer(directory, port) {
|
|
617
889
|
const { createServer } = await import("node:http");
|
|
618
890
|
const { readFile, stat } = await import("node:fs/promises");
|
|
@@ -656,16 +928,50 @@ async function startStaticFileServer(directory, port) {
|
|
|
656
928
|
}
|
|
657
929
|
});
|
|
658
930
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
931
|
+
// Try to start on the requested port, or find an alternative
|
|
932
|
+
const requestedPort = port;
|
|
933
|
+
|
|
934
|
+
return new Promise(async (resolve, reject) => {
|
|
935
|
+
// First try the requested port
|
|
936
|
+
server.once('error', async (err) => {
|
|
937
|
+
if (err.code === 'EADDRINUSE') {
|
|
938
|
+
console.warn(`⚠ Port ${requestedPort} is already in use, finding alternative...`);
|
|
939
|
+
|
|
940
|
+
// Find an available port
|
|
941
|
+
const availablePort = await findAvailablePort(requestedPort + 1, 10);
|
|
942
|
+
|
|
943
|
+
if (availablePort) {
|
|
944
|
+
console.log(`✓ Using port ${availablePort} instead`);
|
|
945
|
+
server.listen(availablePort, (err) => {
|
|
946
|
+
if (err) {
|
|
947
|
+
reject(err);
|
|
948
|
+
} else {
|
|
949
|
+
resolve({
|
|
950
|
+
server,
|
|
951
|
+
port: availablePort,
|
|
952
|
+
stop: () => new Promise((res) => server.close(() => res())),
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
} else {
|
|
957
|
+
reject(new Error(`Unable to find available port. Tried ports ${requestedPort}-${requestedPort + 10}. Please close other SmartMeter instances or specify a different port with --port.`));
|
|
958
|
+
}
|
|
663
959
|
} else {
|
|
960
|
+
reject(err);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
server.listen(requestedPort, (err) => {
|
|
965
|
+
if (err && err.code !== 'EADDRINUSE') {
|
|
966
|
+
reject(err);
|
|
967
|
+
} else if (!err) {
|
|
664
968
|
resolve({
|
|
665
969
|
server,
|
|
970
|
+
port: requestedPort,
|
|
666
971
|
stop: () => new Promise((res) => server.close(() => res())),
|
|
667
972
|
});
|
|
668
973
|
}
|
|
974
|
+
// EADDRINUSE is handled by the error listener above
|
|
669
975
|
});
|
|
670
976
|
});
|
|
671
977
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
cmdEvaluate,
|
|
23
23
|
cmdGuide,
|
|
24
24
|
cmdServe,
|
|
25
|
+
cmdDiff,
|
|
26
|
+
cmdCosts,
|
|
27
|
+
cmdConfig,
|
|
28
|
+
cmdHistory,
|
|
25
29
|
} from "./commands.js";
|
|
26
30
|
|
|
27
31
|
const program = new Command();
|
|
@@ -148,4 +152,29 @@ program
|
|
|
148
152
|
open: opts.open
|
|
149
153
|
}));
|
|
150
154
|
|
|
155
|
+
program
|
|
156
|
+
.command("diff")
|
|
157
|
+
.description("Compare current config with optimized or a backup version")
|
|
158
|
+
.option("-d, --data-dir <path>", "OpenClaw data directory (default: ~/.openclaw)")
|
|
159
|
+
.option("--version <filename>", "Compare with specific backup file")
|
|
160
|
+
.action((opts) => cmdDiff({ ...dataDirOpts(opts), version: opts.version }));
|
|
161
|
+
|
|
162
|
+
program
|
|
163
|
+
.command("costs")
|
|
164
|
+
.description("Show detailed cost breakdown by model and category")
|
|
165
|
+
.option("-d, --data-dir <path>", "OpenClaw data directory (default: ~/.openclaw)")
|
|
166
|
+
.action((opts) => cmdCosts(dataDirOpts(opts)));
|
|
167
|
+
|
|
168
|
+
program
|
|
169
|
+
.command("config")
|
|
170
|
+
.description("Get or set SmartMeter configuration")
|
|
171
|
+
.argument("[key]", "Configuration key to get or set")
|
|
172
|
+
.argument("[value]", "Value to set")
|
|
173
|
+
.action((key, value) => cmdConfig({ key, value }));
|
|
174
|
+
|
|
175
|
+
program
|
|
176
|
+
.command("history")
|
|
177
|
+
.description("Show config backup/version history")
|
|
178
|
+
.action(() => cmdHistory());
|
|
179
|
+
|
|
151
180
|
program.parse();
|
|
@@ -68,9 +68,14 @@ export function generateConfig(analysis, currentConfig = {}) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// 5. Budget controls
|
|
71
|
+
// Use minimum budget values when costs are zero or very low to ensure valid config
|
|
72
|
+
const MIN_DAILY_BUDGET = 1.00; // $1/day minimum
|
|
73
|
+
const MIN_WEEKLY_BUDGET = 5.00; // $5/week minimum
|
|
74
|
+
|
|
71
75
|
const dailyAvg = (analysis.summary.currentMonthlyCost || 0) / 30;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
76
|
+
const calculatedDaily = Math.ceil(dailyAvg * 1.2 * 100) / 100;
|
|
77
|
+
const dailyBudget = Math.max(calculatedDaily, MIN_DAILY_BUDGET);
|
|
78
|
+
const weeklyBudget = Math.max(Math.ceil(dailyBudget * 7 * 100) / 100, MIN_WEEKLY_BUDGET);
|
|
74
79
|
|
|
75
80
|
config.agents.defaults.budget = deepMerge(
|
|
76
81
|
config.agents.defaults.budget || {},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const SMARTMETER_KEYS = new Set([
|
|
2
2
|
"agents",
|
|
3
3
|
"skills",
|
|
4
4
|
"models",
|
|
@@ -9,6 +9,10 @@ const ALLOWED_TOP_KEYS = new Set([
|
|
|
9
9
|
/**
|
|
10
10
|
* Validate an openclaw config object.
|
|
11
11
|
* Returns { valid: boolean, errors: string[] }.
|
|
12
|
+
*
|
|
13
|
+
* Note: This validator focuses on SmartMeter-managed fields only.
|
|
14
|
+
* It allows other OpenClaw config keys (meta, wizard, auth, tools, etc.)
|
|
15
|
+
* to pass through without validation.
|
|
12
16
|
*/
|
|
13
17
|
export function validate(config) {
|
|
14
18
|
const errors = [];
|
|
@@ -17,12 +21,8 @@ export function validate(config) {
|
|
|
17
21
|
return { valid: false, errors: ["Config must be a non-null object"] };
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
23
|
-
errors.push(`Unknown top-level key: "${key}"`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
24
|
+
// Only validate SmartMeter-managed keys, not the entire OpenClaw config
|
|
25
|
+
// This allows existing OpenClaw configs to merge without validation errors
|
|
26
26
|
|
|
27
27
|
// agents.defaults.model.primary must exist and be a string
|
|
28
28
|
const primary = config.agents?.defaults?.model?.primary;
|