@kody-ade/kody-engine 0.4.42 → 0.4.44

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/dist/bin/kody.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.4.42",
6
+ version: "0.4.44",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -391,6 +391,16 @@ function formatBytes(bytes) {
391
391
 
392
392
  // src/agent.ts
393
393
  var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
394
+ var DEFAULT_TURN_TIMEOUT_MS = 3e5;
395
+ function resolveTurnTimeoutMs(opts) {
396
+ if (opts.maxTurnTimeoutMs !== void 0 && opts.maxTurnTimeoutMs !== null) {
397
+ return opts.maxTurnTimeoutMs > 0 ? opts.maxTurnTimeoutMs : 0;
398
+ }
399
+ const envSec = Number(process.env.KODY_TURN_TIMEOUT_SEC);
400
+ if (Number.isFinite(envSec) && envSec > 0) return Math.floor(envSec * 1e3);
401
+ if (Number.isFinite(envSec) && envSec <= 0) return 0;
402
+ return DEFAULT_TURN_TIMEOUT_MS;
403
+ }
394
404
  async function runAgent(opts) {
395
405
  const ndjsonDir = opts.ndjsonDir ?? path3.join(opts.cwd, ".kody");
396
406
  fs3.mkdirSync(ndjsonDir, { recursive: true });
@@ -420,6 +430,9 @@ async function runAgent(opts) {
420
430
  const tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
421
431
  let messageCount = 0;
422
432
  const startedAt = Date.now();
433
+ const turnTimeoutMs = resolveTurnTimeoutMs(opts);
434
+ let ndjsonWriteFailed = false;
435
+ let ndjsonWriteError;
423
436
  try {
424
437
  const queryOptions = {
425
438
  model: opts.model.model,
@@ -456,12 +469,44 @@ async function runAgent(opts) {
456
469
  // biome-ignore lint/suspicious/noExplicitAny: SDK options type is narrow; mcpServers is runtime-passthrough.
457
470
  options: queryOptions
458
471
  });
459
- for await (const msg of result) {
472
+ const iterator = typeof result[Symbol.asyncIterator] === "function" ? result[Symbol.asyncIterator]() : result;
473
+ while (true) {
474
+ const nextPromise = iterator.next();
475
+ let timedOut = false;
476
+ let timer;
477
+ let next;
478
+ if (turnTimeoutMs > 0) {
479
+ const timeoutPromise = new Promise((resolve4) => {
480
+ timer = setTimeout(() => {
481
+ timedOut = true;
482
+ resolve4({ done: true, value: void 0 });
483
+ }, turnTimeoutMs);
484
+ });
485
+ next = await Promise.race([nextPromise, timeoutPromise]);
486
+ if (timer) clearTimeout(timer);
487
+ } else {
488
+ next = await nextPromise;
489
+ }
490
+ if (timedOut) {
491
+ outcome = "failed";
492
+ errorMessage = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
493
+ if (typeof iterator.return === "function") {
494
+ try {
495
+ await iterator.return(void 0);
496
+ } catch {
497
+ }
498
+ }
499
+ break;
500
+ }
501
+ if (next.done) break;
502
+ const msg = next.value;
460
503
  messageCount++;
461
504
  try {
462
505
  fullLog.write(`${JSON.stringify(msg)}
463
506
  `);
464
- } catch {
507
+ } catch (e) {
508
+ ndjsonWriteFailed = true;
509
+ ndjsonWriteError = e instanceof Error ? e.message : String(e);
465
510
  }
466
511
  const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
467
512
  if (line) process.stdout.write(`${line}
@@ -498,6 +543,10 @@ async function runAgent(opts) {
498
543
  } catch {
499
544
  }
500
545
  }
546
+ if (ndjsonWriteFailed) {
547
+ process.stderr.write(`[kody agent] NDJSON write failed (post-mortem may be incomplete): ${ndjsonWriteError ?? "unknown error"}
548
+ `);
549
+ }
501
550
  const finalText = resultTexts.join("\n\n---\n\n");
502
551
  return {
503
552
  outcome,
@@ -2018,6 +2067,13 @@ async function checkLitellmHealth(url) {
2018
2067
  return false;
2019
2068
  }
2020
2069
  }
2070
+ var DEFAULT_LITELLM_STARTUP_TIMEOUT_SEC = 60;
2071
+ var LITELLM_HEALTH_POLL_INTERVAL_MS = 2e3;
2072
+ function resolveLitellmTimeoutMs() {
2073
+ const envSec = Number(process.env.KODY_LITELLM_TIMEOUT_SEC);
2074
+ if (Number.isFinite(envSec) && envSec > 0) return Math.floor(envSec * 1e3);
2075
+ return DEFAULT_LITELLM_STARTUP_TIMEOUT_SEC * 1e3;
2076
+ }
2021
2077
  function generateLitellmConfigYaml(model) {
2022
2078
  const apiKeyVar = providerApiKeyEnvVar(model.provider);
2023
2079
  return [
@@ -2063,8 +2119,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
2063
2119
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
2064
2120
  });
2065
2121
  fs10.closeSync(outFd);
2066
- for (let i = 0; i < 30; i++) {
2067
- await new Promise((r) => setTimeout(r, 2e3));
2122
+ const timeoutMs = resolveLitellmTimeoutMs();
2123
+ const deadline = Date.now() + timeoutMs;
2124
+ while (Date.now() < deadline) {
2125
+ await new Promise((r) => setTimeout(r, LITELLM_HEALTH_POLL_INTERVAL_MS));
2068
2126
  if (await checkLitellmHealth(url)) {
2069
2127
  return {
2070
2128
  url,
@@ -2086,7 +2144,8 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
2086
2144
  child.kill();
2087
2145
  } catch {
2088
2146
  }
2089
- throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
2147
+ const seconds = Math.round(timeoutMs / 1e3);
2148
+ throw new Error(`LiteLLM proxy failed to start within ${seconds}s (KODY_LITELLM_TIMEOUT_SEC overrides). Log tail:
2090
2149
  ${logTail}`);
2091
2150
  }
2092
2151
  function readDotenvApiKeys(projectDir) {
@@ -9100,7 +9159,8 @@ async function runExecutable(profileName, input) {
9100
9159
  return finishAndEnd({ exitCode: 99, reason: `config error: ${err instanceof Error ? err.message : String(err)}` });
9101
9160
  }
9102
9161
  }
9103
- const modelSpec = profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
9162
+ const perExecutableModel = config.agent.perExecutable?.[profileName];
9163
+ const modelSpec = perExecutableModel ? perExecutableModel : profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
9104
9164
  let model;
9105
9165
  try {
9106
9166
  model = parseProviderModel(modelSpec);
@@ -10126,8 +10186,8 @@ async function runScheduledFanOut(cwd, args, opts) {
10126
10186
  return 99;
10127
10187
  }
10128
10188
  const config = loadConfig(cwd);
10129
- let worstExit = 0;
10130
- for (const match of matches) {
10189
+ const serial = process.env.KODY_SERIAL_WATCHES === "1";
10190
+ const runWatch = async (match) => {
10131
10191
  process.stdout.write(`
10132
10192
  \u2192 kody: running watch \`${match.executable}\`
10133
10193
  `);
@@ -10144,13 +10204,27 @@ async function runScheduledFanOut(cwd, args, opts) {
10144
10204
  `[kody] watch \`${match.executable}\` exited ${result.exitCode}: ${result.reason ?? "(no reason)"}
10145
10205
  `
10146
10206
  );
10147
- if (result.exitCode > worstExit) worstExit = result.exitCode;
10207
+ return result.exitCode;
10148
10208
  }
10209
+ return 0;
10149
10210
  } catch (err) {
10150
10211
  const msg = err instanceof Error ? err.message : String(err);
10151
10212
  process.stderr.write(`[kody] watch \`${match.executable}\` crashed: ${msg}
10152
10213
  `);
10153
- worstExit = Math.max(worstExit, 99);
10214
+ return 99;
10215
+ }
10216
+ };
10217
+ let worstExit = 0;
10218
+ if (serial) {
10219
+ for (const match of matches) {
10220
+ const code = await runWatch(match);
10221
+ if (code > worstExit) worstExit = code;
10222
+ }
10223
+ } else {
10224
+ const settled = await Promise.allSettled(matches.map((m) => runWatch(m)));
10225
+ for (const r of settled) {
10226
+ const code = r.status === "fulfilled" ? r.value : 99;
10227
+ if (code > worstExit) worstExit = code;
10154
10228
  }
10155
10229
  }
10156
10230
  return worstExit;
File without changes
@@ -20,34 +20,16 @@
20
20
  "Read",
21
21
  "Grep",
22
22
  "Glob",
23
- "Bash",
24
- "mcp__playwright"
23
+ "Bash"
25
24
  ],
26
25
  "hooks": ["block-write"],
27
26
  "skills": [],
28
27
  "commands": [],
29
28
  "subagents": [],
30
29
  "plugins": [],
31
- "mcpServers": [
32
- {
33
- "name": "playwright",
34
- "command": "npx",
35
- "args": ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp"]
36
- }
37
- ]
30
+ "mcpServers": []
38
31
  },
39
- "cliTools": [
40
- {
41
- "name": "playwright",
42
- "install": {
43
- "required": false,
44
- "checkCommand": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
45
- "installCommand": "npx --yes playwright install --with-deps chromium"
46
- },
47
- "verify": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
48
- "usage": ""
49
- }
50
- ],
32
+ "cliTools": [],
51
33
  "scripts": {
52
34
  "preflight": [
53
35
  {
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -21,34 +21,16 @@
21
21
  "Read",
22
22
  "Grep",
23
23
  "Glob",
24
- "Bash",
25
- "mcp__playwright"
24
+ "Bash"
26
25
  ],
27
26
  "hooks": ["block-write"],
28
27
  "skills": [],
29
28
  "commands": [],
30
29
  "subagents": [],
31
30
  "plugins": [],
32
- "mcpServers": [
33
- {
34
- "name": "playwright",
35
- "command": "npx",
36
- "args": ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp"]
37
- }
38
- ]
31
+ "mcpServers": []
39
32
  },
40
- "cliTools": [
41
- {
42
- "name": "playwright",
43
- "install": {
44
- "required": false,
45
- "checkCommand": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
46
- "installCommand": "npx --yes playwright install --with-deps chromium"
47
- },
48
- "verify": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
49
- "usage": ""
50
- }
51
- ],
33
+ "cliTools": [],
52
34
  "scripts": {
53
35
  "preflight": [
54
36
  {
@@ -135,6 +135,14 @@
135
135
  "description": "Single 'provider/model' string used by kody (single-session pipeline). Use 'claude/...' or 'anthropic/...' for direct Anthropic API; anything else routes through LiteLLM proxy.",
136
136
  "examples": ["claude/claude-sonnet-4-6", "minimax/MiniMax-M2.7-highspeed"]
137
137
  },
138
+ "perExecutable": {
139
+ "type": "object",
140
+ "description": "Per-executable model override. Wins over agent.model for the matching stage. Example: {\"classify\": \"claude/claude-haiku-4-5-20251001\", \"plan\": \"claude/claude-opus-4-7\"}.",
141
+ "additionalProperties": {
142
+ "type": "string",
143
+ "pattern": "^[^/]+/.+$"
144
+ }
145
+ },
138
146
  "modelMap": {
139
147
  "type": "object",
140
148
  "description": "Maps model tiers to 'provider/model' strings. Use 'claude/...' or 'anthropic/...' for direct Anthropic API; anything else routes through LiteLLM proxy.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.42",
3
+ "version": "0.4.44",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,18 +12,6 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
- "scripts": {
16
- "kody": "tsx bin/kody.ts",
17
- "build": "tsup && node scripts/copy-assets.cjs",
18
- "test": "vitest run tests/unit tests/int --no-coverage",
19
- "test:e2e": "vitest run tests/e2e --no-coverage",
20
- "test:all": "vitest run tests --no-coverage",
21
- "typecheck": "tsc --noEmit",
22
- "lint": "biome check",
23
- "lint:fix": "biome check --write",
24
- "format": "biome format --write",
25
- "prepublishOnly": "pnpm build"
26
- },
27
15
  "dependencies": {
28
16
  "@actions/cache": "^6.0.0",
29
17
  "@anthropic-ai/claude-agent-sdk": "0.2.119"
@@ -44,5 +32,16 @@
44
32
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
45
33
  },
46
34
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
47
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
48
- }
35
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
36
+ "scripts": {
37
+ "kody": "tsx bin/kody.ts",
38
+ "build": "tsup && node scripts/copy-assets.cjs",
39
+ "test": "vitest run tests/unit tests/int --no-coverage",
40
+ "test:e2e": "vitest run tests/e2e --no-coverage",
41
+ "test:all": "vitest run tests --no-coverage",
42
+ "typecheck": "tsc --noEmit",
43
+ "lint": "biome check",
44
+ "lint:fix": "biome check --write",
45
+ "format": "biome format --write"
46
+ }
47
+ }