karajan-code 1.43.0 → 1.45.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/package.json +1 -1
- package/src/commands/init.js +33 -0
- package/src/config.js +7 -0
- package/src/prompts/architect.js +4 -2
- package/src/prompts/coder.js +4 -1
- package/src/prompts/hu-reviewer.js +4 -2
- package/src/prompts/reviewer.js +4 -1
- package/src/roles/impeccable-role.js +23 -4
- package/src/utils/display.js +1 -0
- package/src/utils/locale.js +59 -0
- package/src/webperf/cwv-gate.js +94 -0
- package/src/webperf/devtools-detect.js +65 -0
- package/src/webperf/webperf-role-integration.js +66 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -9,6 +9,7 @@ import { detectAvailableAgents } from "../utils/agent-detect.js";
|
|
|
9
9
|
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
10
10
|
import { runCommand } from "../utils/process.js";
|
|
11
11
|
import { getInstallCommand } from "../utils/os-detect.js";
|
|
12
|
+
import { detectOsLocale, SUPPORTED_LANGUAGES } from "../utils/locale.js";
|
|
12
13
|
|
|
13
14
|
async function runWizard(config, logger) {
|
|
14
15
|
const agents = await detectAvailableAgents();
|
|
@@ -88,6 +89,31 @@ async function runWizard(config, logger) {
|
|
|
88
89
|
config.development.require_test_changes = methodology === "tdd";
|
|
89
90
|
logger.info(` -> Methodology: ${methodology}`);
|
|
90
91
|
|
|
92
|
+
const detectedLocale = detectOsLocale();
|
|
93
|
+
const allLangEntries = Object.entries(SUPPORTED_LANGUAGES).map(([code, name]) => ({
|
|
94
|
+
label: `${name} (${code})`,
|
|
95
|
+
value: code,
|
|
96
|
+
available: true
|
|
97
|
+
}));
|
|
98
|
+
// Put detected locale first so it becomes the default selection
|
|
99
|
+
const languageOptions = [
|
|
100
|
+
...allLangEntries.filter((o) => o.value === detectedLocale),
|
|
101
|
+
...allLangEntries.filter((o) => o.value !== detectedLocale)
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const pipelineLang = await wizard.select("Pipeline language:", languageOptions);
|
|
105
|
+
config.language = pipelineLang;
|
|
106
|
+
logger.info(` -> Pipeline language: ${pipelineLang}`);
|
|
107
|
+
|
|
108
|
+
// Reorder for HU language with pipeline language as default
|
|
109
|
+
const huLangOptions = [
|
|
110
|
+
...allLangEntries.filter((o) => o.value === pipelineLang),
|
|
111
|
+
...allLangEntries.filter((o) => o.value !== pipelineLang)
|
|
112
|
+
];
|
|
113
|
+
const huLang = await wizard.select("HU language (for user stories):", huLangOptions);
|
|
114
|
+
config.hu_language = huLang;
|
|
115
|
+
logger.info(` -> HU language: ${huLang}`);
|
|
116
|
+
|
|
91
117
|
logger.info("");
|
|
92
118
|
} finally {
|
|
93
119
|
wizard.close();
|
|
@@ -290,6 +316,13 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
290
316
|
const { config, exists: configExists } = await loadConfig();
|
|
291
317
|
const interactive = flags.noInteractive !== true && isTTY();
|
|
292
318
|
|
|
319
|
+
// Auto-detect language from OS locale for non-interactive mode
|
|
320
|
+
if (!interactive && !configExists) {
|
|
321
|
+
const detectedLocale = detectOsLocale();
|
|
322
|
+
config.language = detectedLocale;
|
|
323
|
+
config.hu_language = detectedLocale;
|
|
324
|
+
}
|
|
325
|
+
|
|
293
326
|
await handleConfigSetup({ config, configExists, interactive, configPath, logger });
|
|
294
327
|
await ensureReviewRules(reviewRulesPath, logger);
|
|
295
328
|
await ensureCoderRules(coderRulesPath, logger);
|
package/src/config.js
CHANGED
|
@@ -123,6 +123,8 @@ const DEFAULTS = {
|
|
|
123
123
|
port: 4000,
|
|
124
124
|
auto_start: false
|
|
125
125
|
},
|
|
126
|
+
language: "en",
|
|
127
|
+
hu_language: "en",
|
|
126
128
|
policies: {},
|
|
127
129
|
serena: { enabled: false },
|
|
128
130
|
planning_game: { enabled: false, project_id: null, codeveloper: null },
|
|
@@ -164,6 +166,11 @@ const DEFAULTS = {
|
|
|
164
166
|
backoff_multiplier: 2,
|
|
165
167
|
jitter_factor: 0.1
|
|
166
168
|
},
|
|
169
|
+
webperf: {
|
|
170
|
+
enabled: true,
|
|
171
|
+
devtools_mcp: false,
|
|
172
|
+
thresholds: { lcp: 2500, cls: 0.1, inp: 200 }
|
|
173
|
+
},
|
|
167
174
|
guards: {
|
|
168
175
|
output: {
|
|
169
176
|
enabled: true,
|
package/src/prompts/architect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
2
|
+
import { getLanguageInstruction } from "../utils/locale.js";
|
|
2
3
|
|
|
3
4
|
const SUBAGENT_PREAMBLE = [
|
|
4
5
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -8,8 +9,9 @@ const SUBAGENT_PREAMBLE = [
|
|
|
8
9
|
|
|
9
10
|
export const VALID_VERDICTS = new Set(["ready", "needs_clarification"]);
|
|
10
11
|
|
|
11
|
-
export async function buildArchitectPrompt({ task, instructions, researchContext = null, productContext = null, projectDir = null }) {
|
|
12
|
-
const
|
|
12
|
+
export async function buildArchitectPrompt({ task, instructions, researchContext = null, productContext = null, projectDir = null, language = "en" }) {
|
|
13
|
+
const langInstruction = getLanguageInstruction(language);
|
|
14
|
+
const sections = [SUBAGENT_PREAMBLE, ...(langInstruction ? [langInstruction] : [])];
|
|
13
15
|
|
|
14
16
|
if (instructions) {
|
|
15
17
|
sections.push(instructions);
|
package/src/prompts/coder.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RTK_INSTRUCTIONS } from "./rtk-snippet.js";
|
|
2
2
|
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
3
|
+
import { getLanguageInstruction } from "../utils/locale.js";
|
|
3
4
|
|
|
4
5
|
const SUBAGENT_PREAMBLE = [
|
|
5
6
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -32,9 +33,11 @@ const SERENA_INSTRUCTIONS = [
|
|
|
32
33
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
33
34
|
].join("\n");
|
|
34
35
|
|
|
35
|
-
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, plan = null, projectDir = null }) {
|
|
36
|
+
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, plan = null, projectDir = null, language = "en" }) {
|
|
37
|
+
const langInstruction = getLanguageInstruction(language);
|
|
36
38
|
const sections = [
|
|
37
39
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
40
|
+
...(langInstruction ? [langInstruction] : []),
|
|
38
41
|
`Task:\n${task}`,
|
|
39
42
|
"Implement directly in the repository.",
|
|
40
43
|
"Keep changes minimal and production-ready.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extractFirstJson } from "../utils/json-extract.js";
|
|
2
|
+
import { getLanguageInstruction } from "../utils/locale.js";
|
|
2
3
|
|
|
3
4
|
const SUBAGENT_PREAMBLE = [
|
|
4
5
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -21,8 +22,9 @@ const DIMENSION_KEYS = [
|
|
|
21
22
|
* @param {{stories: Array<{id: string, text: string}>, instructions: string|null, context?: string|null}} params
|
|
22
23
|
* @returns {string} The assembled prompt.
|
|
23
24
|
*/
|
|
24
|
-
export function buildHuReviewerPrompt({ stories, instructions, context = null, productContext = null }) {
|
|
25
|
-
const
|
|
25
|
+
export function buildHuReviewerPrompt({ stories, instructions, context = null, productContext = null, hu_language = "en" }) {
|
|
26
|
+
const langInstruction = getLanguageInstruction(hu_language);
|
|
27
|
+
const sections = [SUBAGENT_PREAMBLE, ...(langInstruction ? [langInstruction] : [])];
|
|
26
28
|
|
|
27
29
|
if (instructions) {
|
|
28
30
|
sections.push(instructions);
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RTK_INSTRUCTIONS } from "./rtk-snippet.js";
|
|
2
2
|
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
3
|
+
import { getLanguageInstruction } from "../utils/locale.js";
|
|
3
4
|
|
|
4
5
|
const SUBAGENT_PREAMBLE = [
|
|
5
6
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -23,11 +24,13 @@ const SERENA_INSTRUCTIONS = [
|
|
|
23
24
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
24
25
|
].join("\n");
|
|
25
26
|
|
|
26
|
-
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, projectDir = null }) {
|
|
27
|
+
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, projectDir = null, language = "en" }) {
|
|
27
28
|
const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
|
|
28
29
|
|
|
30
|
+
const langInstruction = getLanguageInstruction(language);
|
|
29
31
|
const sections = [
|
|
30
32
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
33
|
+
...(langInstruction ? [langInstruction] : []),
|
|
31
34
|
`You are a code reviewer in ${mode} mode.`,
|
|
32
35
|
"CRITICAL SCOPE RULE: Only review changes that are part of the diff below. Do NOT flag issues in unchanged code, missing features planned for future tasks, or improvements outside the scope of this task. If the diff is correct for what the task asks, approve it — even if the broader codebase has other issues.",
|
|
33
36
|
"Only block approval for issues IN THE DIFF that are bugs, security vulnerabilities, or clear violations of the review rules.",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseRole } from "./base-role.js";
|
|
2
2
|
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
|
+
import { isDevToolsMcpAvailable, ensureWebPerfSkills } from "../webperf/devtools-detect.js";
|
|
3
4
|
|
|
4
5
|
const SUBAGENT_PREAMBLE = [
|
|
5
6
|
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
@@ -64,14 +65,32 @@ export class ImpeccableRole extends BaseRole {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
async execute(input) {
|
|
67
|
-
const { task, diff } = typeof input === "string"
|
|
68
|
-
? { task: input, diff: null }
|
|
69
|
-
: { task: input?.task || this.context?.task || "", diff: input?.diff || null };
|
|
68
|
+
const { task, diff, projectDir } = typeof input === "string"
|
|
69
|
+
? { task: input, diff: null, projectDir: null }
|
|
70
|
+
: { task: input?.task || this.context?.task || "", diff: input?.diff || null, projectDir: input?.projectDir || null };
|
|
71
|
+
|
|
72
|
+
// Auto-install WebPerf skills when DevTools MCP is configured
|
|
73
|
+
if (isDevToolsMcpAvailable(this.config)) {
|
|
74
|
+
const dir = projectDir || process.cwd();
|
|
75
|
+
try {
|
|
76
|
+
const webperfResult = await ensureWebPerfSkills(dir, this.logger);
|
|
77
|
+
if (webperfResult.installed.length > 0) {
|
|
78
|
+
this.logger?.info?.(`WebPerf skills installed: ${webperfResult.installed.join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
this.logger?.warn?.(`WebPerf skill installation failed: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
this.logger?.info?.("Chrome DevTools MCP not configured — skipping WebPerf skills");
|
|
85
|
+
}
|
|
70
86
|
|
|
71
87
|
const provider = resolveProvider(this.config);
|
|
72
88
|
const agent = this._createAgent(provider, this.config, this.logger);
|
|
73
89
|
|
|
74
|
-
const
|
|
90
|
+
const webperfNote = isDevToolsMcpAvailable(this.config)
|
|
91
|
+
? "\n\nNote: WebPerf domain skills are available. Use them for Core Web Vitals analysis if the audit involves frontend performance."
|
|
92
|
+
: "";
|
|
93
|
+
const prompt = buildPrompt({ task, diff, instructions: this.instructions }) + webperfNote;
|
|
75
94
|
const result = await agent.runTask({ prompt, role: "impeccable" });
|
|
76
95
|
|
|
77
96
|
if (!result.ok) {
|
package/src/utils/display.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
// TODO: i18n display messages
|
|
5
6
|
const DISPLAY_PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
6
7
|
const DISPLAY_VERSION = JSON.parse(readFileSync(DISPLAY_PKG_PATH, "utf8")).version;
|
|
7
8
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS locale detection and language instruction generation for i18n support.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SUPPORTED_LANGUAGES = {
|
|
6
|
+
en: "English",
|
|
7
|
+
es: "Español"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const LANG_ENV_VARS = ["LANG", "LANGUAGE", "LC_ALL", "LC_MESSAGES"];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect the 2-letter language code from OS environment variables.
|
|
14
|
+
* Checks LANG, LANGUAGE, LC_ALL, LC_MESSAGES in order.
|
|
15
|
+
* @returns {string} 2-letter language code (e.g. "en", "es")
|
|
16
|
+
*/
|
|
17
|
+
export function detectOsLocale() {
|
|
18
|
+
for (const envVar of LANG_ENV_VARS) {
|
|
19
|
+
const value = process.env[envVar];
|
|
20
|
+
if (value) {
|
|
21
|
+
const code = extractLangCode(value);
|
|
22
|
+
if (code) return code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return "en";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract a 2-letter language code from a locale string.
|
|
30
|
+
* Handles formats like "es_ES.UTF-8", "en_US", "es", "C.UTF-8".
|
|
31
|
+
* @param {string} locale
|
|
32
|
+
* @returns {string|null}
|
|
33
|
+
*/
|
|
34
|
+
function extractLangCode(locale) {
|
|
35
|
+
const trimmed = locale.trim();
|
|
36
|
+
if (!trimmed || trimmed === "C" || trimmed === "POSIX") return null;
|
|
37
|
+
// Match first 2 lowercase letters before _, ., or end
|
|
38
|
+
const match = /^([a-z]{2})(?:[_.\-]|$)/i.exec(trimmed);
|
|
39
|
+
if (match) return match[1].toLowerCase();
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get a prompt instruction string for the given language.
|
|
45
|
+
* Returns empty string for English (agents default to English).
|
|
46
|
+
* @param {string} lang - 2-letter language code
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function getLanguageInstruction(lang) {
|
|
50
|
+
if (!lang || lang === "en") return "";
|
|
51
|
+
const languageName = SUPPORTED_LANGUAGES[lang];
|
|
52
|
+
if (lang === "es") {
|
|
53
|
+
return "IMPORTANT: Respond in Spanish. All output, explanations, and feedback must be in Spanish.";
|
|
54
|
+
}
|
|
55
|
+
if (languageName) {
|
|
56
|
+
return `IMPORTANT: Respond in ${languageName}. All output must be in ${languageName}.`;
|
|
57
|
+
}
|
|
58
|
+
return `IMPORTANT: Respond in ${lang}. All output must be in ${lang}.`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Web Vitals quality gate.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates LCP, CLS and INP against Google's recommended thresholds
|
|
5
|
+
* and returns a structured verdict usable by the pipeline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CWV_THRESHOLDS = {
|
|
9
|
+
lcp: { good: 2500, poor: 4000 },
|
|
10
|
+
cls: { good: 0.1, poor: 0.25 },
|
|
11
|
+
inp: { good: 200, poor: 500 }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Deep-merge custom thresholds on top of the defaults.
|
|
16
|
+
* Only overrides metrics/boundaries that the caller provides.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} defaults - Base thresholds (e.g. CWV_THRESHOLDS)
|
|
19
|
+
* @param {object} [custom] - Partial overrides
|
|
20
|
+
* @returns {object} Merged thresholds
|
|
21
|
+
*/
|
|
22
|
+
export function mergeThresholds(defaults, custom) {
|
|
23
|
+
if (!custom || typeof custom !== "object") return { ...defaults };
|
|
24
|
+
|
|
25
|
+
const merged = {};
|
|
26
|
+
for (const metric of Object.keys(defaults)) {
|
|
27
|
+
const base = defaults[metric];
|
|
28
|
+
const over = custom[metric];
|
|
29
|
+
if (over && typeof over === "object") {
|
|
30
|
+
merged[metric] = {
|
|
31
|
+
good: over.good !== undefined ? over.good : base.good,
|
|
32
|
+
poor: over.poor !== undefined ? over.poor : base.poor
|
|
33
|
+
};
|
|
34
|
+
} else {
|
|
35
|
+
merged[metric] = { ...base };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Include any extra metrics the caller added that are not in defaults
|
|
40
|
+
for (const metric of Object.keys(custom)) {
|
|
41
|
+
if (!merged[metric] && typeof custom[metric] === "object") {
|
|
42
|
+
merged[metric] = { ...custom[metric] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function rateMetric(value, good, poor) {
|
|
50
|
+
if (value <= good) return "good";
|
|
51
|
+
if (value >= poor) return "poor";
|
|
52
|
+
return "needs-improvement";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Evaluate Core Web Vitals metrics against thresholds.
|
|
57
|
+
*
|
|
58
|
+
* @param {{ lcp?: number, cls?: number, inp?: number }} metrics
|
|
59
|
+
* @param {object} [customThresholds] - Optional per-metric overrides
|
|
60
|
+
* @returns {{
|
|
61
|
+
* pass: boolean,
|
|
62
|
+
* scores: Record<string, { value: number, rating: string }>,
|
|
63
|
+
* blocking: Array<{ metric: string, value: number, threshold: number, rating: string }>,
|
|
64
|
+
* advisory: Array<{ metric: string, value: number, threshold: number, rating: string }>
|
|
65
|
+
* }}
|
|
66
|
+
*/
|
|
67
|
+
export function evaluateCwv(metrics, customThresholds) {
|
|
68
|
+
const thresholds = mergeThresholds(CWV_THRESHOLDS, customThresholds);
|
|
69
|
+
|
|
70
|
+
const scores = {};
|
|
71
|
+
const blocking = [];
|
|
72
|
+
const advisory = [];
|
|
73
|
+
|
|
74
|
+
for (const [metric, bounds] of Object.entries(thresholds)) {
|
|
75
|
+
const value = metrics[metric];
|
|
76
|
+
if (value === undefined || value === null) continue;
|
|
77
|
+
|
|
78
|
+
const rating = rateMetric(value, bounds.good, bounds.poor);
|
|
79
|
+
scores[metric] = { value, rating };
|
|
80
|
+
|
|
81
|
+
if (rating === "poor") {
|
|
82
|
+
blocking.push({ metric, value, threshold: bounds.poor, rating });
|
|
83
|
+
} else if (rating === "needs-improvement") {
|
|
84
|
+
advisory.push({ metric, value, threshold: bounds.good, rating });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
pass: blocking.length === 0,
|
|
90
|
+
scores,
|
|
91
|
+
blocking,
|
|
92
|
+
advisory
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome DevTools MCP detection and WebPerf skill auto-installation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isOpenSkillsAvailable, installSkill, listSkills } from "../skills/openskills-client.js";
|
|
6
|
+
|
|
7
|
+
/** Skill names to auto-install when WebPerf is enabled. */
|
|
8
|
+
export const WEBPERF_SKILLS = [
|
|
9
|
+
"webperf",
|
|
10
|
+
"webperf-core-web-vitals",
|
|
11
|
+
"webperf-loading"
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check whether Chrome DevTools MCP is configured and available.
|
|
16
|
+
* Since Karajan runs as an MCP server itself, it cannot directly call other MCPs.
|
|
17
|
+
* Instead, we check the config flag `webperf.devtools_mcp`.
|
|
18
|
+
* @param {object} config — merged Karajan config
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
export function isDevToolsMcpAvailable(config) {
|
|
22
|
+
return Boolean(config?.webperf?.devtools_mcp);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure WebPerf skills are installed via OpenSkills.
|
|
27
|
+
* Installs any missing skills from WEBPERF_SKILLS.
|
|
28
|
+
* @param {string} projectDir — absolute path to the project root
|
|
29
|
+
* @param {object} [logger] — optional logger ({ info, warn })
|
|
30
|
+
* @returns {Promise<{ installed: string[], alreadyInstalled: string[], skipped: string[] }>}
|
|
31
|
+
*/
|
|
32
|
+
export async function ensureWebPerfSkills(projectDir, logger) {
|
|
33
|
+
const result = { installed: [], alreadyInstalled: [], skipped: [] };
|
|
34
|
+
|
|
35
|
+
const available = await isOpenSkillsAvailable();
|
|
36
|
+
if (!available) {
|
|
37
|
+
logger?.warn?.("OpenSkills CLI not available — skipping WebPerf skill installation");
|
|
38
|
+
result.skipped = [...WEBPERF_SKILLS];
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get currently installed skills
|
|
43
|
+
const listResult = await listSkills({ projectDir });
|
|
44
|
+
const installedNames = new Set(
|
|
45
|
+
(listResult.ok ? listResult.skills : []).map(s => s.name)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
for (const skill of WEBPERF_SKILLS) {
|
|
49
|
+
if (installedNames.has(skill)) {
|
|
50
|
+
result.alreadyInstalled.push(skill);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const installResult = await installSkill(skill, { projectDir });
|
|
55
|
+
if (installResult.ok) {
|
|
56
|
+
result.installed.push(skill);
|
|
57
|
+
logger?.info?.(`Installed WebPerf skill: ${skill}`);
|
|
58
|
+
} else {
|
|
59
|
+
result.skipped.push(skill);
|
|
60
|
+
logger?.warn?.(`Failed to install WebPerf skill "${skill}": ${installResult.error}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration helpers that format CWV gate results for the
|
|
3
|
+
* impeccable prompt and for pipeline progress events.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const RATING_ICONS = { good: "PASS", "needs-improvement": "WARN", poor: "FAIL" };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a human-readable section for the impeccable prompt.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ pass: boolean, scores: object, blocking: Array, advisory: Array }} cwvResult
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function buildWebPerfSection(cwvResult) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
const verdict = cwvResult.pass ? "PASSED" : "FAILED";
|
|
17
|
+
lines.push(`## WebPerf — Core Web Vitals (${verdict})`);
|
|
18
|
+
lines.push("");
|
|
19
|
+
|
|
20
|
+
for (const [metric, { value, rating }] of Object.entries(cwvResult.scores)) {
|
|
21
|
+
const icon = RATING_ICONS[rating] || rating;
|
|
22
|
+
const unit = metric === "cls" ? "" : "ms";
|
|
23
|
+
lines.push(`- **${metric.toUpperCase()}**: ${value}${unit} [${icon}]`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (cwvResult.blocking.length > 0) {
|
|
27
|
+
lines.push("");
|
|
28
|
+
lines.push("### Blocking issues");
|
|
29
|
+
for (const b of cwvResult.blocking) {
|
|
30
|
+
const unit = b.metric === "cls" ? "" : "ms";
|
|
31
|
+
lines.push(`- ${b.metric.toUpperCase()}: ${b.value}${unit} exceeds poor threshold (${b.threshold}${unit})`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (cwvResult.advisory.length > 0) {
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push("### Advisory (needs improvement)");
|
|
38
|
+
for (const a of cwvResult.advisory) {
|
|
39
|
+
const unit = a.metric === "cls" ? "" : "ms";
|
|
40
|
+
lines.push(`- ${a.metric.toUpperCase()}: ${a.value}${unit} exceeds good threshold (${a.threshold}${unit})`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format CWV result for a pipeline progress event payload.
|
|
49
|
+
*
|
|
50
|
+
* @param {{ pass: boolean, scores: object, blocking: Array, advisory: Array }} cwvResult
|
|
51
|
+
* @returns {{ type: string, pass: boolean, metrics: object, blockingCount: number, advisoryCount: number }}
|
|
52
|
+
*/
|
|
53
|
+
export function formatCwvForEvent(cwvResult) {
|
|
54
|
+
const metrics = {};
|
|
55
|
+
for (const [metric, { value, rating }] of Object.entries(cwvResult.scores)) {
|
|
56
|
+
metrics[metric] = { value, rating };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
type: "webperf-cwv",
|
|
61
|
+
pass: cwvResult.pass,
|
|
62
|
+
metrics,
|
|
63
|
+
blockingCount: cwvResult.blocking.length,
|
|
64
|
+
advisoryCount: cwvResult.advisory.length
|
|
65
|
+
};
|
|
66
|
+
}
|