@kody-ade/kody-engine-lite 0.1.100 → 0.1.102
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 +1 -1
- package/dist/bin/cli.js +227 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ kody-engine-lite rerun --issue-number 42 --from verify
|
|
|
154
154
|
|
|
155
155
|
## Documentation
|
|
156
156
|
|
|
157
|
-
**Understand Kody:** [About](docs/ABOUT.md) · [Features](docs/FEATURES.md) · [Pipeline](docs/PIPELINE.md) · [Comparison](docs/COMPARISON.md)
|
|
157
|
+
**Understand Kody:** [About](docs/ABOUT.md) · [Tech Stack](docs/TECH-STACK.md) · [Features](docs/FEATURES.md) · [Pipeline](docs/PIPELINE.md) · [Comparison](docs/COMPARISON.md)
|
|
158
158
|
|
|
159
159
|
**Set up & use:** [CLI](docs/CLI.md) · [Configuration](docs/CONFIGURATION.md) · [Bootstrap](docs/BOOTSTRAP.md) · [LiteLLM](docs/LITELLM.md)
|
|
160
160
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -305,7 +305,13 @@ function getProjectConfig() {
|
|
|
305
305
|
},
|
|
306
306
|
timeouts: raw.timeouts ?? void 0,
|
|
307
307
|
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
308
|
-
mcp: raw.mcp ? {
|
|
308
|
+
mcp: raw.mcp ? {
|
|
309
|
+
servers: {},
|
|
310
|
+
stages: ["build", "verify", "review", "review-fix"],
|
|
311
|
+
...raw.mcp,
|
|
312
|
+
// Auto-enable when devServer is configured (user can still set enabled: false to override)
|
|
313
|
+
enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
|
|
314
|
+
} : void 0
|
|
309
315
|
};
|
|
310
316
|
} catch {
|
|
311
317
|
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
@@ -861,7 +867,7 @@ var init_github_api = __esm({
|
|
|
861
867
|
"use strict";
|
|
862
868
|
init_logger();
|
|
863
869
|
API_TIMEOUT_MS = 3e4;
|
|
864
|
-
LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
|
|
870
|
+
LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
|
|
865
871
|
KODY_MARKERS = [
|
|
866
872
|
"Kody Review",
|
|
867
873
|
"\u{1F916} Generated by Kody",
|
|
@@ -1281,6 +1287,20 @@ var init_context_tiers = __esm({
|
|
|
1281
1287
|
});
|
|
1282
1288
|
|
|
1283
1289
|
// src/mcp-config.ts
|
|
1290
|
+
function withPlaywrightIfNeeded(mcpConfig, hasUI) {
|
|
1291
|
+
if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
|
|
1292
|
+
const hasPlaywright = Object.keys(mcpConfig.servers).some(
|
|
1293
|
+
(name) => name.toLowerCase().includes("playwright")
|
|
1294
|
+
);
|
|
1295
|
+
if (hasPlaywright) return mcpConfig;
|
|
1296
|
+
return {
|
|
1297
|
+
...mcpConfig,
|
|
1298
|
+
servers: {
|
|
1299
|
+
...mcpConfig.servers,
|
|
1300
|
+
playwright: PLAYWRIGHT_SERVER
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1284
1304
|
function buildMcpConfigJson(mcpConfig) {
|
|
1285
1305
|
if (!mcpConfig?.enabled) return void 0;
|
|
1286
1306
|
if (Object.keys(mcpConfig.servers).length === 0) return void 0;
|
|
@@ -1297,15 +1317,18 @@ function buildMcpConfigJson(mcpConfig) {
|
|
|
1297
1317
|
}
|
|
1298
1318
|
function isMcpEnabledForStage(stageName, mcpConfig) {
|
|
1299
1319
|
if (!mcpConfig?.enabled) return false;
|
|
1300
|
-
if (Object.keys(mcpConfig.servers).length === 0) return false;
|
|
1301
1320
|
const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
|
|
1302
1321
|
return allowedStages.includes(stageName);
|
|
1303
1322
|
}
|
|
1304
|
-
var DEFAULT_MCP_STAGES;
|
|
1323
|
+
var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
|
|
1305
1324
|
var init_mcp_config = __esm({
|
|
1306
1325
|
"src/mcp-config.ts"() {
|
|
1307
1326
|
"use strict";
|
|
1308
1327
|
DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
|
|
1328
|
+
PLAYWRIGHT_SERVER = {
|
|
1329
|
+
command: "npx",
|
|
1330
|
+
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
|
1331
|
+
};
|
|
1309
1332
|
}
|
|
1310
1333
|
});
|
|
1311
1334
|
|
|
@@ -1523,6 +1546,11 @@ ${prompt}` : prompt;
|
|
|
1523
1546
|
}
|
|
1524
1547
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1525
1548
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1549
|
+
const qaGuidePath = path5.join(projectDir, ".kody", "qa-guide.md");
|
|
1550
|
+
if (fs5.existsSync(qaGuidePath)) {
|
|
1551
|
+
const qaGuide = fs5.readFileSync(qaGuidePath, "utf-8").trim();
|
|
1552
|
+
assembled = assembled + "\n\n" + qaGuide;
|
|
1553
|
+
}
|
|
1526
1554
|
}
|
|
1527
1555
|
return assembled;
|
|
1528
1556
|
}
|
|
@@ -1685,7 +1713,8 @@ async function executeAgentStage(ctx, def) {
|
|
|
1685
1713
|
if (sessionInfo) {
|
|
1686
1714
|
logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
|
|
1687
1715
|
}
|
|
1688
|
-
const
|
|
1716
|
+
const mcpForStage = isMcpEnabledForStage(def.name, config.mcp) ? withPlaywrightIfNeeded(config.mcp, taskHasUI(ctx.taskDir)) : void 0;
|
|
1717
|
+
const mcpConfigJson = buildMcpConfigJson(mcpForStage);
|
|
1689
1718
|
if (mcpConfigJson) {
|
|
1690
1719
|
logger.info(` MCP servers enabled for ${def.name}`);
|
|
1691
1720
|
}
|
|
@@ -2670,6 +2699,7 @@ function applyPreStageLabel(ctx, def) {
|
|
|
2670
2699
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
2671
2700
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
2672
2701
|
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
2702
|
+
if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
|
|
2673
2703
|
}
|
|
2674
2704
|
function checkQuestionsAfterStage(ctx, def, state) {
|
|
2675
2705
|
if (def.name !== "taskify" && def.name !== "plan") return null;
|
|
@@ -4507,12 +4537,7 @@ function detectMcpConfig(cwd, pm, pkg) {
|
|
|
4507
4537
|
const defaultPort = isNext ? 3e3 : isVite ? 5173 : 3e3;
|
|
4508
4538
|
const mcp = {
|
|
4509
4539
|
enabled: true,
|
|
4510
|
-
servers: {
|
|
4511
|
-
playwright: {
|
|
4512
|
-
command: "npx",
|
|
4513
|
-
args: ["@playwright/mcp@latest"]
|
|
4514
|
-
}
|
|
4515
|
-
},
|
|
4540
|
+
servers: {},
|
|
4516
4541
|
stages: ["build", "review"]
|
|
4517
4542
|
};
|
|
4518
4543
|
if (hasDevScript) {
|
|
@@ -5009,6 +5034,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5009
5034
|
}
|
|
5010
5035
|
}
|
|
5011
5036
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
5037
|
+
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
5038
|
+
const qaGuidePath = path21.join(cwd, ".kody", "qa-guide.md");
|
|
5039
|
+
if (!fs22.existsSync(qaGuidePath) || opts.force) {
|
|
5040
|
+
const discovery = discoverQaContext(cwd);
|
|
5041
|
+
if (discovery.routes.length > 0) {
|
|
5042
|
+
const qaGuide = generateQaGuide(discovery);
|
|
5043
|
+
fs22.writeFileSync(qaGuidePath, qaGuide);
|
|
5044
|
+
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
5045
|
+
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
5046
|
+
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
5047
|
+
console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
|
|
5048
|
+
} else {
|
|
5049
|
+
console.log(" \u25CB No routes detected \u2014 skipping QA guide");
|
|
5050
|
+
}
|
|
5051
|
+
} else {
|
|
5052
|
+
console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
|
|
5053
|
+
}
|
|
5012
5054
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
5013
5055
|
try {
|
|
5014
5056
|
let repoSlug = "";
|
|
@@ -5027,6 +5069,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5027
5069
|
{ name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
|
|
5028
5070
|
{ name: "kody:building", color: "0e8a16", description: "Kody is building code" },
|
|
5029
5071
|
{ name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
|
|
5072
|
+
{ name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
|
|
5030
5073
|
{ name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
|
|
5031
5074
|
{ name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
|
|
5032
5075
|
{ name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
|
|
@@ -5085,6 +5128,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5085
5128
|
const filesToCommit = [
|
|
5086
5129
|
".kody/memory/architecture.md",
|
|
5087
5130
|
".kody/memory/conventions.md",
|
|
5131
|
+
".kody/qa-guide.md",
|
|
5088
5132
|
...installedSkillPaths
|
|
5089
5133
|
].filter((f) => fs22.existsSync(path21.join(cwd, f)));
|
|
5090
5134
|
if (fs22.existsSync(path21.join(cwd, "skills-lock.json"))) {
|
|
@@ -5198,6 +5242,178 @@ Create it manually.`, cwd);
|
|
|
5198
5242
|
console.log(" \u2713 Project bootstrap complete!");
|
|
5199
5243
|
console.log(" Kody now has project-specific memory and customized step files.\n");
|
|
5200
5244
|
}
|
|
5245
|
+
function discoverQaContext(cwd) {
|
|
5246
|
+
const result = {
|
|
5247
|
+
routes: [],
|
|
5248
|
+
authFiles: [],
|
|
5249
|
+
loginPage: null,
|
|
5250
|
+
adminPath: null,
|
|
5251
|
+
roles: [],
|
|
5252
|
+
devCommand: "",
|
|
5253
|
+
devPort: 3e3
|
|
5254
|
+
};
|
|
5255
|
+
try {
|
|
5256
|
+
const pkg = JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
|
|
5257
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5258
|
+
const pm = fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs22.existsSync(path21.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5259
|
+
if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
|
|
5260
|
+
if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
|
|
5261
|
+
else if (allDeps.vite) result.devPort = 5173;
|
|
5262
|
+
} catch {
|
|
5263
|
+
}
|
|
5264
|
+
const appDirs = ["src/app", "app"];
|
|
5265
|
+
for (const appDir of appDirs) {
|
|
5266
|
+
const fullAppDir = path21.join(cwd, appDir);
|
|
5267
|
+
if (!fs22.existsSync(fullAppDir)) continue;
|
|
5268
|
+
scanRoutes(fullAppDir, appDir, "", result);
|
|
5269
|
+
break;
|
|
5270
|
+
}
|
|
5271
|
+
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
5272
|
+
for (const p of authPatterns) {
|
|
5273
|
+
if (fs22.existsSync(path21.join(cwd, p))) result.authFiles.push(p);
|
|
5274
|
+
}
|
|
5275
|
+
const authConfigGlobs = [
|
|
5276
|
+
"src/app/api/auth",
|
|
5277
|
+
"src/auth",
|
|
5278
|
+
"src/lib/auth",
|
|
5279
|
+
"auth.config.ts",
|
|
5280
|
+
"auth.ts",
|
|
5281
|
+
"src/app/api/oauth"
|
|
5282
|
+
];
|
|
5283
|
+
for (const g of authConfigGlobs) {
|
|
5284
|
+
if (fs22.existsSync(path21.join(cwd, g))) result.authFiles.push(g);
|
|
5285
|
+
}
|
|
5286
|
+
try {
|
|
5287
|
+
const rolePaths = [
|
|
5288
|
+
"src/types",
|
|
5289
|
+
"src/lib",
|
|
5290
|
+
"src/utils",
|
|
5291
|
+
"src/constants",
|
|
5292
|
+
"src/access",
|
|
5293
|
+
"src/collections"
|
|
5294
|
+
];
|
|
5295
|
+
for (const rp of rolePaths) {
|
|
5296
|
+
const dir = path21.join(cwd, rp);
|
|
5297
|
+
if (!fs22.existsSync(dir)) continue;
|
|
5298
|
+
const files = fs22.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
5299
|
+
for (const f of files) {
|
|
5300
|
+
try {
|
|
5301
|
+
const content = fs22.readFileSync(path21.join(dir, f), "utf-8").slice(0, 5e3);
|
|
5302
|
+
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
5303
|
+
if (roleMatches) {
|
|
5304
|
+
for (const m of roleMatches) {
|
|
5305
|
+
const val = m.match(/['"](\w+)['"]/);
|
|
5306
|
+
if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
5310
|
+
if (enumMatch) {
|
|
5311
|
+
const vals = enumMatch[1].match(/['"](\w+)['"]/g);
|
|
5312
|
+
if (vals) {
|
|
5313
|
+
for (const v of vals) {
|
|
5314
|
+
const clean = v.replace(/['"]/g, "");
|
|
5315
|
+
if (!result.roles.includes(clean)) result.roles.push(clean);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
} catch {
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
} catch {
|
|
5324
|
+
}
|
|
5325
|
+
return result;
|
|
5326
|
+
}
|
|
5327
|
+
function scanRoutes(dir, baseDir, prefix, result) {
|
|
5328
|
+
let entries;
|
|
5329
|
+
try {
|
|
5330
|
+
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
5331
|
+
} catch {
|
|
5332
|
+
return;
|
|
5333
|
+
}
|
|
5334
|
+
const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
|
|
5335
|
+
if (hasPage) {
|
|
5336
|
+
const routePath = prefix || "/";
|
|
5337
|
+
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
5338
|
+
result.routes.push({ path: routePath, group });
|
|
5339
|
+
if (prefix.includes("/login")) result.loginPage = routePath;
|
|
5340
|
+
if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
|
|
5341
|
+
}
|
|
5342
|
+
for (const entry of entries) {
|
|
5343
|
+
if (!entry.isDirectory()) continue;
|
|
5344
|
+
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5345
|
+
let segment = entry.name;
|
|
5346
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5347
|
+
scanRoutes(path21.join(dir, entry.name), baseDir, prefix, result);
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
5351
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
5352
|
+
}
|
|
5353
|
+
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
5354
|
+
segment = `:${segment.slice(2, -2)}?`;
|
|
5355
|
+
}
|
|
5356
|
+
scanRoutes(path21.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
function generateQaGuide(discovery) {
|
|
5360
|
+
const lines = ["# QA Guide", "", "## Authentication", ""];
|
|
5361
|
+
if (discovery.loginPage) {
|
|
5362
|
+
lines.push(`- Login page: \`${discovery.loginPage}\``);
|
|
5363
|
+
}
|
|
5364
|
+
lines.push(
|
|
5365
|
+
"",
|
|
5366
|
+
"### Test Accounts",
|
|
5367
|
+
"<!-- Fill in your test/preview environment credentials below -->",
|
|
5368
|
+
"| Role | Email | Password |",
|
|
5369
|
+
"|------|-------|----------|",
|
|
5370
|
+
"| Admin | admin@example.com | CHANGE_ME |",
|
|
5371
|
+
"| User | user@example.com | CHANGE_ME |",
|
|
5372
|
+
"",
|
|
5373
|
+
"### Login Steps",
|
|
5374
|
+
`1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
|
|
5375
|
+
"2. Enter credentials from the test accounts table above",
|
|
5376
|
+
"3. Submit the login form",
|
|
5377
|
+
"4. Verify redirect to dashboard or home page"
|
|
5378
|
+
);
|
|
5379
|
+
if (discovery.authFiles.length > 0) {
|
|
5380
|
+
lines.push("", "### Auth Files");
|
|
5381
|
+
for (const f of discovery.authFiles) {
|
|
5382
|
+
lines.push(`- \`${f}\``);
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
if (discovery.roles.length > 0) {
|
|
5386
|
+
lines.push("", "## Roles", "");
|
|
5387
|
+
for (const role of discovery.roles) {
|
|
5388
|
+
lines.push(`- \`${role}\``);
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
lines.push("", "## Key Pages", "");
|
|
5392
|
+
const groups = {};
|
|
5393
|
+
for (const route of discovery.routes) {
|
|
5394
|
+
if (!groups[route.group]) groups[route.group] = [];
|
|
5395
|
+
groups[route.group].push(route.path);
|
|
5396
|
+
}
|
|
5397
|
+
for (const [group, routes] of Object.entries(groups)) {
|
|
5398
|
+
lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
|
|
5399
|
+
const sorted = routes.sort();
|
|
5400
|
+
for (const r of sorted.slice(0, 20)) {
|
|
5401
|
+
lines.push(`- \`${r}\``);
|
|
5402
|
+
}
|
|
5403
|
+
if (sorted.length > 20) {
|
|
5404
|
+
lines.push(`- ... and ${sorted.length - 20} more`);
|
|
5405
|
+
}
|
|
5406
|
+
lines.push("");
|
|
5407
|
+
}
|
|
5408
|
+
lines.push(
|
|
5409
|
+
"## Dev Server",
|
|
5410
|
+
"",
|
|
5411
|
+
`- Command: \`${discovery.devCommand || "pnpm dev"}\``,
|
|
5412
|
+
`- URL: \`http://localhost:${discovery.devPort}\``,
|
|
5413
|
+
""
|
|
5414
|
+
);
|
|
5415
|
+
return lines.join("\n");
|
|
5416
|
+
}
|
|
5201
5417
|
function detectArchitectureBasic(cwd) {
|
|
5202
5418
|
const detected = [];
|
|
5203
5419
|
const pkgPath = path21.join(cwd, "package.json");
|