openclaw-smartmeter 0.1.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/CONTRIBUTING.md +148 -0
- package/LICENSE +190 -0
- package/README.md +275 -0
- package/SECURITY.md +89 -0
- package/SKILL.md +0 -0
- package/SPEC.md +708 -0
- package/canvas-template/README.md +166 -0
- package/canvas-template/analysis.public.json +141 -0
- package/canvas-template/app.js +425 -0
- package/canvas-template/index.html +162 -0
- package/canvas-template/preview-server.py +63 -0
- package/canvas-template/styles.css +575 -0
- package/docs/backlog.md +63 -0
- package/package.json +41 -0
- package/src/analyzer/aggregator.js +256 -0
- package/src/analyzer/classifier.js +160 -0
- package/src/analyzer/parser.js +187 -0
- package/src/analyzer/recommender.js +158 -0
- package/src/analyzer/storage.js +31 -0
- package/src/canvas/deployer.js +321 -0
- package/src/cli/commands.js +267 -0
- package/src/cli/index.js +82 -0
- package/src/cli/utils.js +146 -0
- package/src/generator/agent-creator.js +61 -0
- package/src/generator/config-builder.js +163 -0
- package/src/generator/merger.js +27 -0
- package/src/generator/validator.js +54 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createAgents } from "./agent-creator.js";
|
|
2
|
+
import { deepMerge } from "./merger.js";
|
|
3
|
+
import { validate } from "./validator.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate an optimized openclaw.json config from analysis data.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} analysis - Enriched analysis from recommend()
|
|
9
|
+
* @param {object} currentConfig - Existing openclaw.json (or {} for fresh)
|
|
10
|
+
* @returns {{ config: object, validation: object, backup: object }}
|
|
11
|
+
*/
|
|
12
|
+
export function generateConfig(analysis, currentConfig = {}) {
|
|
13
|
+
const backup = structuredClone(currentConfig);
|
|
14
|
+
const comments = {};
|
|
15
|
+
|
|
16
|
+
// Start from a scaffold, then merge with existing config
|
|
17
|
+
let config = deepMerge(scaffold(), currentConfig);
|
|
18
|
+
|
|
19
|
+
// 1. Primary model optimization
|
|
20
|
+
const { primary, oldPrimary } = optimizePrimary(analysis, config);
|
|
21
|
+
config.agents.defaults.model.primary = primary;
|
|
22
|
+
if (oldPrimary && oldPrimary !== primary) {
|
|
23
|
+
comments["agents.defaults.model.primary"] =
|
|
24
|
+
`SMARTMETER: Changed from ${oldPrimary}. ` +
|
|
25
|
+
`Saves ~$${analysis.summary.potentialSavings}/month`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Specialized agents
|
|
29
|
+
const agents = createAgents(analysis);
|
|
30
|
+
for (const [name, agentConfig] of Object.entries(agents)) {
|
|
31
|
+
config.agents[name] = agentConfig;
|
|
32
|
+
comments[`agents.${name}`] =
|
|
33
|
+
`SMARTMETER: Auto-created for ${name} workload`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Skill optimization (no-op while skills are stubbed)
|
|
37
|
+
const usedSkillNames = Object.keys(analysis.skills.used || {});
|
|
38
|
+
if (usedSkillNames.length > 0 && usedSkillNames.length < 20) {
|
|
39
|
+
config.skills = {
|
|
40
|
+
allowBundled: false,
|
|
41
|
+
allow: usedSkillNames,
|
|
42
|
+
};
|
|
43
|
+
const unusedCount = (analysis.skills.unused || []).length;
|
|
44
|
+
comments["skills"] =
|
|
45
|
+
`SMARTMETER: Disabled ${unusedCount} unused skills. ` +
|
|
46
|
+
`Saves ~${unusedCount * 200} tokens/request`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 4. Caching configuration
|
|
50
|
+
const patterns = analysis.temporal?.patterns || {};
|
|
51
|
+
if (patterns.burstUsage) {
|
|
52
|
+
const primaryModel = config.agents.defaults.model.primary;
|
|
53
|
+
config.models = config.models || {};
|
|
54
|
+
config.models[primaryModel] = deepMerge(
|
|
55
|
+
config.models[primaryModel] || {},
|
|
56
|
+
{ params: { cacheRetention: "long" } },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (patterns.quietHours?.length > 0) {
|
|
60
|
+
const quietRange = patterns.quietHours[0]; // e.g. "00-06"
|
|
61
|
+
config.heartbeat = {
|
|
62
|
+
every: "55m",
|
|
63
|
+
schedule: quietRange,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
comments["models"] = "SMARTMETER: Long cache retention for burst usage";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 5. Budget controls
|
|
71
|
+
const dailyAvg = (analysis.summary.currentMonthlyCost || 0) / 30;
|
|
72
|
+
const dailyBudget = Math.ceil(dailyAvg * 1.2 * 100) / 100;
|
|
73
|
+
const weeklyBudget = Math.ceil(dailyBudget * 7 * 100) / 100;
|
|
74
|
+
|
|
75
|
+
config.agents.defaults.budget = deepMerge(
|
|
76
|
+
config.agents.defaults.budget || {},
|
|
77
|
+
{
|
|
78
|
+
daily: dailyBudget,
|
|
79
|
+
weekly: weeklyBudget,
|
|
80
|
+
alert: { telegram: true, threshold: 0.75 },
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 6. Fallback chain
|
|
85
|
+
const fallback = buildFallbackChain(
|
|
86
|
+
analysis.models,
|
|
87
|
+
config.agents.defaults.model.primary,
|
|
88
|
+
);
|
|
89
|
+
if (fallback.length > 0) {
|
|
90
|
+
config.agents.defaults.model.fallback = fallback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Metadata
|
|
94
|
+
config._smartmeter = {
|
|
95
|
+
generatedAt: new Date().toISOString(),
|
|
96
|
+
analysisperiod: analysis.period,
|
|
97
|
+
comments,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const validation = validate(config);
|
|
101
|
+
|
|
102
|
+
return { config, validation, backup };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Internals ---
|
|
106
|
+
|
|
107
|
+
function scaffold() {
|
|
108
|
+
return {
|
|
109
|
+
agents: {
|
|
110
|
+
defaults: {
|
|
111
|
+
model: { primary: "unknown" },
|
|
112
|
+
budget: {},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Determine the optimal primary model.
|
|
120
|
+
* Strategy: find the dominant category (most tasks), use its recommendation.
|
|
121
|
+
*/
|
|
122
|
+
function optimizePrimary(analysis, config) {
|
|
123
|
+
const oldPrimary = config.agents?.defaults?.model?.primary;
|
|
124
|
+
|
|
125
|
+
// Find the category with the most tasks that has a recommendation
|
|
126
|
+
let bestCategory = null;
|
|
127
|
+
let bestCount = 0;
|
|
128
|
+
for (const [name, cat] of Object.entries(analysis.categories || {})) {
|
|
129
|
+
if (cat.recommendation && cat.count > bestCount) {
|
|
130
|
+
bestCount = cat.count;
|
|
131
|
+
bestCategory = cat;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (bestCategory?.recommendation?.confidence >= 0.7) {
|
|
136
|
+
return {
|
|
137
|
+
primary: bestCategory.recommendation.optimalModel,
|
|
138
|
+
oldPrimary,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// No strong recommendation — keep existing or pick the most-used model
|
|
143
|
+
if (oldPrimary && oldPrimary !== "unknown") {
|
|
144
|
+
return { primary: oldPrimary, oldPrimary: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const models = Object.entries(analysis.models || {});
|
|
148
|
+
if (models.length === 0) return { primary: "unknown", oldPrimary: null };
|
|
149
|
+
|
|
150
|
+
models.sort((a, b) => b[1].count - a[1].count);
|
|
151
|
+
return { primary: models[0][0], oldPrimary: null };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build fallback chain: all models sorted by avgCostPerTask ascending,
|
|
156
|
+
* excluding the primary.
|
|
157
|
+
*/
|
|
158
|
+
function buildFallbackChain(models, primary) {
|
|
159
|
+
return Object.entries(models || {})
|
|
160
|
+
.filter(([name]) => name !== primary)
|
|
161
|
+
.sort((a, b) => a[1].avgCostPerTask - b[1].avgCostPerTask)
|
|
162
|
+
.map(([name]) => name);
|
|
163
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep merge source into target. Returns a new object.
|
|
3
|
+
* - Objects are recursively merged
|
|
4
|
+
* - Arrays from source replace target arrays
|
|
5
|
+
* - Primitives from source overwrite target
|
|
6
|
+
* - Neither input is mutated
|
|
7
|
+
*/
|
|
8
|
+
export function deepMerge(target, source) {
|
|
9
|
+
const result = { ...target };
|
|
10
|
+
|
|
11
|
+
for (const key of Object.keys(source)) {
|
|
12
|
+
const srcVal = source[key];
|
|
13
|
+
const tgtVal = result[key];
|
|
14
|
+
|
|
15
|
+
if (isPlainObject(srcVal) && isPlainObject(tgtVal)) {
|
|
16
|
+
result[key] = deepMerge(tgtVal, srcVal);
|
|
17
|
+
} else {
|
|
18
|
+
result[key] = structuredClone(srcVal);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPlainObject(val) {
|
|
26
|
+
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const ALLOWED_TOP_KEYS = new Set([
|
|
2
|
+
"agents",
|
|
3
|
+
"skills",
|
|
4
|
+
"models",
|
|
5
|
+
"heartbeat",
|
|
6
|
+
"_smartmeter",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate an openclaw config object.
|
|
11
|
+
* Returns { valid: boolean, errors: string[] }.
|
|
12
|
+
*/
|
|
13
|
+
export function validate(config) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
|
|
16
|
+
if (config == null || typeof config !== "object" || Array.isArray(config)) {
|
|
17
|
+
return { valid: false, errors: ["Config must be a non-null object"] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check top-level keys
|
|
21
|
+
for (const key of Object.keys(config)) {
|
|
22
|
+
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
23
|
+
errors.push(`Unknown top-level key: "${key}"`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// agents.defaults.model.primary must exist and be a string
|
|
28
|
+
const primary = config.agents?.defaults?.model?.primary;
|
|
29
|
+
if (primary === undefined) {
|
|
30
|
+
errors.push("Missing agents.defaults.model.primary");
|
|
31
|
+
} else if (typeof primary !== "string") {
|
|
32
|
+
errors.push("agents.defaults.model.primary must be a string");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Budget validation (if present)
|
|
36
|
+
const budget = config.agents?.defaults?.budget;
|
|
37
|
+
if (budget) {
|
|
38
|
+
if (budget.daily !== undefined && (typeof budget.daily !== "number" || budget.daily <= 0)) {
|
|
39
|
+
errors.push("agents.defaults.budget.daily must be a positive number");
|
|
40
|
+
}
|
|
41
|
+
if (budget.weekly !== undefined && (typeof budget.weekly !== "number" || budget.weekly <= 0)) {
|
|
42
|
+
errors.push("agents.defaults.budget.weekly must be a positive number");
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
typeof budget.daily === "number" &&
|
|
46
|
+
typeof budget.weekly === "number" &&
|
|
47
|
+
budget.weekly < budget.daily
|
|
48
|
+
) {
|
|
49
|
+
errors.push("agents.defaults.budget.weekly must be >= daily");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { valid: errors.length === 0, errors };
|
|
54
|
+
}
|