cue-ai 0.5.0 → 0.7.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/README.md +757 -110
- package/package.json +5 -5
- package/profiles/README.md +12 -12
- package/profiles/SCHEMA.md +31 -3
- package/profiles/_cache/README.md +1 -1
- package/profiles/_types.ts +26 -1
- package/profiles/backend/profile.yaml +1 -0
- package/profiles/career/profile.yaml +13 -0
- package/profiles/core/profile.yaml +76 -9
- package/profiles/creative-media/README.md +1 -1
- package/profiles/cybersecurity/profile.yaml +779 -756
- package/profiles/ecc/profile.yaml +39 -0
- package/profiles/event-design/profile.yaml +10 -0
- package/profiles/fleet-control/README.md +1 -1
- package/profiles/frontend/profile.yaml +14 -0
- package/profiles/full/README.md +1 -1
- package/profiles/go-api/profile.yaml +1 -0
- package/profiles/marketing/profile.yaml +12 -1
- package/profiles/predict-everything/profile.yaml +9 -0
- package/profiles/rust/profile.yaml +22 -3
- package/profiles/rust-cli/profile.yaml +14 -0
- package/profiles/rust-core/profile.yaml +35 -0
- package/profiles/rust-embedded/profile.yaml +11 -0
- package/profiles/rust-ffi/profile.yaml +13 -0
- package/profiles/rust-game/profile.yaml +11 -0
- package/profiles/rust-wasm/profile.yaml +11 -0
- package/profiles/rust-web/profile.yaml +17 -0
- package/profiles/schema.json +44 -4
- package/profiles/trendradar/profile.yaml +11 -0
- package/resources/mcps/README.md +39 -164
- package/resources/mcps/configs/claude.sanitized.json +55 -0
- package/resources/mcps/configs/claude_runtime.sanitized.json +47 -0
- package/resources/skills/README.md +70 -113
- package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +43 -0
- package/resources/skills/skills/meta/acpx/SKILL.md +78 -0
- package/resources/skills/skills/meta/cue-usage/SKILL.md +24 -0
- package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +24 -0
- package/resources/skills/skills/predict-everything/mirofish/SKILL.md +75 -0
- package/resources/skills/skills/research/trendradar/SKILL.md +88 -0
- package/resources/skills/skills/rust/async-tokio/SKILL.md +27 -0
- package/resources/skills/skills/rust/axum-api/SKILL.md +38 -0
- package/resources/skills/skills/rust/bacon-watch/SKILL.md +24 -0
- package/resources/skills/skills/rust/bevy/SKILL.md +43 -0
- package/resources/skills/skills/rust/bindgen/SKILL.md +39 -0
- package/resources/skills/skills/rust/cargo-audit/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-basics/SKILL.md +28 -0
- package/resources/skills/skills/rust/cargo-chef/SKILL.md +43 -0
- package/resources/skills/skills/rust/cargo-edit/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-expand/SKILL.md +24 -0
- package/resources/skills/skills/rust/cargo-flamegraph/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-fuzz/SKILL.md +34 -0
- package/resources/skills/skills/rust/cargo-hack/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-msrv/SKILL.md +30 -0
- package/resources/skills/skills/rust/cargo-mutants/SKILL.md +26 -0
- package/resources/skills/skills/rust/cargo-nextest/SKILL.md +24 -0
- package/resources/skills/skills/rust/cargo-readme/SKILL.md +36 -0
- package/resources/skills/skills/rust/cbindgen/SKILL.md +41 -0
- package/resources/skills/skills/rust/chisel-tool/SKILL.md +32 -0
- package/resources/skills/skills/rust/clap-cli/SKILL.md +44 -0
- package/resources/skills/skills/rust/clippy-and-fmt/SKILL.md +25 -0
- package/resources/skills/skills/rust/cross-compile/SKILL.md +26 -0
- package/resources/skills/skills/rust/embedded/SKILL.md +33 -0
- package/resources/skills/skills/rust/error-handling/SKILL.md +32 -0
- package/resources/skills/skills/rust/just-runner/SKILL.md +26 -0
- package/resources/skills/skills/rust/mdbook/SKILL.md +25 -0
- package/resources/skills/skills/rust/napi-rs/SKILL.md +32 -0
- package/resources/skills/skills/rust/no-std/SKILL.md +42 -0
- package/resources/skills/skills/rust/property-testing/SKILL.md +35 -0
- package/resources/skills/skills/rust/pyo3/SKILL.md +40 -0
- package/resources/skills/skills/rust/ratatui-tui/SKILL.md +36 -0
- package/resources/skills/skills/rust/release-plz/SKILL.md +27 -0
- package/resources/skills/skills/rust/reqwest/SKILL.md +37 -0
- package/resources/skills/skills/rust/sccache/SKILL.md +28 -0
- package/resources/skills/skills/rust/serde/SKILL.md +30 -0
- package/resources/skills/skills/rust/snapshot-testing/SKILL.md +30 -0
- package/resources/skills/skills/rust/sqlx-cli/SKILL.md +33 -0
- package/resources/skills/skills/rust/tracing/SKILL.md +36 -0
- package/resources/skills/skills/rust/typos-spellcheck/SKILL.md +31 -0
- package/resources/skills/skills/rust/uniffi/SKILL.md +38 -0
- package/resources/skills/skills/rust/wasm-rust/SKILL.md +27 -0
- package/resources/skills/skills/security/agentshield/SKILL.md +119 -0
- package/src/commands/_index.ts +47 -3
- package/src/commands/cli.test.ts +192 -0
- package/src/commands/cli.ts +303 -0
- package/src/commands/current.ts +1 -1
- package/src/commands/debug.test.ts +62 -0
- package/src/commands/debug.ts +212 -0
- package/src/commands/discover.scoring.test.ts +216 -0
- package/src/commands/discover.test.ts +145 -0
- package/src/commands/discover.ts +2618 -0
- package/src/commands/eval-behavior.test.ts +56 -0
- package/src/commands/eval-behavior.ts +189 -0
- package/src/commands/eval.test.ts +102 -0
- package/src/commands/eval.ts +348 -0
- package/src/commands/evolve.ts +291 -0
- package/src/commands/failures.test.ts +78 -0
- package/src/commands/failures.ts +393 -0
- package/src/commands/feedback.ts +219 -0
- package/src/commands/init.ts +26 -0
- package/src/commands/launch.e2e.test.ts +9 -1
- package/src/commands/launch.ts +174 -11
- package/src/commands/lint-skill.ts +157 -0
- package/src/commands/marketplace.ts +763 -2
- package/src/commands/new.ts +1 -1
- package/src/commands/optimizer.ts +92 -28
- package/src/commands/profile-draft-skill.test.ts +96 -0
- package/src/commands/profile-draft-skill.ts +287 -0
- package/src/commands/profile-evolve.test.ts +126 -0
- package/src/commands/profile-evolve.ts +0 -0
- package/src/commands/profile-suggest.ts +223 -0
- package/src/commands/profile.ts +41 -0
- package/src/commands/quick.ts +2 -17
- package/src/commands/scan.ts +2 -2
- package/src/commands/score.ts +1 -1
- package/src/commands/share.ts +1 -1
- package/src/commands/sources.ts +2 -2
- package/src/commands/submit-profile.ts +262 -0
- package/src/commands/upgrade.ts +1 -1
- package/src/commands/use.ts +35 -5
- package/src/commands/validate.ts +1 -1
- package/src/index.ts +66 -0
- package/src/lib/analytics.ts +48 -2
- package/src/lib/claude-binary.ts +39 -0
- package/src/lib/cli-extractor.ts +77 -0
- package/src/lib/cluster-skills.test.ts +268 -0
- package/src/lib/cluster-skills.ts +290 -0
- package/src/lib/credentials-sync.test.ts +208 -0
- package/src/lib/credentials-sync.ts +205 -0
- package/src/lib/mcp-materializer.test.ts +1 -1
- package/src/lib/persona-playbooks.test.ts +111 -0
- package/src/lib/pr-poster.test.ts +243 -0
- package/src/lib/pr-poster.ts +285 -0
- package/src/lib/pr-throttle.test.ts +148 -0
- package/src/lib/pr-throttle.ts +209 -0
- package/src/lib/profile-generator.test.ts +1 -1
- package/src/lib/profile-generator.ts +2 -2
- package/src/lib/profile-linter.test.ts +6 -3
- package/src/lib/profile-linter.ts +71 -8
- package/src/lib/profile-loader.test.ts +1 -1
- package/src/lib/profile-loader.ts +16 -0
- package/src/lib/resolver-local.test.ts +1 -1
- package/src/lib/resolver-npx.test.ts +76 -1
- package/src/lib/resolver-npx.ts +35 -3
- package/src/lib/resolver-plugins.test.ts +1 -1
- package/src/lib/runtime-materializer.test.ts +191 -7
- package/src/lib/runtime-materializer.ts +310 -42
- package/src/lib/scan-plugins.test.ts +1 -1
- package/src/lib/skill-linter.test.ts +174 -0
- package/src/lib/skill-linter.ts +507 -0
- package/src/lib/skill-subset.test.ts +95 -0
- package/src/lib/skill-subset.ts +166 -0
- package/src/lib/star-prompt.ts +1 -1
- package/src/lib/uvx-installer.test.ts +229 -0
- package/src/lib/uvx-installer.ts +278 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cue eval [profile] [--breakdown] [--compare a b] [--json]`
|
|
3
|
+
*
|
|
4
|
+
* Measures the per-message token overhead a profile drops into context.
|
|
5
|
+
*
|
|
6
|
+
* Honest math (after the followup refactor):
|
|
7
|
+
* - **perMessage**: what Claude actually sees on every turn — skill
|
|
8
|
+
* descriptions (frontmatter only), the list of rule/command names from the
|
|
9
|
+
* CLAUDE.md stamp, and a tiny constant for hooks (the matcher block in
|
|
10
|
+
* settings.json, NOT the script body which runs server-side).
|
|
11
|
+
* - **onDemand**: lazy-loaded bodies — full skill content, full rule files,
|
|
12
|
+
* full command files. Read only when the model invokes them.
|
|
13
|
+
*
|
|
14
|
+
* The old combined "total" lives on as `bytesOnDisk` for context, but the
|
|
15
|
+
* cost-per-message and the score now use perMessage so they reflect reality.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { resolve, join, dirname, basename, isAbsolute } from "node:path";
|
|
19
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
|
|
23
|
+
import { loadProfile, listProfiles } from "../lib/profile-loader";
|
|
24
|
+
import { resolveProfileForCwd } from "../lib/cwd-resolver";
|
|
25
|
+
import { computeStats } from "../lib/analytics";
|
|
26
|
+
import type { ResolvedProfile } from "../../profiles/_types";
|
|
27
|
+
|
|
28
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
29
|
+
const SKILLS_ROOT = join(REPO_ROOT, "resources", "skills", "skills");
|
|
30
|
+
const RULES_ROOT = join(REPO_ROOT, "resources", "rules");
|
|
31
|
+
const COMMANDS_ROOT = join(REPO_ROOT, "resources", "commands");
|
|
32
|
+
const HOOKS_ROOT = join(REPO_ROOT, "resources", "hooks");
|
|
33
|
+
|
|
34
|
+
// Approximate fixed per-message cost of one hook entry in settings.json
|
|
35
|
+
// (matcher + command path + description). Hook scripts themselves never enter
|
|
36
|
+
// the model's context.
|
|
37
|
+
const HOOK_PER_MSG_TOKENS = 30;
|
|
38
|
+
|
|
39
|
+
interface Bucket {
|
|
40
|
+
perMessage: number;
|
|
41
|
+
onDemand: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Breakdown {
|
|
45
|
+
skills: Bucket;
|
|
46
|
+
rules: Bucket;
|
|
47
|
+
commands: Bucket;
|
|
48
|
+
hooks: Bucket;
|
|
49
|
+
perMessageTotal: number;
|
|
50
|
+
onDemandTotal: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bytesToTokens(n: number): number { return Math.ceil(n / 4); }
|
|
54
|
+
|
|
55
|
+
function readFileSafe(path: string): string | null {
|
|
56
|
+
try { return readFileSync(path, "utf8"); } catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* SKILL.md description: yaml frontmatter at the top. That's all the model
|
|
61
|
+
* needs to know whether to invoke the skill. The rest is read on demand.
|
|
62
|
+
*/
|
|
63
|
+
function skillDescriptionTokens(skillId: string): { perMsg: number; onDemand: number } {
|
|
64
|
+
const body = readFileSafe(join(SKILLS_ROOT, skillId, "SKILL.md"));
|
|
65
|
+
if (!body) return { perMsg: 0, onDemand: 0 };
|
|
66
|
+
// Frontmatter is between the first two `---` lines.
|
|
67
|
+
const fm = body.match(/^---\n([\s\S]*?)\n---/);
|
|
68
|
+
const perMsg = fm ? bytesToTokens(fm[0].length) : bytesToTokens(Math.min(body.length, 200));
|
|
69
|
+
const onDemand = Math.max(0, bytesToTokens(body.length) - perMsg);
|
|
70
|
+
return { perMsg, onDemand };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveRef(ref: string, base: string, addExt: boolean): string {
|
|
74
|
+
const withExt = addExt && !ref.endsWith(".md") ? `${ref}.md` : ref;
|
|
75
|
+
return isAbsolute(withExt) ? withExt : join(base, withExt);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Per-rule and per-command per-message cost: just the index line we write
|
|
80
|
+
* into CLAUDE.md (e.g. `- \`rules/security.md\``). Conservative estimate.
|
|
81
|
+
*/
|
|
82
|
+
const RULE_INDEX_LINE_TOKENS = 12;
|
|
83
|
+
const COMMAND_INDEX_LINE_TOKENS = 6;
|
|
84
|
+
|
|
85
|
+
function computeBreakdown(p: ResolvedProfile): Breakdown {
|
|
86
|
+
let skillsPerMsg = 0, skillsOnDemand = 0;
|
|
87
|
+
for (const s of p.skills.local) {
|
|
88
|
+
if (s.id.includes("*")) continue;
|
|
89
|
+
const { perMsg, onDemand } = skillDescriptionTokens(s.id);
|
|
90
|
+
skillsPerMsg += perMsg;
|
|
91
|
+
skillsOnDemand += onDemand;
|
|
92
|
+
}
|
|
93
|
+
let rulesPerMsg = 0, rulesOnDemand = 0;
|
|
94
|
+
for (const r of (p.rules ?? [])) {
|
|
95
|
+
rulesPerMsg += RULE_INDEX_LINE_TOKENS;
|
|
96
|
+
const body = readFileSafe(resolveRef(r, RULES_ROOT, true));
|
|
97
|
+
if (body) rulesOnDemand += bytesToTokens(body.length);
|
|
98
|
+
}
|
|
99
|
+
let cmdsPerMsg = 0, cmdsOnDemand = 0;
|
|
100
|
+
for (const c of (p.commands ?? [])) {
|
|
101
|
+
cmdsPerMsg += COMMAND_INDEX_LINE_TOKENS;
|
|
102
|
+
const body = readFileSafe(resolveRef(c, COMMANDS_ROOT, true));
|
|
103
|
+
if (body) cmdsOnDemand += bytesToTokens(body.length);
|
|
104
|
+
}
|
|
105
|
+
let hooksPerMsg = 0, hooksOnDemand = 0;
|
|
106
|
+
for (const h of (p.hooks ?? [])) {
|
|
107
|
+
hooksPerMsg += HOOK_PER_MSG_TOKENS;
|
|
108
|
+
const body = readFileSafe(resolveRef(h, HOOKS_ROOT, false));
|
|
109
|
+
if (body) hooksOnDemand += bytesToTokens(body.length);
|
|
110
|
+
}
|
|
111
|
+
const perMessageTotal = skillsPerMsg + rulesPerMsg + cmdsPerMsg + hooksPerMsg;
|
|
112
|
+
const onDemandTotal = skillsOnDemand + rulesOnDemand + cmdsOnDemand + hooksOnDemand;
|
|
113
|
+
return {
|
|
114
|
+
skills: { perMessage: skillsPerMsg, onDemand: skillsOnDemand },
|
|
115
|
+
rules: { perMessage: rulesPerMsg, onDemand: rulesOnDemand },
|
|
116
|
+
commands: { perMessage: cmdsPerMsg, onDemand: cmdsOnDemand },
|
|
117
|
+
hooks: { perMessage: hooksPerMsg, onDemand: hooksOnDemand },
|
|
118
|
+
perMessageTotal, onDemandTotal,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
123
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
124
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
125
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
126
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
127
|
+
|
|
128
|
+
function fmtTok(n: number): string {
|
|
129
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : `${n}`;
|
|
130
|
+
}
|
|
131
|
+
function cost(n: number): string { return `$${((n / 1000) * 0.003).toFixed(4)}`; }
|
|
132
|
+
|
|
133
|
+
function scoreOf(p: ResolvedProfile, b: Breakdown, fullPerMsg: number, sessions: number): number {
|
|
134
|
+
const savings = fullPerMsg > 0 ? Math.max(0, Math.round((1 - b.perMessageTotal / fullPerMsg) * 100)) : 0;
|
|
135
|
+
return Math.min(100, Math.round(
|
|
136
|
+
(savings * 0.4) +
|
|
137
|
+
(Math.min(sessions, 20) / 20 * 30) +
|
|
138
|
+
(p.mcps.length > 0 ? 15 : 0) +
|
|
139
|
+
(p.plugins.length > 0 ? 15 : 0)
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function grade(score: number): { letter: string; color: (s: string) => string } {
|
|
144
|
+
if (score >= 90) return { letter: "A", color: green };
|
|
145
|
+
if (score >= 75) return { letter: "B", color: green };
|
|
146
|
+
if (score >= 60) return { letter: "C", color: yellow };
|
|
147
|
+
if (score >= 40) return { letter: "D", color: yellow };
|
|
148
|
+
return { letter: "F", color: red };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function fullProfilePerMessage(): Promise<number> {
|
|
152
|
+
try {
|
|
153
|
+
const full = await loadProfile("full");
|
|
154
|
+
return computeBreakdown(full).perMessageTotal;
|
|
155
|
+
} catch { return 0; }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sessionsFor(name: string): number {
|
|
159
|
+
return computeStats().find((s) => s.profile === name)?.sessions ?? 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function renderOne(name: string, showBreakdown: boolean, asJson: boolean): Promise<number> {
|
|
163
|
+
const profile = await loadProfile(name);
|
|
164
|
+
const b = computeBreakdown(profile);
|
|
165
|
+
const sessions = sessionsFor(name);
|
|
166
|
+
const fullPerMsg = await fullProfilePerMessage();
|
|
167
|
+
const savings = fullPerMsg > 0 ? Math.max(0, Math.round((1 - b.perMessageTotal / fullPerMsg) * 100)) : 0;
|
|
168
|
+
const score = scoreOf(profile, b, fullPerMsg, sessions);
|
|
169
|
+
const g = grade(score);
|
|
170
|
+
|
|
171
|
+
if (asJson) {
|
|
172
|
+
process.stdout.write(JSON.stringify({
|
|
173
|
+
profile: name,
|
|
174
|
+
counts: {
|
|
175
|
+
skills: profile.skills.local.length + profile.skills.npx.length,
|
|
176
|
+
rules: (profile.rules ?? []).length,
|
|
177
|
+
commands: (profile.commands ?? []).length,
|
|
178
|
+
hooks: (profile.hooks ?? []).length,
|
|
179
|
+
mcps: profile.mcps.length,
|
|
180
|
+
plugins: profile.plugins.length,
|
|
181
|
+
},
|
|
182
|
+
tokens: {
|
|
183
|
+
perMessage: b.perMessageTotal,
|
|
184
|
+
onDemand: b.onDemandTotal,
|
|
185
|
+
bySource: {
|
|
186
|
+
skills: b.skills,
|
|
187
|
+
rules: b.rules,
|
|
188
|
+
commands: b.commands,
|
|
189
|
+
hooks: b.hooks,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
fullPerMessage: fullPerMsg,
|
|
193
|
+
savingsPct: savings,
|
|
194
|
+
costPerMessage: cost(b.perMessageTotal),
|
|
195
|
+
sessions,
|
|
196
|
+
score,
|
|
197
|
+
grade: g.letter,
|
|
198
|
+
}, null, 2) + "\n");
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
process.stdout.write(`\n ${bold("Profile Eval:")} ${name}\n\n`);
|
|
203
|
+
process.stdout.write(` ${bold("Loadout")}\n`);
|
|
204
|
+
process.stdout.write(` Skills: ${profile.skills.local.length} Rules: ${(profile.rules ?? []).length} Commands: ${(profile.commands ?? []).length} Hooks: ${(profile.hooks ?? []).length} MCPs: ${profile.mcps.length} Plugins: ${profile.plugins.length}\n`);
|
|
205
|
+
process.stdout.write(` Per-message: ${green(fmtTok(b.perMessageTotal))} tokens (${cost(b.perMessageTotal)}/msg)\n`);
|
|
206
|
+
process.stdout.write(` On-demand: ${dim(fmtTok(b.onDemandTotal) + " tokens (lazy — only when invoked)")}\n\n`);
|
|
207
|
+
|
|
208
|
+
if (showBreakdown) {
|
|
209
|
+
process.stdout.write(` ${bold("Breakdown (per-message tokens)")}\n`);
|
|
210
|
+
const rows: [string, Bucket][] = [
|
|
211
|
+
["skills", b.skills],
|
|
212
|
+
["rules", b.rules],
|
|
213
|
+
["commands", b.commands],
|
|
214
|
+
["hooks", b.hooks],
|
|
215
|
+
];
|
|
216
|
+
const max = Math.max(1, ...rows.map(([, v]) => v.perMessage));
|
|
217
|
+
for (const [label, bucket] of rows) {
|
|
218
|
+
const pct = b.perMessageTotal > 0 ? Math.round((bucket.perMessage / b.perMessageTotal) * 100) : 0;
|
|
219
|
+
const bar = "█".repeat(Math.round((bucket.perMessage / max) * 20));
|
|
220
|
+
const ondem = bucket.onDemand > 0 ? dim(` (+${fmtTok(bucket.onDemand)} on-demand)`) : "";
|
|
221
|
+
process.stdout.write(` ${label.padEnd(9)} ${fmtTok(bucket.perMessage).padStart(6)} ${dim(`${pct}%`)} ${bar}${ondem}\n`);
|
|
222
|
+
}
|
|
223
|
+
process.stdout.write(` ${dim("on-demand bodies stay resident for the rest of the session once read")}\n\n`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
process.stdout.write(` ${bold("Efficiency vs full")}\n`);
|
|
227
|
+
process.stdout.write(` This: ${fmtTok(b.perMessageTotal)} Full: ${fmtTok(fullPerMsg)} ${green(`Savings: ${savings}%`)}\n\n`);
|
|
228
|
+
|
|
229
|
+
process.stdout.write(` ${bold("Usage")} Sessions: ${sessions}\n`);
|
|
230
|
+
process.stdout.write(`\n ${bold("Score:")} ${g.color(`${score}/100 (${g.letter})`)}\n`);
|
|
231
|
+
process.stdout.write(` ${dim("40% savings + 30% usage + 15% MCPs + 15% plugins")}\n\n`);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function renderCompare(a: string, b: string, asJson: boolean): Promise<number> {
|
|
236
|
+
const [pa, pb] = await Promise.all([loadProfile(a), loadProfile(b)]);
|
|
237
|
+
const [ba, bb] = [computeBreakdown(pa), computeBreakdown(pb)];
|
|
238
|
+
const fullPerMsg = await fullProfilePerMessage();
|
|
239
|
+
const [sa, sb] = [sessionsFor(a), sessionsFor(b)];
|
|
240
|
+
const [scA, scB] = [scoreOf(pa, ba, fullPerMsg, sa), scoreOf(pb, bb, fullPerMsg, sb)];
|
|
241
|
+
|
|
242
|
+
if (asJson) {
|
|
243
|
+
process.stdout.write(JSON.stringify({
|
|
244
|
+
a: { profile: a, tokens: { perMessage: ba.perMessageTotal, onDemand: ba.onDemandTotal }, sessions: sa, score: scA },
|
|
245
|
+
b: { profile: b, tokens: { perMessage: bb.perMessageTotal, onDemand: bb.onDemandTotal }, sessions: sb, score: scB },
|
|
246
|
+
delta: { perMessage: bb.perMessageTotal - ba.perMessageTotal, score: scB - scA },
|
|
247
|
+
}, null, 2) + "\n");
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fmtRow = (label: string, va: string, vb: string) =>
|
|
252
|
+
` ${label.padEnd(14)} ${va.padStart(10)} ${vb.padStart(10)}\n`;
|
|
253
|
+
|
|
254
|
+
process.stdout.write(`\n ${bold("Compare:")} ${a} vs ${b}\n\n`);
|
|
255
|
+
process.stdout.write(` ${"".padEnd(14)} ${a.padStart(10)} ${b.padStart(10)}\n`);
|
|
256
|
+
process.stdout.write(` ${"".padEnd(14)} ${"-".repeat(10)} ${"-".repeat(10)}\n`);
|
|
257
|
+
process.stdout.write(fmtRow("skills", String(pa.skills.local.length), String(pb.skills.local.length)));
|
|
258
|
+
process.stdout.write(fmtRow("rules", String((pa.rules ?? []).length), String((pb.rules ?? []).length)));
|
|
259
|
+
process.stdout.write(fmtRow("commands", String((pa.commands ?? []).length), String((pb.commands ?? []).length)));
|
|
260
|
+
process.stdout.write(fmtRow("hooks", String((pa.hooks ?? []).length), String((pb.hooks ?? []).length)));
|
|
261
|
+
process.stdout.write(fmtRow("mcps", String(pa.mcps.length), String(pb.mcps.length)));
|
|
262
|
+
process.stdout.write(fmtRow("per-msg", fmtTok(ba.perMessageTotal), fmtTok(bb.perMessageTotal)));
|
|
263
|
+
process.stdout.write(fmtRow("on-demand", fmtTok(ba.onDemandTotal), fmtTok(bb.onDemandTotal)));
|
|
264
|
+
process.stdout.write(fmtRow("cost/msg", cost(ba.perMessageTotal), cost(bb.perMessageTotal)));
|
|
265
|
+
process.stdout.write(fmtRow("sessions", String(sa), String(sb)));
|
|
266
|
+
const ga = grade(scA), gb = grade(scB);
|
|
267
|
+
process.stdout.write(fmtRow("score", ga.color(`${scA} (${ga.letter})`), gb.color(`${scB} (${gb.letter})`)));
|
|
268
|
+
const delta = bb.perMessageTotal - ba.perMessageTotal;
|
|
269
|
+
const arrow = delta > 0 ? red(`+${fmtTok(delta)}`) : delta < 0 ? green(`-${fmtTok(-delta)}`) : dim("0");
|
|
270
|
+
process.stdout.write(`\n ${dim(`${b} uses ${arrow} tokens per message vs ${a}`)}\n\n`);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function renderAll(asJson: boolean): Promise<number> {
|
|
275
|
+
const names = await listProfiles();
|
|
276
|
+
const fullPerMsg = await fullProfilePerMessage();
|
|
277
|
+
const rows = await Promise.all(names.map(async (n) => {
|
|
278
|
+
try {
|
|
279
|
+
const p = await loadProfile(n);
|
|
280
|
+
const b = computeBreakdown(p);
|
|
281
|
+
const sessions = sessionsFor(n);
|
|
282
|
+
const score = scoreOf(p, b, fullPerMsg, sessions);
|
|
283
|
+
return { name: n, perMessage: b.perMessageTotal, onDemand: b.onDemandTotal, sessions, score, ok: true as const };
|
|
284
|
+
} catch (e) {
|
|
285
|
+
return { name: n, perMessage: 0, onDemand: 0, sessions: 0, score: 0, ok: false as const, error: String(e) };
|
|
286
|
+
}
|
|
287
|
+
}));
|
|
288
|
+
// Sort by per-message ascending — leanest first.
|
|
289
|
+
rows.sort((a, b) => a.perMessage - b.perMessage);
|
|
290
|
+
|
|
291
|
+
if (asJson) {
|
|
292
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
process.stdout.write(`\n ${bold("All Profiles")} (sorted by per-message tokens)\n\n`);
|
|
297
|
+
process.stdout.write(` ${"profile".padEnd(20)} ${"per-msg".padStart(8)} ${"on-demand".padStart(10)} ${"sessions".padStart(8)} ${"score".padStart(7)}\n`);
|
|
298
|
+
process.stdout.write(` ${"-".repeat(20)} ${"-".repeat(8)} ${"-".repeat(10)} ${"-".repeat(8)} ${"-".repeat(7)}\n`);
|
|
299
|
+
for (const r of rows) {
|
|
300
|
+
if (!r.ok) {
|
|
301
|
+
process.stdout.write(` ${r.name.padEnd(20)} ${red("error".padStart(8))}\n`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const g = grade(r.score);
|
|
305
|
+
process.stdout.write(
|
|
306
|
+
` ${r.name.padEnd(20)} ` +
|
|
307
|
+
`${fmtTok(r.perMessage).padStart(8)} ` +
|
|
308
|
+
`${dim(fmtTok(r.onDemand).padStart(10))} ` +
|
|
309
|
+
`${String(r.sessions).padStart(8)} ` +
|
|
310
|
+
`${g.color(`${r.score} (${g.letter})`.padStart(7))}\n`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
process.stdout.write("\n");
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function run(args: string[]): Promise<number> {
|
|
318
|
+
const asJson = args.includes("--json");
|
|
319
|
+
const breakdown = args.includes("--breakdown");
|
|
320
|
+
const all = args.includes("--all");
|
|
321
|
+
const compareIdx = args.indexOf("--compare");
|
|
322
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
323
|
+
|
|
324
|
+
if (all) return renderAll(asJson);
|
|
325
|
+
|
|
326
|
+
if (compareIdx >= 0) {
|
|
327
|
+
const a = args[compareIdx + 1];
|
|
328
|
+
const b = args[compareIdx + 2];
|
|
329
|
+
if (!a || !b || a.startsWith("-") || b.startsWith("-")) {
|
|
330
|
+
process.stderr.write("Usage: cue eval --compare <profile-a> <profile-b>\n");
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
return renderCompare(a, b, asJson);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let profileName = positional[0];
|
|
337
|
+
if (!profileName) {
|
|
338
|
+
try {
|
|
339
|
+
const resolved = await resolveProfileForCwd({ cwd: process.cwd(), homeDir: homedir(), configDir: join(homedir(), ".config", "cue") });
|
|
340
|
+
if (resolved.source !== "none") profileName = (resolved as any).profile;
|
|
341
|
+
} catch {}
|
|
342
|
+
}
|
|
343
|
+
if (!profileName) {
|
|
344
|
+
process.stderr.write("Usage: cue eval [profile] [--breakdown] [--all] [--compare a b] [--json]\n");
|
|
345
|
+
return 1;
|
|
346
|
+
}
|
|
347
|
+
return renderOne(profileName, breakdown, asJson);
|
|
348
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cue evolve` — auto-learning loop for profiles.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* (default) — scan sessions, detect gaps, propose skill changes
|
|
6
|
+
* --apply — apply the last proposal to profile.yaml
|
|
7
|
+
* --history — show evolution log
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import {
|
|
12
|
+
readFileSync,
|
|
13
|
+
existsSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
statSync,
|
|
18
|
+
appendFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { join, resolve, dirname } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
|
|
23
|
+
import { loadProfile, listProfiles } from "../lib/profile-loader";
|
|
24
|
+
import { resolveProfileForCwd } from "../lib/cwd-resolver";
|
|
25
|
+
|
|
26
|
+
const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cue");
|
|
27
|
+
const EVO_LOG = join(CONFIG_DIR, "evolution-log.jsonl");
|
|
28
|
+
const SESSIONS_ROOT = join(homedir(), ".claude", "projects");
|
|
29
|
+
const DAYS_7 = 7 * 24 * 60 * 60 * 1000;
|
|
30
|
+
const DAYS_30 = 30 * 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
interface Proposal {
|
|
33
|
+
add: string[];
|
|
34
|
+
remove: string[];
|
|
35
|
+
mcps: string[];
|
|
36
|
+
gaps: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Session scanning
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function findSessionFiles(maxAge: number): string[] {
|
|
44
|
+
const files: string[] = [];
|
|
45
|
+
const cutoff = Date.now() - maxAge;
|
|
46
|
+
if (!existsSync(SESSIONS_ROOT)) return files;
|
|
47
|
+
|
|
48
|
+
function walk(dir: string) {
|
|
49
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const p = join(dir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) walk(p);
|
|
52
|
+
else if (entry.name.endsWith(".jsonl")) {
|
|
53
|
+
try { if (statSync(p).mtimeMs >= cutoff) files.push(p); } catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
walk(SESSIONS_ROOT);
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseSessionLines(files: string[]): any[] {
|
|
62
|
+
const lines: any[] = [];
|
|
63
|
+
for (const f of files) {
|
|
64
|
+
try {
|
|
65
|
+
for (const line of readFileSync(f, "utf8").split("\n")) {
|
|
66
|
+
if (!line.trim()) continue;
|
|
67
|
+
try { lines.push(JSON.parse(line)); } catch {}
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Pattern detection
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function detectGaps(lines: any[]): Map<string, number> {
|
|
79
|
+
const topics = new Map<string, number>();
|
|
80
|
+
const gapPatterns = /\b(can you|how do i|how to|help me|is there a way)\b/i;
|
|
81
|
+
|
|
82
|
+
for (const msg of lines) {
|
|
83
|
+
if (msg.role !== "user" && msg.type !== "human") continue;
|
|
84
|
+
const text = typeof msg.content === "string" ? msg.content : msg.message ?? "";
|
|
85
|
+
if (!gapPatterns.test(text)) continue;
|
|
86
|
+
// Extract topic: first 3-4 meaningful words after the pattern
|
|
87
|
+
const match = text.match(gapPatterns);
|
|
88
|
+
if (!match) continue;
|
|
89
|
+
const after = text.slice((match.index ?? 0) + match[0].length).trim().split(/\s+/).slice(0, 4).join(" ").toLowerCase();
|
|
90
|
+
if (after.length > 3) topics.set(after, (topics.get(after) ?? 0) + 1);
|
|
91
|
+
}
|
|
92
|
+
return topics;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function detectErrors(lines: any[]): string[] {
|
|
96
|
+
const errors: string[] = [];
|
|
97
|
+
for (const msg of lines) {
|
|
98
|
+
if (msg.type === "tool_result" && msg.is_error) {
|
|
99
|
+
const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "");
|
|
100
|
+
if (text.length > 5) errors.push(text.slice(0, 120));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return errors;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function checkSkillUsage(lines: any[], skills: string[]): { used: string[]; unused: string[] } {
|
|
107
|
+
const content = lines.map(l => {
|
|
108
|
+
const t = typeof l.content === "string" ? l.content : l.message ?? "";
|
|
109
|
+
return t.toLowerCase();
|
|
110
|
+
}).join("\n");
|
|
111
|
+
|
|
112
|
+
const used: string[] = [];
|
|
113
|
+
const unused: string[] = [];
|
|
114
|
+
for (const skill of skills) {
|
|
115
|
+
const name = skill.split("/").pop()!.toLowerCase().replace(/-/g, " ");
|
|
116
|
+
if (content.includes(name) || content.includes(skill.toLowerCase())) used.push(skill);
|
|
117
|
+
else unused.push(skill);
|
|
118
|
+
}
|
|
119
|
+
return { used, unused };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// GitHub search for gap-filling skills
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
function ghSearchSkills(query: string): string[] {
|
|
127
|
+
const res = spawnSync("gh", [
|
|
128
|
+
"api", "search/repositories",
|
|
129
|
+
"--method", "GET",
|
|
130
|
+
"-f", `q=path:SKILL.md ${query}`,
|
|
131
|
+
"-f", "per_page=5",
|
|
132
|
+
"-f", "sort=stars",
|
|
133
|
+
"--jq", ".items[].full_name",
|
|
134
|
+
], { encoding: "utf8", timeout: 15000 });
|
|
135
|
+
if (res.status !== 0) return [];
|
|
136
|
+
return res.stdout.trim().split("\n").filter(Boolean);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Proposal generation
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
async function generateProposal(profileName: string): Promise<Proposal> {
|
|
144
|
+
const profile = await loadProfile(profileName);
|
|
145
|
+
const skills = (profile.skills ?? []).map((s: any) => typeof s === "string" ? s : s.path ?? s.name ?? "");
|
|
146
|
+
|
|
147
|
+
const recentFiles = findSessionFiles(DAYS_7);
|
|
148
|
+
const allFiles = findSessionFiles(DAYS_30);
|
|
149
|
+
const recentLines = parseSessionLines(recentFiles);
|
|
150
|
+
const allLines = parseSessionLines(allFiles);
|
|
151
|
+
|
|
152
|
+
// Detect gaps (asked 3+ times, no skill covers it)
|
|
153
|
+
const gaps = detectGaps(recentLines);
|
|
154
|
+
const significantGaps = [...gaps.entries()].filter(([, count]) => count >= 3).map(([topic]) => topic);
|
|
155
|
+
|
|
156
|
+
// Detect errors
|
|
157
|
+
const errors = detectErrors(recentLines);
|
|
158
|
+
|
|
159
|
+
// Check skill usage over 30 days
|
|
160
|
+
const { unused } = checkSkillUsage(allLines, skills);
|
|
161
|
+
|
|
162
|
+
// Search for skills to fill gaps
|
|
163
|
+
const suggestions: string[] = [];
|
|
164
|
+
for (const gap of significantGaps.slice(0, 3)) {
|
|
165
|
+
suggestions.push(...ghSearchSkills(gap));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
add: [...new Set(suggestions)].slice(0, 5),
|
|
170
|
+
remove: unused.slice(0, 5),
|
|
171
|
+
mcps: [],
|
|
172
|
+
gaps: significantGaps,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Apply proposal
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
async function applyProposal(profileName: string, proposal: Proposal): Promise<void> {
|
|
181
|
+
const profilesDir = resolve(dirname(new URL(import.meta.url).pathname), "..", "..", "profiles");
|
|
182
|
+
const yamlPath = join(profilesDir, profileName, "profile.yaml");
|
|
183
|
+
if (!existsSync(yamlPath)) {
|
|
184
|
+
process.stderr.write(`❌ Profile YAML not found: ${yamlPath}\n`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let content = readFileSync(yamlPath, "utf8");
|
|
189
|
+
|
|
190
|
+
// Append new skills under local: section
|
|
191
|
+
for (const skill of proposal.add) {
|
|
192
|
+
if (!content.includes(skill)) {
|
|
193
|
+
content = content.replace(/(local:\s*\n)/, `$1 - ${skill}\n`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Remove unused skills
|
|
198
|
+
for (const skill of proposal.remove) {
|
|
199
|
+
content = content.replace(new RegExp(`\\s*-\\s*${skill.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\n?`), "\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
writeFileSync(yamlPath, content);
|
|
203
|
+
|
|
204
|
+
// Log the evolution
|
|
205
|
+
const entry = { ts: new Date().toISOString(), profile: profileName, added: proposal.add, removed: proposal.remove, gaps: proposal.gaps };
|
|
206
|
+
mkdirSync(dirname(EVO_LOG), { recursive: true });
|
|
207
|
+
appendFileSync(EVO_LOG, JSON.stringify(entry) + "\n");
|
|
208
|
+
|
|
209
|
+
process.stdout.write(`✅ Applied: +${proposal.add.length} skills, -${proposal.remove.length} skills\n`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// History
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
function showHistory(): void {
|
|
217
|
+
if (!existsSync(EVO_LOG)) {
|
|
218
|
+
process.stdout.write("No evolution history yet.\n");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const lines = readFileSync(EVO_LOG, "utf8").trim().split("\n");
|
|
222
|
+
for (const line of lines.slice(-20)) {
|
|
223
|
+
try {
|
|
224
|
+
const e = JSON.parse(line);
|
|
225
|
+
process.stdout.write(`${e.ts} ${e.profile} +${e.added?.length ?? 0}/-${e.removed?.length ?? 0} gaps: ${(e.gaps ?? []).join(", ")}\n`);
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Main
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
export async function run(args: string[]): Promise<number> {
|
|
235
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
236
|
+
if (showHelp) {
|
|
237
|
+
process.stdout.write(`Usage: cue evolve [--apply] [--history] [profile]
|
|
238
|
+
|
|
239
|
+
(default) Scan sessions, detect gaps, propose skill changes
|
|
240
|
+
--apply Apply the last proposal to profile.yaml
|
|
241
|
+
--history Show evolution log (what was added/removed and when)
|
|
242
|
+
`);
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (args.includes("--history")) { showHistory(); return 0; }
|
|
247
|
+
|
|
248
|
+
// Resolve profile
|
|
249
|
+
const configDir = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cue");
|
|
250
|
+
const explicit = args.find(a => !a.startsWith("-"));
|
|
251
|
+
let profileName = explicit;
|
|
252
|
+
if (!profileName) {
|
|
253
|
+
const resolved = await resolveProfileForCwd({ cwd: process.cwd(), homeDir: homedir(), configDir });
|
|
254
|
+
profileName = "profile" in resolved ? resolved.profile : "core";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
process.stdout.write(`🧬 Evolving profile: ${profileName}\n\n`);
|
|
258
|
+
|
|
259
|
+
const proposal = await generateProposal(profileName);
|
|
260
|
+
|
|
261
|
+
if (!proposal.gaps.length && !proposal.remove.length && !proposal.add.length) {
|
|
262
|
+
process.stdout.write("✅ Profile is well-adapted — no changes suggested.\n");
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (proposal.gaps.length) {
|
|
267
|
+
process.stdout.write(`📊 Detected gaps (asked 3+ times, no skill covers):\n`);
|
|
268
|
+
for (const g of proposal.gaps) process.stdout.write(` • ${g}\n`);
|
|
269
|
+
process.stdout.write("\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (proposal.remove.length) {
|
|
273
|
+
process.stdout.write(`🗑️ Unused skills (30+ days, candidates for removal):\n`);
|
|
274
|
+
for (const s of proposal.remove) process.stdout.write(` • ${s}\n`);
|
|
275
|
+
process.stdout.write("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (proposal.add.length) {
|
|
279
|
+
process.stdout.write(`💡 Suggested skills to add:\n`);
|
|
280
|
+
for (const s of proposal.add) process.stdout.write(` • ${s}\n`);
|
|
281
|
+
process.stdout.write("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (args.includes("--apply")) {
|
|
285
|
+
await applyProposal(profileName, proposal);
|
|
286
|
+
} else {
|
|
287
|
+
process.stdout.write("Run with --apply to apply these changes.\n");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return 0;
|
|
291
|
+
}
|