@kody-ade/kody-engine-lite 0.1.133 → 0.1.135

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.
Files changed (2) hide show
  1. package/dist/bin/cli.js +226 -92
  2. package/package.json +1 -1
package/dist/bin/cli.js CHANGED
@@ -1183,8 +1183,8 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
1183
1183
  logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
1184
1184
  }
1185
1185
  }
1186
- const { spawn: spawn2 } = await import("child_process");
1187
- const child = spawn2(cmd, args2, {
1186
+ const { spawn: spawn3 } = await import("child_process");
1187
+ const child = spawn3(cmd, args2, {
1188
1188
  stdio: ["ignore", "pipe", "pipe"],
1189
1189
  detached: true,
1190
1190
  env: { ...process.env, ...dotenvVars }
@@ -2610,8 +2610,8 @@ async function startProxy(config, url) {
2610
2610
  fs13.writeFileSync(CONFIG_PATH, config);
2611
2611
  const portMatch = url.match(/:(\d+)/);
2612
2612
  const port = portMatch ? portMatch[1] : "4099";
2613
- const { spawn: spawn2 } = await import("child_process");
2614
- const child = spawn2("litellm", ["--config", CONFIG_PATH, "--port", port], {
2613
+ const { spawn: spawn3 } = await import("child_process");
2614
+ const child = spawn3("litellm", ["--config", CONFIG_PATH, "--port", port], {
2615
2615
  stdio: ["ignore", "pipe", "pipe"],
2616
2616
  detached: true,
2617
2617
  env: process.env
@@ -3741,7 +3741,15 @@ function getDevServerInfo(taskDir) {
3741
3741
  }
3742
3742
  function getBrowserToolGuidance(stageName, taskDir) {
3743
3743
  const devServer = getDevServerInfo(taskDir);
3744
- const devServerBlock = devServer ? `
3744
+ const engineManagedServer = process.env.KODY_DEV_SERVER_READY !== void 0;
3745
+ const serverReady = process.env.KODY_DEV_SERVER_READY === "true";
3746
+ const serverUrl = process.env.KODY_DEV_SERVER_URL ?? devServer?.url;
3747
+ const devServerBlock = engineManagedServer ? serverReady ? `
3748
+ ### Dev Server
3749
+ The dev server is already running at ${serverUrl}. Do NOT start it yourself.
3750
+ You can use browser tools to navigate to ${serverUrl} directly.` : `
3751
+ ### Dev Server
3752
+ The dev server failed to start (e.g. DB connection issues). Skip browser verification and proceed with code-only changes. Do NOT attempt to start the dev server yourself \u2014 it will hang.` : devServer ? `
3745
3753
  ### Dev Server Setup (REQUIRED before browsing)
3746
3754
  You MUST start the dev server before using any browser navigation tools:
3747
3755
  \`\`\`bash
@@ -3963,6 +3971,76 @@ var init_runner_selection = __esm({
3963
3971
  }
3964
3972
  });
3965
3973
 
3974
+ // src/dev-server.ts
3975
+ import { spawn as spawn2 } from "child_process";
3976
+ async function pollReady(url, timeoutSec) {
3977
+ const deadline = Date.now() + timeoutSec * 1e3;
3978
+ while (Date.now() < deadline) {
3979
+ try {
3980
+ const res = await fetch(url, { signal: AbortSignal.timeout(2e3) });
3981
+ if (res.ok || res.status >= 200 && res.status < 400) return true;
3982
+ } catch {
3983
+ }
3984
+ await new Promise((r) => setTimeout(r, 1e3));
3985
+ }
3986
+ return false;
3987
+ }
3988
+ async function startDevServer(opts) {
3989
+ const timeout = opts.readyTimeout ?? 30;
3990
+ const [cmd, ...args2] = opts.command.split(/\s+/);
3991
+ let child;
3992
+ try {
3993
+ child = spawn2(cmd, args2, {
3994
+ stdio: ["ignore", "pipe", "pipe"],
3995
+ detached: true,
3996
+ shell: true,
3997
+ env: { ...process.env, ...opts.envVars }
3998
+ });
3999
+ } catch (err) {
4000
+ logger.warn(` Dev server failed to spawn: ${err instanceof Error ? err.message : String(err)}`);
4001
+ return { ready: false, url: opts.url, pid: void 0, stop: () => {
4002
+ } };
4003
+ }
4004
+ let stderr = "";
4005
+ child.stderr?.on("data", (chunk) => {
4006
+ stderr += chunk.toString();
4007
+ });
4008
+ let exited = false;
4009
+ child.on("exit", () => {
4010
+ exited = true;
4011
+ });
4012
+ child.unref();
4013
+ const ready = await pollReady(opts.url, timeout);
4014
+ if (!ready) {
4015
+ if (exited) {
4016
+ logger.warn(` Dev server exited before becoming ready`);
4017
+ } else {
4018
+ logger.warn(` Dev server did not respond within ${timeout}s at ${opts.url}`);
4019
+ }
4020
+ if (stderr) {
4021
+ logger.warn(` Dev server stderr (last 500 chars): ${stderr.slice(-500)}`);
4022
+ }
4023
+ }
4024
+ const pid = child.pid;
4025
+ const stop = () => {
4026
+ try {
4027
+ if (pid) process.kill(-pid, "SIGTERM");
4028
+ } catch {
4029
+ }
4030
+ try {
4031
+ child.kill("SIGTERM");
4032
+ } catch {
4033
+ }
4034
+ };
4035
+ return { ready, url: opts.url, pid, stop };
4036
+ }
4037
+ var init_dev_server = __esm({
4038
+ "src/dev-server.ts"() {
4039
+ "use strict";
4040
+ init_logger();
4041
+ }
4042
+ });
4043
+
3966
4044
  // src/stages/agent.ts
3967
4045
  import * as fs19 from "fs";
3968
4046
  import * as path17 from "path";
@@ -4020,6 +4098,29 @@ async function executeAgentStage(ctx, def) {
4020
4098
  if (mcpConfigJson) {
4021
4099
  logger.info(` MCP servers enabled for ${def.name}`);
4022
4100
  }
4101
+ let devServerHandle = null;
4102
+ const ds = config.mcp?.devServer;
4103
+ if (mcpConfigJson && ds && taskHasUI(ctx.taskDir)) {
4104
+ logger.info(` Starting dev server: ${ds.command}`);
4105
+ const envVars = {};
4106
+ for (const varName of ds.env ?? []) {
4107
+ if (process.env[varName]) envVars[varName] = process.env[varName];
4108
+ }
4109
+ devServerHandle = await startDevServer({
4110
+ command: ds.command,
4111
+ url: ds.url,
4112
+ readyTimeout: ds.readyTimeout ?? 30,
4113
+ envVars
4114
+ });
4115
+ if (devServerHandle.ready) {
4116
+ logger.info(` Dev server ready at ${ds.url}`);
4117
+ extraEnv.KODY_DEV_SERVER_URL = ds.url;
4118
+ extraEnv.KODY_DEV_SERVER_READY = "true";
4119
+ } else {
4120
+ logger.warn(` Dev server not ready \u2014 Claude will work without browser verification`);
4121
+ extraEnv.KODY_DEV_SERVER_READY = "false";
4122
+ }
4123
+ }
4023
4124
  const runner = getRunnerForStage(ctx, def.name);
4024
4125
  const maxRetries = def.maxRetries ?? 0;
4025
4126
  let lastResult = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
@@ -4047,6 +4148,10 @@ async function executeAgentStage(ctx, def) {
4047
4148
  mcpConfigJson
4048
4149
  });
4049
4150
  }
4151
+ if (devServerHandle) {
4152
+ devServerHandle.stop();
4153
+ logger.info(` Dev server stopped`);
4154
+ }
4050
4155
  if (lastResult.outcome !== "completed") {
4051
4156
  return { outcome: lastResult.outcome, error: lastResult.error, retries };
4052
4157
  }
@@ -4137,6 +4242,7 @@ var init_agent = __esm({
4137
4242
  init_config();
4138
4243
  init_mcp_config();
4139
4244
  init_runner_selection();
4245
+ init_dev_server();
4140
4246
  init_logger();
4141
4247
  SESSION_GROUP = {
4142
4248
  taskify: "explore",
@@ -7689,74 +7795,80 @@ function detectToolsForBootstrap(cwd) {
7689
7795
  (tool) => tool.detect.some((pattern) => fs8.existsSync(path7.join(cwd, pattern)))
7690
7796
  );
7691
7797
  }
7692
- var FRAMEWORK_SKILL_RULES = [
7693
- {
7694
- detect: (deps) => "next" in deps,
7695
- skills: [
7696
- { skill: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices" },
7697
- { skill: "wshobson/agents@nextjs-app-router-patterns", label: "Next.js App Router patterns" }
7698
- ]
7699
- },
7700
- {
7701
- detect: (deps) => "react" in deps && !("next" in deps),
7702
- skills: [
7703
- { skill: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices" }
7704
- ]
7705
- },
7706
- {
7707
- detect: (deps) => "vue" in deps,
7708
- skills: [
7709
- { skill: "antfu/skills@vue", label: "Vue best practices" }
7710
- ]
7711
- },
7712
- {
7713
- detect: (deps) => "svelte" in deps || "@sveltejs/kit" in deps,
7714
- skills: [
7715
- { skill: "ejirocodes/agent-skills@svelte5-best-practices", label: "Svelte best practices" }
7716
- ]
7717
- },
7718
- {
7719
- detect: (deps) => "@angular/core" in deps,
7720
- skills: [
7721
- { skill: "analogjs/angular-skills@angular-component", label: "Angular component patterns" }
7722
- ]
7723
- },
7724
- {
7725
- detect: (deps) => "payload" in deps,
7726
- skills: [
7727
- { skill: "payloadcms/skills@payload", label: "Payload CMS patterns" }
7728
- ]
7729
- },
7730
- {
7731
- detect: (deps) => "tailwindcss" in deps,
7732
- skills: [
7733
- { skill: "wshobson/agents@tailwind-design-system", label: "Tailwind design system" }
7734
- ]
7735
- }
7736
- ];
7737
- function detectFrameworkSkills(cwd) {
7798
+ var FRAMEWORK_KEYWORDS = {
7799
+ next: "nextjs",
7800
+ react: "react",
7801
+ vue: "vue",
7802
+ svelte: "svelte",
7803
+ "@angular/core": "angular",
7804
+ payload: "payload cms",
7805
+ tailwindcss: "tailwind",
7806
+ nuxt: "nuxt",
7807
+ astro: "astro",
7808
+ "solid-js": "solidjs",
7809
+ express: "express",
7810
+ fastify: "fastify",
7811
+ prisma: "prisma"
7812
+ };
7813
+ function parseSkillsSearchOutput(output) {
7814
+ const stripped = output.replace(/\x1b\[[0-9;]*m/g, "");
7815
+ const results = [];
7816
+ for (const line of stripped.split("\n")) {
7817
+ const match = line.match(/^([a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+)\s+([\d.]+)([KM]?)\s+installs/);
7818
+ if (!match) continue;
7819
+ const ref = match[1];
7820
+ const name = ref.split("@").pop() ?? "";
7821
+ let installs = parseFloat(match[2]);
7822
+ if (match[3] === "K") installs *= 1e3;
7823
+ if (match[3] === "M") installs *= 1e6;
7824
+ results.push({ ref, name, installs });
7825
+ }
7826
+ return results;
7827
+ }
7828
+ function detectProjectKeywords(cwd) {
7738
7829
  const pkgPath = path7.join(cwd, "package.json");
7739
7830
  if (!fs8.existsSync(pkgPath)) return [];
7740
7831
  try {
7741
7832
  const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
7742
7833
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7743
- const seen = /* @__PURE__ */ new Set();
7744
- const skills = [];
7745
- for (const rule of FRAMEWORK_SKILL_RULES) {
7746
- if (rule.detect(allDeps)) {
7747
- for (const skill of rule.skills) {
7748
- if (!seen.has(skill.skill)) {
7749
- seen.add(skill.skill);
7750
- skills.push(skill);
7751
- }
7752
- }
7753
- }
7834
+ const keywords = [];
7835
+ for (const [dep, keyword] of Object.entries(FRAMEWORK_KEYWORDS)) {
7836
+ if (dep in allDeps) keywords.push(keyword);
7754
7837
  }
7755
- return skills;
7838
+ return keywords;
7756
7839
  } catch {
7757
7840
  return [];
7758
7841
  }
7759
7842
  }
7843
+ function searchSkills(keywords, exclude, limit) {
7844
+ const allResults = [];
7845
+ const seen = /* @__PURE__ */ new Set();
7846
+ for (const keyword of keywords) {
7847
+ try {
7848
+ const output = execFileSync4("npx", ["skills", "find", keyword], {
7849
+ encoding: "utf-8",
7850
+ timeout: 15e3,
7851
+ stdio: ["pipe", "pipe", "pipe"]
7852
+ });
7853
+ for (const skill of parseSkillsSearchOutput(output)) {
7854
+ if (!seen.has(skill.ref) && !exclude.has(skill.name)) {
7855
+ seen.add(skill.ref);
7856
+ allResults.push(skill);
7857
+ }
7858
+ }
7859
+ } catch {
7860
+ }
7861
+ }
7862
+ return allResults.sort((a, b) => b.installs - a.installs).slice(0, limit);
7863
+ }
7864
+ function collectSkillPaths(cwd, skillName, paths) {
7865
+ for (const dir of [".claude/skills", ".agents/skills"]) {
7866
+ const skillPath = path7.join(dir, skillName);
7867
+ if (fs8.existsSync(path7.join(cwd, skillPath))) {
7868
+ paths.push(skillPath);
7869
+ }
7870
+ }
7871
+ }
7760
7872
  function bootstrapCommand(opts, pkgRoot) {
7761
7873
  const cwd = process.cwd();
7762
7874
  setConfigDir(cwd);
@@ -8213,44 +8325,66 @@ ${entries}
8213
8325
  }
8214
8326
  console.log("\n\u2500\u2500 Skills \u2500\u2500");
8215
8327
  const installedSkillPaths = [];
8216
- const allSkills = [];
8217
- const seen = /* @__PURE__ */ new Set();
8328
+ const excludeSkills = /* @__PURE__ */ new Set();
8329
+ const claudeSkillsDir = path7.join(cwd, ".claude", "skills");
8330
+ if (fs8.existsSync(claudeSkillsDir)) {
8331
+ try {
8332
+ for (const entry of fs8.readdirSync(claudeSkillsDir, { withFileTypes: true })) {
8333
+ if (entry.isDirectory() || entry.isSymbolicLink()) excludeSkills.add(entry.name);
8334
+ }
8335
+ } catch {
8336
+ }
8337
+ }
8218
8338
  for (const tool of detectToolsForBootstrap(cwd)) {
8219
- if (tool.skill && !seen.has(tool.skill)) {
8220
- seen.add(tool.skill);
8221
- allSkills.push({ skill: tool.skill, label: `${tool.name} CLI` });
8339
+ if (tool.skill) {
8340
+ const toolSkillName = tool.skill.split("@").pop() ?? "";
8341
+ excludeSkills.add(toolSkillName);
8222
8342
  }
8223
8343
  }
8224
- for (const mapping of detectFrameworkSkills(cwd)) {
8225
- if (!seen.has(mapping.skill)) {
8226
- seen.add(mapping.skill);
8227
- allSkills.push(mapping);
8344
+ for (const tool of detectToolsForBootstrap(cwd)) {
8345
+ if (!tool.skill) continue;
8346
+ const skillName = tool.skill.split("@").pop() ?? "";
8347
+ if (excludeSkills.has(skillName) && fs8.existsSync(path7.join(claudeSkillsDir, skillName))) continue;
8348
+ try {
8349
+ console.log(` Installing: ${tool.name} CLI (${tool.skill})`);
8350
+ execFileSync4("npx", ["skills", "add", tool.skill, "--yes"], {
8351
+ cwd,
8352
+ encoding: "utf-8",
8353
+ timeout: 6e4,
8354
+ stdio: ["pipe", "pipe", "pipe"]
8355
+ });
8356
+ collectSkillPaths(cwd, skillName, installedSkillPaths);
8357
+ excludeSkills.add(skillName);
8358
+ console.log(` \u2713 ${tool.name} CLI`);
8359
+ } catch {
8360
+ console.log(` \u2717 ${tool.name} CLI \u2014 failed to install`);
8228
8361
  }
8229
8362
  }
8230
- if (allSkills.length > 0) {
8231
- for (const { skill, label } of allSkills) {
8232
- try {
8233
- console.log(` Installing: ${label} (${skill})`);
8234
- execFileSync4("npx", ["skills", "add", skill, "--yes"], {
8235
- cwd,
8236
- encoding: "utf-8",
8237
- timeout: 6e4,
8238
- stdio: ["pipe", "pipe", "pipe"]
8239
- });
8240
- const skillName = skill.split("@").pop() ?? "";
8241
- for (const dir of [".claude/skills", ".agents/skills"]) {
8242
- const skillPath = path7.join(dir, skillName);
8243
- if (fs8.existsSync(path7.join(cwd, skillPath))) {
8244
- installedSkillPaths.push(skillPath);
8245
- }
8363
+ const keywords = detectProjectKeywords(cwd);
8364
+ if (keywords.length > 0) {
8365
+ console.log(` Searching skills.sh for: ${keywords.join(", ")}`);
8366
+ const found = searchSkills(keywords, excludeSkills, 5);
8367
+ if (found.length > 0) {
8368
+ for (const skill of found) {
8369
+ try {
8370
+ console.log(` Installing: ${skill.name} (${skill.ref})`);
8371
+ execFileSync4("npx", ["skills", "add", skill.ref, "--yes"], {
8372
+ cwd,
8373
+ encoding: "utf-8",
8374
+ timeout: 6e4,
8375
+ stdio: ["pipe", "pipe", "pipe"]
8376
+ });
8377
+ collectSkillPaths(cwd, skill.name, installedSkillPaths);
8378
+ console.log(` \u2713 ${skill.name}`);
8379
+ } catch {
8380
+ console.log(` \u2717 ${skill.name} \u2014 failed to install`);
8246
8381
  }
8247
- console.log(` \u2713 ${label}`);
8248
- } catch {
8249
- console.log(` \u2717 ${label} \u2014 failed to install`);
8250
8382
  }
8383
+ } else {
8384
+ console.log(" \u25CB No matching skills found on skills.sh");
8251
8385
  }
8252
8386
  } else {
8253
- console.log(" \u25CB No skills to install (no frameworks detected)");
8387
+ console.log(" \u25CB No frameworks detected \u2014 skipping skill search");
8254
8388
  }
8255
8389
  if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
8256
8390
  installedSkillPaths.push("skills-lock.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine-lite",
3
- "version": "0.1.133",
3
+ "version": "0.1.135",
4
4
  "description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
5
5
  "license": "MIT",
6
6
  "type": "module",