@kody-ade/kody-engine 0.2.32 → 0.2.33

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/kody2.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.2.32",
6
+ version: "0.2.33",
7
7
  description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -50,81 +50,115 @@ var package_default = {
50
50
  };
51
51
 
52
52
  // src/chat-cli.ts
53
- import { execFileSync as execFileSync16 } from "child_process";
54
- import * as fs19 from "fs";
55
- import * as path16 from "path";
53
+ import * as path14 from "path";
56
54
 
57
- // src/chat/events.ts
58
- import * as fs from "fs";
59
- import * as path from "path";
60
- function eventsFilePath(cwd, sessionId) {
61
- return path.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
55
+ // src/chat/pull.ts
56
+ var PullError = class extends Error {
57
+ constructor(message, status) {
58
+ super(message);
59
+ this.status = status;
60
+ this.name = "PullError";
61
+ }
62
+ status;
63
+ };
64
+ function createPullClient(opts) {
65
+ const fetchFn = opts.fetchFn ?? fetch;
66
+ const parsed = parseUrl(opts.baseUrl);
67
+ const token = opts.token ?? parsed.token;
68
+ if (!token) {
69
+ throw new PullError("session token not provided (expected inline ?token= in dashboardUrl)");
70
+ }
71
+ return async function pull(since, timeoutMs) {
72
+ const url = new URL(parsed.origin);
73
+ url.pathname = "/api/kody/chat/pull";
74
+ url.searchParams.set("sessionId", opts.sessionId);
75
+ url.searchParams.set("since", String(since));
76
+ url.searchParams.set("timeoutMs", String(timeoutMs));
77
+ url.searchParams.set("token", token);
78
+ const abort = AbortSignal.timeout(timeoutMs + 1e4);
79
+ const res = await fetchFn(url.toString(), {
80
+ method: "GET",
81
+ headers: { authorization: `Bearer ${token}` },
82
+ signal: abort
83
+ });
84
+ if (!res.ok) {
85
+ const body = await res.text().catch(() => "");
86
+ throw new PullError(`pull ${url.pathname} \u2192 ${res.status}: ${body.slice(0, 200)}`, res.status);
87
+ }
88
+ const data = await res.json();
89
+ return data;
90
+ };
62
91
  }
63
- var FileSink = class {
64
- constructor(file) {
65
- this.file = file;
66
- }
67
- file;
68
- async emit(event) {
69
- fs.mkdirSync(path.dirname(this.file), { recursive: true });
70
- fs.appendFileSync(this.file, `${JSON.stringify(event)}
71
- `);
92
+ function parseUrl(baseUrl) {
93
+ try {
94
+ const u = new URL(baseUrl);
95
+ const token = u.searchParams.get("token");
96
+ const origin = `${u.protocol}//${u.host}${u.pathname !== "/" ? u.pathname : ""}`.replace(/\/$/, "");
97
+ return { origin: origin || u.origin, token };
98
+ } catch {
99
+ return { origin: baseUrl, token: null };
72
100
  }
73
- };
101
+ }
102
+
103
+ // src/chat/events.ts
74
104
  var HttpSink = class {
75
- constructor(baseUrl, sessionId, logger = {
105
+ constructor(baseUrl, sessionId, token, fetchFn = fetch, logger = {
76
106
  warn: (m) => process.stderr.write(`[kody2:chat] ${m}
77
107
  `)
78
108
  }) {
79
- this.baseUrl = baseUrl;
80
109
  this.sessionId = sessionId;
110
+ this.fetchFn = fetchFn;
81
111
  this.logger = logger;
112
+ const parsed = parseUrl(baseUrl);
113
+ this.origin = parsed.origin;
114
+ const resolved = token ?? parsed.token;
115
+ if (!resolved) {
116
+ throw new Error("HttpSink: session token not provided (expected inline ?token= in baseUrl)");
117
+ }
118
+ this.token = resolved;
82
119
  }
83
- baseUrl;
84
120
  sessionId;
121
+ fetchFn;
85
122
  logger;
123
+ origin;
124
+ token;
86
125
  async emit(event) {
87
- const url = withSessionParam(this.baseUrl, this.sessionId);
126
+ const url = new URL(this.origin);
127
+ url.pathname = "/api/kody/events/ingest";
128
+ url.searchParams.set("sessionId", this.sessionId);
129
+ url.searchParams.set("token", this.token);
88
130
  try {
89
- const res = await fetch(url, {
131
+ const res = await this.fetchFn(url.toString(), {
90
132
  method: "POST",
91
- headers: { "content-type": "application/json" },
133
+ headers: {
134
+ "content-type": "application/json",
135
+ authorization: `Bearer ${this.token}`
136
+ },
92
137
  body: JSON.stringify(event),
93
138
  signal: AbortSignal.timeout(5e3)
94
139
  });
95
140
  if (!res.ok) {
96
- this.logger.warn(`HttpSink POST ${url} \u2192 ${res.status}`);
141
+ this.logger.warn(`HttpSink POST ${url.pathname} \u2192 ${res.status}`);
97
142
  }
98
143
  } catch (err) {
99
- this.logger.warn(`HttpSink POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
144
+ this.logger.warn(
145
+ `HttpSink POST ${url.pathname} failed: ${err instanceof Error ? err.message : String(err)}`
146
+ );
100
147
  }
101
148
  }
102
149
  };
103
- var TeeSink = class {
104
- constructor(sinks) {
105
- this.sinks = sinks;
106
- }
107
- sinks;
108
- async emit(event) {
109
- await Promise.all(this.sinks.map((s) => s.emit(event)));
110
- }
111
- };
112
- function withSessionParam(baseUrl, sessionId) {
113
- const joiner = baseUrl.includes("?") ? "&" : "?";
114
- return `${baseUrl}${joiner}sessionId=${encodeURIComponent(sessionId)}`;
115
- }
116
150
  function makeRunId(sessionId, suffix) {
117
151
  return `chat-${sessionId}-${suffix}`;
118
152
  }
119
153
 
120
154
  // src/agent.ts
121
- import * as fs3 from "fs";
122
- import * as path3 from "path";
155
+ import * as fs2 from "fs";
156
+ import * as path2 from "path";
123
157
  import { query } from "@anthropic-ai/claude-agent-sdk";
124
158
 
125
159
  // src/config.ts
126
- import * as fs2 from "fs";
127
- import * as path2 from "path";
160
+ import * as fs from "fs";
161
+ import * as path from "path";
128
162
  var LITELLM_DEFAULT_PORT = 4e3;
129
163
  var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
130
164
  function parseProviderModel(s) {
@@ -142,13 +176,13 @@ function needsLitellmProxy(model) {
142
176
  return model.provider !== "claude" && model.provider !== "anthropic";
143
177
  }
144
178
  function loadConfig(projectDir = process.cwd()) {
145
- const configPath = path2.join(projectDir, "kody.config.json");
146
- if (!fs2.existsSync(configPath)) {
179
+ const configPath = path.join(projectDir, "kody.config.json");
180
+ if (!fs.existsSync(configPath)) {
147
181
  throw new Error(`kody.config.json not found at ${configPath}`);
148
182
  }
149
183
  let raw;
150
184
  try {
151
- raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
185
+ raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
152
186
  } catch (err) {
153
187
  const msg = err instanceof Error ? err.message : String(err);
154
188
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
@@ -325,10 +359,10 @@ function formatBytes(bytes) {
325
359
  // src/agent.ts
326
360
  var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
327
361
  async function runAgent(opts) {
328
- const ndjsonDir = opts.ndjsonDir ?? path3.join(opts.cwd, ".kody2");
329
- fs3.mkdirSync(ndjsonDir, { recursive: true });
330
- const ndjsonPath = path3.join(ndjsonDir, "last-run.jsonl");
331
- const fullLog = fs3.createWriteStream(ndjsonPath, { flags: "w" });
362
+ const ndjsonDir = opts.ndjsonDir ?? path2.join(opts.cwd, ".kody2");
363
+ fs2.mkdirSync(ndjsonDir, { recursive: true });
364
+ const ndjsonPath = path2.join(ndjsonDir, "last-run.jsonl");
365
+ const fullLog = fs2.createWriteStream(ndjsonPath, { flags: "w" });
332
366
  const env = {
333
367
  ...process.env,
334
368
  SKIP_HOOKS: "1",
@@ -359,6 +393,9 @@ async function runAgent(opts) {
359
393
  if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
360
394
  queryOptions.maxTurns = opts.maxTurns;
361
395
  }
396
+ if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
397
+ queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
398
+ }
362
399
  if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
363
400
  queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: opts.systemPromptAppend };
364
401
  }
@@ -400,53 +437,6 @@ async function runAgent(opts) {
400
437
  return { outcome, finalText, error: errorMessage, ndjsonPath };
401
438
  }
402
439
 
403
- // src/chat/session.ts
404
- import * as fs4 from "fs";
405
- import * as path4 from "path";
406
- function sessionFilePath(cwd, sessionId) {
407
- return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
408
- }
409
- function readSession(file) {
410
- if (!fs4.existsSync(file)) return [];
411
- const raw = fs4.readFileSync(file, "utf-8").trim();
412
- if (!raw) return [];
413
- const turns = [];
414
- for (const line of raw.split("\n")) {
415
- if (!line.trim()) continue;
416
- try {
417
- const parsed = JSON.parse(line);
418
- if (parsed.role !== "user" && parsed.role !== "assistant") continue;
419
- if (typeof parsed.content !== "string") continue;
420
- turns.push(parsed);
421
- } catch {
422
- }
423
- }
424
- return turns;
425
- }
426
- function appendTurn(file, turn) {
427
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
428
- const line = JSON.stringify({
429
- role: turn.role,
430
- content: turn.content,
431
- timestamp: turn.timestamp,
432
- toolCalls: turn.toolCalls ?? []
433
- });
434
- fs4.appendFileSync(file, `${line}
435
- `);
436
- }
437
- function seedInitialMessage(file, message) {
438
- if (!message.trim()) return false;
439
- const turns = readSession(file);
440
- const lastUser = [...turns].reverse().find((t) => t.role === "user");
441
- if (lastUser && lastUser.content === message) return false;
442
- appendTurn(file, {
443
- role: "user",
444
- content: message,
445
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
446
- });
447
- return true;
448
- }
449
-
450
440
  // src/chat/loop.ts
451
441
  var CHAT_SYSTEM_PROMPT = [
452
442
  "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
@@ -455,20 +445,15 @@ var CHAT_SYSTEM_PROMPT = [
455
445
  "read repository code or execute small checks when it helps you answer \u2014 otherwise",
456
446
  "reply directly. Do not invent file paths, commit SHAs, or command output."
457
447
  ].join("\n");
458
- async function runChatTurn(opts) {
459
- const turns = readSession(opts.sessionFile);
460
- if (turns.length === 0) {
461
- const error = "session file is empty \u2014 nothing to reply to";
462
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
463
- return { exitCode: 64, error };
464
- }
465
- const lastTurn = turns[turns.length - 1];
466
- if (lastTurn.role !== "user") {
467
- const error = "last turn is not a user message \u2014 assistant already replied";
468
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
469
- return { exitCode: 64, error };
470
- }
471
- const prompt = buildPrompt(turns, opts.systemPrompt ?? CHAT_SYSTEM_PROMPT);
448
+ var DEFAULT_IDLE_TIMEOUT_MS = 3 * 60 * 1e3;
449
+ var DEFAULT_HARD_TIMEOUT_MS = 5 * 60 * 60 * 1e3;
450
+ var DEFAULT_PULL_TIMEOUT_MS = 25e3;
451
+ async function runChatSession(opts) {
452
+ const now = opts.now ?? (() => Date.now());
453
+ const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
454
+ const hardTimeoutMs = opts.hardTimeoutMs ?? DEFAULT_HARD_TIMEOUT_MS;
455
+ const pullTimeoutMs = opts.pullTimeoutMs ?? DEFAULT_PULL_TIMEOUT_MS;
456
+ const systemPrompt = opts.systemPrompt ?? CHAT_SYSTEM_PROMPT;
472
457
  const invoke = opts.invokeAgent ?? ((p) => runAgent({
473
458
  prompt: p,
474
459
  model: opts.model,
@@ -477,34 +462,68 @@ async function runChatTurn(opts) {
477
462
  verbose: opts.verbose,
478
463
  quiet: opts.quiet
479
464
  }));
480
- let result;
481
- try {
482
- result = await invoke(prompt);
483
- } catch (err) {
484
- const error = err instanceof Error ? err.message : String(err);
485
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
486
- return { exitCode: 99, error };
487
- }
488
- if (result.outcome !== "completed") {
489
- const error = result.error ?? "agent did not complete";
490
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
491
- return { exitCode: 99, error };
465
+ const history = [];
466
+ let since = 0;
467
+ let lastActivityAt = now();
468
+ const startedAt = now();
469
+ let turnsProcessed = 0;
470
+ while (true) {
471
+ if (now() - startedAt > hardTimeoutMs) {
472
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", {
473
+ sessionId: opts.sessionId,
474
+ reason: "hard-timeout"
475
+ });
476
+ return { exitCode: 0, turnsProcessed, reason: "hard-timeout" };
477
+ }
478
+ let response;
479
+ try {
480
+ response = await opts.pull(since, pullTimeoutMs);
481
+ } catch (err) {
482
+ const msg = err instanceof Error ? err.message : String(err);
483
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: `pull failed: ${msg}` });
484
+ return { exitCode: 99, turnsProcessed, reason: `pull failed: ${msg}` };
485
+ }
486
+ if (response.turns.length === 0) {
487
+ if (now() - lastActivityAt > idleTimeoutMs) {
488
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", {
489
+ sessionId: opts.sessionId,
490
+ reason: "idle-timeout"
491
+ });
492
+ return { exitCode: 0, turnsProcessed, reason: "idle-timeout" };
493
+ }
494
+ continue;
495
+ }
496
+ const newUserTurns = response.turns.filter((t) => t.role === "user");
497
+ for (const t of newUserTurns) history.push(t);
498
+ since = response.nextSince;
499
+ if (newUserTurns.length === 0) continue;
500
+ lastActivityAt = now();
501
+ const prompt = buildPrompt(history, systemPrompt);
502
+ let result;
503
+ try {
504
+ result = await invoke(prompt);
505
+ } catch (err) {
506
+ const msg = err instanceof Error ? err.message : String(err);
507
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: msg });
508
+ return { exitCode: 99, turnsProcessed, reason: msg };
509
+ }
510
+ if (result.outcome !== "completed") {
511
+ const error = result.error ?? "agent did not complete";
512
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
513
+ return { exitCode: 99, turnsProcessed, reason: error };
514
+ }
515
+ const reply = result.finalText.trim();
516
+ const replyTimestamp = (/* @__PURE__ */ new Date()).toISOString();
517
+ history.push({ role: "assistant", content: reply, timestamp: replyTimestamp });
518
+ turnsProcessed++;
519
+ lastActivityAt = now();
520
+ await emit(opts.sink, "chat.message", opts.sessionId, `message-${turnsProcessed}`, {
521
+ sessionId: opts.sessionId,
522
+ role: "assistant",
523
+ content: reply,
524
+ timestamp: replyTimestamp
525
+ });
492
526
  }
493
- const reply = result.finalText.trim();
494
- const now = (/* @__PURE__ */ new Date()).toISOString();
495
- appendTurn(opts.sessionFile, {
496
- role: "assistant",
497
- content: reply,
498
- timestamp: now
499
- });
500
- await emit(opts.sink, "chat.message", opts.sessionId, "message", {
501
- sessionId: opts.sessionId,
502
- role: "assistant",
503
- content: reply,
504
- timestamp: now
505
- });
506
- await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
507
- return { exitCode: 0, reply };
508
527
  }
509
528
  function buildPrompt(turns, systemPrompt) {
510
529
  const header = `System: ${systemPrompt}`;
@@ -526,11 +545,11 @@ async function emit(sink, type, sessionId, suffix, payload) {
526
545
 
527
546
  // src/kody2-cli.ts
528
547
  import { execFileSync as execFileSync15 } from "child_process";
529
- import * as fs18 from "fs";
530
- import * as path15 from "path";
548
+ import * as fs16 from "fs";
549
+ import * as path13 from "path";
531
550
 
532
551
  // src/dispatch.ts
533
- import * as fs5 from "fs";
552
+ import * as fs3 from "fs";
534
553
  function autoDispatch(opts) {
535
554
  const explicit = opts?.explicit;
536
555
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
@@ -542,10 +561,10 @@ function autoDispatch(opts) {
542
561
  }
543
562
  const eventName = process.env.GITHUB_EVENT_NAME;
544
563
  const eventPath = process.env.GITHUB_EVENT_PATH;
545
- if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
564
+ if (!eventName || !eventPath || !fs3.existsSync(eventPath)) return null;
546
565
  let event = {};
547
566
  try {
548
- event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
567
+ event = JSON.parse(fs3.readFileSync(eventPath, "utf-8"));
549
568
  } catch {
550
569
  return null;
551
570
  }
@@ -614,14 +633,14 @@ function extractFeedback(afterTag) {
614
633
  }
615
634
 
616
635
  // src/executor.ts
617
- import * as fs17 from "fs";
618
- import * as path14 from "path";
636
+ import * as fs15 from "fs";
637
+ import * as path12 from "path";
619
638
 
620
639
  // src/litellm.ts
621
640
  import { execFileSync, spawn } from "child_process";
622
- import * as fs6 from "fs";
641
+ import * as fs4 from "fs";
623
642
  import * as os from "os";
624
- import * as path5 from "path";
643
+ import * as path3 from "path";
625
644
  async function checkLitellmHealth(url) {
626
645
  try {
627
646
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -661,20 +680,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
661
680
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
662
681
  }
663
682
  }
664
- const configPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
665
- fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
683
+ const configPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
684
+ fs4.writeFileSync(configPath, generateLitellmConfigYaml(model));
666
685
  const portMatch = url.match(/:(\d+)/);
667
686
  const port = portMatch ? portMatch[1] : "4000";
668
687
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
669
688
  const dotenvVars = readDotenvApiKeys(projectDir);
670
- const logPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
671
- const outFd = fs6.openSync(logPath, "w");
689
+ const logPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
690
+ const outFd = fs4.openSync(logPath, "w");
672
691
  const child = spawn(cmd, args, {
673
692
  stdio: ["ignore", outFd, outFd],
674
693
  detached: true,
675
694
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
676
695
  });
677
- fs6.closeSync(outFd);
696
+ fs4.closeSync(outFd);
678
697
  for (let i = 0; i < 30; i++) {
679
698
  await new Promise((r) => setTimeout(r, 2e3));
680
699
  if (await checkLitellmHealth(url)) {
@@ -691,7 +710,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
691
710
  }
692
711
  let logTail = "";
693
712
  try {
694
- logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
713
+ logTail = fs4.readFileSync(logPath, "utf-8").slice(-2e3);
695
714
  } catch {
696
715
  }
697
716
  try {
@@ -702,10 +721,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
702
721
  ${logTail}`);
703
722
  }
704
723
  function readDotenvApiKeys(projectDir) {
705
- const dotenvPath = path5.join(projectDir, ".env");
706
- if (!fs6.existsSync(dotenvPath)) return {};
724
+ const dotenvPath = path3.join(projectDir, ".env");
725
+ if (!fs4.existsSync(dotenvPath)) return {};
707
726
  const result = {};
708
- for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
727
+ for (const rawLine of fs4.readFileSync(dotenvPath, "utf-8").split("\n")) {
709
728
  const line = rawLine.trim();
710
729
  if (!line || line.startsWith("#")) continue;
711
730
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -728,8 +747,8 @@ function stripBlockingEnv(env) {
728
747
  }
729
748
 
730
749
  // src/profile.ts
731
- import * as fs7 from "fs";
732
- import * as path6 from "path";
750
+ import * as fs5 from "fs";
751
+ import * as path4 from "path";
733
752
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
734
753
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
735
754
  var ProfileError = class extends Error {
@@ -742,12 +761,12 @@ var ProfileError = class extends Error {
742
761
  profilePath;
743
762
  };
744
763
  function loadProfile(profilePath) {
745
- if (!fs7.existsSync(profilePath)) {
764
+ if (!fs5.existsSync(profilePath)) {
746
765
  throw new ProfileError(profilePath, "file not found");
747
766
  }
748
767
  let raw;
749
768
  try {
750
- raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
769
+ raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
751
770
  } catch (err) {
752
771
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
753
772
  }
@@ -771,7 +790,7 @@ function loadProfile(profilePath) {
771
790
  outputContract: r.outputContract,
772
791
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
773
792
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
774
- dir: path6.dirname(profilePath)
793
+ dir: path4.dirname(profilePath)
775
794
  };
776
795
  return profile;
777
796
  }
@@ -837,6 +856,7 @@ function parseClaudeCode(p, raw) {
837
856
  model: typeof r.model === "string" ? r.model : "inherit",
838
857
  permissionMode,
839
858
  maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
859
+ maxThinkingTokens: typeof r.maxThinkingTokens === "number" ? r.maxThinkingTokens : null,
840
860
  systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
841
861
  tools,
842
862
  hooks: Array.isArray(r.hooks) ? r.hooks : [],
@@ -950,21 +970,21 @@ function parseScriptList(p, key, raw) {
950
970
  }
951
971
 
952
972
  // src/scripts/buildSyntheticPlugin.ts
953
- import * as fs8 from "fs";
973
+ import * as fs6 from "fs";
954
974
  import * as os2 from "os";
955
- import * as path7 from "path";
975
+ import * as path5 from "path";
956
976
  function getPluginsCatalogRoot() {
957
- const here = path7.dirname(new URL(import.meta.url).pathname);
977
+ const here = path5.dirname(new URL(import.meta.url).pathname);
958
978
  const candidates = [
959
- path7.join(here, "..", "plugins"),
979
+ path5.join(here, "..", "plugins"),
960
980
  // dev: src/scripts → src/plugins
961
- path7.join(here, "..", "..", "plugins"),
981
+ path5.join(here, "..", "..", "plugins"),
962
982
  // built: dist/scripts → dist/plugins
963
- path7.join(here, "..", "..", "src", "plugins")
983
+ path5.join(here, "..", "..", "src", "plugins")
964
984
  // fallback
965
985
  ];
966
986
  for (const c of candidates) {
967
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
987
+ if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
968
988
  }
969
989
  return candidates[0];
970
990
  }
@@ -974,50 +994,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
974
994
  if (!needsSynthetic) return;
975
995
  const catalog = getPluginsCatalogRoot();
976
996
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
977
- const root = path7.join(os2.tmpdir(), `kody2-synth-${runId}`);
978
- fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
997
+ const root = path5.join(os2.tmpdir(), `kody2-synth-${runId}`);
998
+ fs6.mkdirSync(path5.join(root, ".claude-plugin"), { recursive: true });
979
999
  if (cc.skills.length > 0) {
980
- const dst = path7.join(root, "skills");
981
- fs8.mkdirSync(dst, { recursive: true });
1000
+ const dst = path5.join(root, "skills");
1001
+ fs6.mkdirSync(dst, { recursive: true });
982
1002
  for (const name of cc.skills) {
983
- const src = path7.join(catalog, "skills", name);
984
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
985
- copyDir(src, path7.join(dst, name));
1003
+ const src = path5.join(catalog, "skills", name);
1004
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1005
+ copyDir(src, path5.join(dst, name));
986
1006
  }
987
1007
  }
988
1008
  if (cc.commands.length > 0) {
989
- const dst = path7.join(root, "commands");
990
- fs8.mkdirSync(dst, { recursive: true });
1009
+ const dst = path5.join(root, "commands");
1010
+ fs6.mkdirSync(dst, { recursive: true });
991
1011
  for (const name of cc.commands) {
992
- const src = path7.join(catalog, "commands", `${name}.md`);
993
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
994
- fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1012
+ const src = path5.join(catalog, "commands", `${name}.md`);
1013
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1014
+ fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
995
1015
  }
996
1016
  }
997
1017
  if (cc.subagents.length > 0) {
998
- const dst = path7.join(root, "agents");
999
- fs8.mkdirSync(dst, { recursive: true });
1018
+ const dst = path5.join(root, "agents");
1019
+ fs6.mkdirSync(dst, { recursive: true });
1000
1020
  for (const name of cc.subagents) {
1001
- const src = path7.join(catalog, "agents", `${name}.md`);
1002
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1003
- fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1021
+ const src = path5.join(catalog, "agents", `${name}.md`);
1022
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1023
+ fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
1004
1024
  }
1005
1025
  }
1006
1026
  if (cc.hooks.length > 0) {
1007
- const dst = path7.join(root, "hooks");
1008
- fs8.mkdirSync(dst, { recursive: true });
1027
+ const dst = path5.join(root, "hooks");
1028
+ fs6.mkdirSync(dst, { recursive: true });
1009
1029
  const merged = { hooks: {} };
1010
1030
  for (const name of cc.hooks) {
1011
- const src = path7.join(catalog, "hooks", `${name}.json`);
1012
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1013
- const parsed = JSON.parse(fs8.readFileSync(src, "utf-8"));
1031
+ const src = path5.join(catalog, "hooks", `${name}.json`);
1032
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1033
+ const parsed = JSON.parse(fs6.readFileSync(src, "utf-8"));
1014
1034
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1015
1035
  if (!Array.isArray(entries)) continue;
1016
1036
  if (!merged.hooks[event]) merged.hooks[event] = [];
1017
1037
  merged.hooks[event].push(...entries);
1018
1038
  }
1019
1039
  }
1020
- fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1040
+ fs6.writeFileSync(path5.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1021
1041
  `);
1022
1042
  }
1023
1043
  const manifest = {
@@ -1028,17 +1048,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1028
1048
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
1029
1049
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
1030
1050
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
1031
- fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1051
+ fs6.writeFileSync(path5.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1032
1052
  `);
1033
1053
  ctx.data.syntheticPluginPath = root;
1034
1054
  };
1035
1055
  function copyDir(src, dst) {
1036
- fs8.mkdirSync(dst, { recursive: true });
1037
- for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
1038
- const s = path7.join(src, ent.name);
1039
- const d = path7.join(dst, ent.name);
1056
+ fs6.mkdirSync(dst, { recursive: true });
1057
+ for (const ent of fs6.readdirSync(src, { withFileTypes: true })) {
1058
+ const s = path5.join(src, ent.name);
1059
+ const d = path5.join(dst, ent.name);
1040
1060
  if (ent.isDirectory()) copyDir(s, d);
1041
- else if (ent.isFile()) fs8.copyFileSync(s, d);
1061
+ else if (ent.isFile()) fs6.copyFileSync(s, d);
1042
1062
  }
1043
1063
  }
1044
1064
 
@@ -1104,18 +1124,18 @@ function formatMissesForFeedback(misses) {
1104
1124
  }
1105
1125
 
1106
1126
  // src/prompt.ts
1107
- import * as fs9 from "fs";
1108
- import * as path8 from "path";
1127
+ import * as fs7 from "fs";
1128
+ import * as path6 from "path";
1109
1129
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1110
1130
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1111
1131
  function loadProjectConventions(projectDir) {
1112
1132
  const out = [];
1113
1133
  for (const rel of CONVENTION_FILES) {
1114
- const abs = path8.join(projectDir, rel);
1115
- if (!fs9.existsSync(abs)) continue;
1134
+ const abs = path6.join(projectDir, rel);
1135
+ if (!fs7.existsSync(abs)) continue;
1116
1136
  let content;
1117
1137
  try {
1118
- content = fs9.readFileSync(abs, "utf-8");
1138
+ content = fs7.readFileSync(abs, "utf-8");
1119
1139
  } catch {
1120
1140
  continue;
1121
1141
  }
@@ -1236,8 +1256,8 @@ import { execFileSync as execFileSync4 } from "child_process";
1236
1256
 
1237
1257
  // src/commit.ts
1238
1258
  import { execFileSync as execFileSync3 } from "child_process";
1239
- import * as fs10 from "fs";
1240
- import * as path9 from "path";
1259
+ import * as fs8 from "fs";
1260
+ import * as path7 from "path";
1241
1261
  var FORBIDDEN_PATH_PREFIXES = [
1242
1262
  ".kody/",
1243
1263
  ".kody-engine/",
@@ -1292,18 +1312,18 @@ function tryGit(args, cwd) {
1292
1312
  }
1293
1313
  function abortUnfinishedGitOps(cwd) {
1294
1314
  const aborted = [];
1295
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
1296
- if (!fs10.existsSync(gitDir)) return aborted;
1297
- if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
1315
+ const gitDir = path7.join(cwd ?? process.cwd(), ".git");
1316
+ if (!fs8.existsSync(gitDir)) return aborted;
1317
+ if (fs8.existsSync(path7.join(gitDir, "MERGE_HEAD"))) {
1298
1318
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1299
1319
  }
1300
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
1320
+ if (fs8.existsSync(path7.join(gitDir, "CHERRY_PICK_HEAD"))) {
1301
1321
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1302
1322
  }
1303
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
1323
+ if (fs8.existsSync(path7.join(gitDir, "REVERT_HEAD"))) {
1304
1324
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1305
1325
  }
1306
- if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
1326
+ if (fs8.existsSync(path7.join(gitDir, "rebase-merge")) || fs8.existsSync(path7.join(gitDir, "rebase-apply"))) {
1307
1327
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1308
1328
  }
1309
1329
  try {
@@ -1345,7 +1365,7 @@ function normalizeCommitMessage(raw) {
1345
1365
  function commitAndPush(branch, agentMessage, cwd) {
1346
1366
  const allChanged = listChangedFiles(cwd);
1347
1367
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1348
- const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1368
+ const mergeHeadExists = fs8.existsSync(path7.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1349
1369
  if (allowedFiles.length === 0 && !mergeHeadExists) {
1350
1370
  return { committed: false, pushed: false, sha: "", message: "" };
1351
1371
  }
@@ -1440,20 +1460,20 @@ function defaultCommitMessage(mode, data) {
1440
1460
  }
1441
1461
 
1442
1462
  // src/scripts/composePrompt.ts
1443
- import * as fs11 from "fs";
1444
- import * as path10 from "path";
1463
+ import * as fs9 from "fs";
1464
+ import * as path8 from "path";
1445
1465
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
1446
1466
  var composePrompt = async (ctx, profile) => {
1447
1467
  const explicit = ctx.data.promptTemplate;
1448
1468
  const mode = ctx.args.mode;
1449
1469
  const candidates = [
1450
- explicit ? path10.join(profile.dir, explicit) : null,
1451
- mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
1452
- path10.join(profile.dir, "prompt.md")
1470
+ explicit ? path8.join(profile.dir, explicit) : null,
1471
+ mode ? path8.join(profile.dir, "prompts", `${mode}.md`) : null,
1472
+ path8.join(profile.dir, "prompt.md")
1453
1473
  ].filter(Boolean);
1454
1474
  let templatePath = "";
1455
1475
  for (const c of candidates) {
1456
- if (fs11.existsSync(c)) {
1476
+ if (fs9.existsSync(c)) {
1457
1477
  templatePath = c;
1458
1478
  break;
1459
1479
  }
@@ -1461,7 +1481,7 @@ var composePrompt = async (ctx, profile) => {
1461
1481
  if (!templatePath) {
1462
1482
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
1463
1483
  }
1464
- const template = fs11.readFileSync(templatePath, "utf-8");
1484
+ const template = fs9.readFileSync(templatePath, "utf-8");
1465
1485
  const tokens = {
1466
1486
  ...stringifyAll(ctx.args, "args."),
1467
1487
  ...stringifyAll(ctx.data, ""),
@@ -1936,7 +1956,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
1936
1956
 
1937
1957
  // src/gha.ts
1938
1958
  import { execFileSync as execFileSync7 } from "child_process";
1939
- import * as fs12 from "fs";
1959
+ import * as fs10 from "fs";
1940
1960
  function getRunUrl() {
1941
1961
  const server = process.env.GITHUB_SERVER_URL;
1942
1962
  const repo = process.env.GITHUB_REPOSITORY;
@@ -1947,10 +1967,10 @@ function getRunUrl() {
1947
1967
  function reactToTriggerComment(cwd) {
1948
1968
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
1949
1969
  const eventPath = process.env.GITHUB_EVENT_PATH;
1950
- if (!eventPath || !fs12.existsSync(eventPath)) return;
1970
+ if (!eventPath || !fs10.existsSync(eventPath)) return;
1951
1971
  let event = null;
1952
1972
  try {
1953
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
1973
+ event = JSON.parse(fs10.readFileSync(eventPath, "utf-8"));
1954
1974
  } catch {
1955
1975
  return;
1956
1976
  }
@@ -2176,35 +2196,35 @@ function tryPostPr2(prNumber, body, cwd) {
2176
2196
 
2177
2197
  // src/scripts/initFlow.ts
2178
2198
  import { execFileSync as execFileSync9 } from "child_process";
2179
- import * as fs14 from "fs";
2180
- import * as path12 from "path";
2199
+ import * as fs12 from "fs";
2200
+ import * as path10 from "path";
2181
2201
 
2182
2202
  // src/registry.ts
2183
- import * as fs13 from "fs";
2184
- import * as path11 from "path";
2203
+ import * as fs11 from "fs";
2204
+ import * as path9 from "path";
2185
2205
  function getExecutablesRoot() {
2186
- const here = path11.dirname(new URL(import.meta.url).pathname);
2206
+ const here = path9.dirname(new URL(import.meta.url).pathname);
2187
2207
  const candidates = [
2188
- path11.join(here, "executables"),
2208
+ path9.join(here, "executables"),
2189
2209
  // dev: src/
2190
- path11.join(here, "..", "executables"),
2210
+ path9.join(here, "..", "executables"),
2191
2211
  // built: dist/bin → dist/executables
2192
- path11.join(here, "..", "src", "executables")
2212
+ path9.join(here, "..", "src", "executables")
2193
2213
  // fallback
2194
2214
  ];
2195
2215
  for (const c of candidates) {
2196
- if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2216
+ if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
2197
2217
  }
2198
2218
  return candidates[0];
2199
2219
  }
2200
2220
  function listExecutables(root = getExecutablesRoot()) {
2201
- if (!fs13.existsSync(root)) return [];
2202
- const entries = fs13.readdirSync(root, { withFileTypes: true });
2221
+ if (!fs11.existsSync(root)) return [];
2222
+ const entries = fs11.readdirSync(root, { withFileTypes: true });
2203
2223
  const out = [];
2204
2224
  for (const ent of entries) {
2205
2225
  if (!ent.isDirectory()) continue;
2206
- const profilePath = path11.join(root, ent.name, "profile.json");
2207
- if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
2226
+ const profilePath = path9.join(root, ent.name, "profile.json");
2227
+ if (fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile()) {
2208
2228
  out.push({ name: ent.name, profilePath });
2209
2229
  }
2210
2230
  }
@@ -2212,8 +2232,8 @@ function listExecutables(root = getExecutablesRoot()) {
2212
2232
  }
2213
2233
  function hasExecutable(name, root = getExecutablesRoot()) {
2214
2234
  if (!isSafeName(name)) return false;
2215
- const profilePath = path11.join(root, name, "profile.json");
2216
- return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
2235
+ const profilePath = path9.join(root, name, "profile.json");
2236
+ return fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile();
2217
2237
  }
2218
2238
  function isSafeName(name) {
2219
2239
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -2242,9 +2262,9 @@ function parseGenericFlags(argv) {
2242
2262
 
2243
2263
  // src/scripts/initFlow.ts
2244
2264
  function detectPackageManager(cwd) {
2245
- if (fs14.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2246
- if (fs14.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
2247
- if (fs14.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
2265
+ if (fs12.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2266
+ if (fs12.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
2267
+ if (fs12.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
2248
2268
  return "npm";
2249
2269
  }
2250
2270
  function qualityCommandsFor(pm) {
@@ -2365,22 +2385,22 @@ function performInit(cwd, force) {
2365
2385
  const pm = detectPackageManager(cwd);
2366
2386
  const ownerRepo = detectOwnerRepo(cwd);
2367
2387
  const defaultBranch = defaultBranchFromGit(cwd);
2368
- const configPath = path12.join(cwd, "kody.config.json");
2369
- if (fs14.existsSync(configPath) && !force) {
2388
+ const configPath = path10.join(cwd, "kody.config.json");
2389
+ if (fs12.existsSync(configPath) && !force) {
2370
2390
  skipped.push("kody.config.json");
2371
2391
  } else {
2372
2392
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
2373
- fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2393
+ fs12.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2374
2394
  `);
2375
2395
  wrote.push("kody.config.json");
2376
2396
  }
2377
- const workflowDir = path12.join(cwd, ".github", "workflows");
2378
- const workflowPath = path12.join(workflowDir, "kody2.yml");
2379
- if (fs14.existsSync(workflowPath) && !force) {
2397
+ const workflowDir = path10.join(cwd, ".github", "workflows");
2398
+ const workflowPath = path10.join(workflowDir, "kody2.yml");
2399
+ if (fs12.existsSync(workflowPath) && !force) {
2380
2400
  skipped.push(".github/workflows/kody2.yml");
2381
2401
  } else {
2382
- fs14.mkdirSync(workflowDir, { recursive: true });
2383
- fs14.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2402
+ fs12.mkdirSync(workflowDir, { recursive: true });
2403
+ fs12.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2384
2404
  wrote.push(".github/workflows/kody2.yml");
2385
2405
  }
2386
2406
  for (const exe of listExecutables()) {
@@ -2391,12 +2411,12 @@ function performInit(cwd, force) {
2391
2411
  continue;
2392
2412
  }
2393
2413
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
2394
- const target = path12.join(workflowDir, `kody2-${exe.name}.yml`);
2395
- if (fs14.existsSync(target) && !force) {
2414
+ const target = path10.join(workflowDir, `kody2-${exe.name}.yml`);
2415
+ if (fs12.existsSync(target) && !force) {
2396
2416
  skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
2397
2417
  continue;
2398
2418
  }
2399
- fs14.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2419
+ fs12.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2400
2420
  wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
2401
2421
  }
2402
2422
  return { wrote, skipped };
@@ -2903,8 +2923,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
2903
2923
 
2904
2924
  // src/scripts/releaseFlow.ts
2905
2925
  import { execFileSync as execFileSync11, spawnSync } from "child_process";
2906
- import * as fs15 from "fs";
2907
- import * as path13 from "path";
2926
+ import * as fs13 from "fs";
2927
+ import * as path11 from "path";
2908
2928
  function bumpVersion(current, bump) {
2909
2929
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2910
2930
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -2920,12 +2940,12 @@ function bumpVersion(current, bump) {
2920
2940
  return `${major}.${minor}.${patch}`;
2921
2941
  }
2922
2942
  function updateVersionInFile(file, newVersion, cwd) {
2923
- const abs = path13.join(cwd, file);
2924
- if (!fs15.existsSync(abs)) return false;
2925
- const content = fs15.readFileSync(abs, "utf-8");
2943
+ const abs = path11.join(cwd, file);
2944
+ if (!fs13.existsSync(abs)) return false;
2945
+ const content = fs13.readFileSync(abs, "utf-8");
2926
2946
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2927
2947
  if (updated === content) return false;
2928
- fs15.writeFileSync(abs, updated);
2948
+ fs13.writeFileSync(abs, updated);
2929
2949
  return true;
2930
2950
  }
2931
2951
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2973,19 +2993,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2973
2993
  return parts.join("\n");
2974
2994
  }
2975
2995
  function prependChangelog(cwd, entry) {
2976
- const p = path13.join(cwd, "CHANGELOG.md");
2996
+ const p = path11.join(cwd, "CHANGELOG.md");
2977
2997
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2978
- if (fs15.existsSync(p)) {
2979
- const prior = fs15.readFileSync(p, "utf-8");
2998
+ if (fs13.existsSync(p)) {
2999
+ const prior = fs13.readFileSync(p, "utf-8");
2980
3000
  if (/^#\s*Changelog\b/m.test(prior)) {
2981
3001
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2982
- fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
3002
+ fs13.writeFileSync(p, `${prior.slice(0, idx + 1)}
2983
3003
  ${entry}${prior.slice(idx + 1)}`);
2984
3004
  } else {
2985
- fs15.writeFileSync(p, `${header}${entry}${prior}`);
3005
+ fs13.writeFileSync(p, `${header}${entry}${prior}`);
2986
3006
  }
2987
3007
  } else {
2988
- fs15.writeFileSync(p, `${header}${entry}`);
3008
+ fs13.writeFileSync(p, `${header}${entry}`);
2989
3009
  }
2990
3010
  }
2991
3011
  function git3(args, cwd, timeout = 6e4) {
@@ -3036,13 +3056,13 @@ var releaseFlow = async (ctx) => {
3036
3056
  };
3037
3057
  async function runPrepare(args) {
3038
3058
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
3039
- const pkgPath = path13.join(cwd, "package.json");
3040
- if (!fs15.existsSync(pkgPath)) {
3059
+ const pkgPath = path11.join(cwd, "package.json");
3060
+ if (!fs13.existsSync(pkgPath)) {
3041
3061
  ctx.output.exitCode = 99;
3042
3062
  ctx.output.reason = "release prepare: package.json not found";
3043
3063
  return;
3044
3064
  }
3045
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3065
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3046
3066
  if (typeof pkg.version !== "string") {
3047
3067
  ctx.output.exitCode = 99;
3048
3068
  ctx.output.reason = "release prepare: package.json has no version";
@@ -3113,8 +3133,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
3113
3133
  }
3114
3134
  async function runFinalize(args) {
3115
3135
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
3116
- const pkgPath = path13.join(cwd, "package.json");
3117
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3136
+ const pkgPath = path11.join(cwd, "package.json");
3137
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3118
3138
  if (typeof pkg.version !== "string") {
3119
3139
  ctx.output.exitCode = 99;
3120
3140
  ctx.output.reason = "release finalize: package.json has no version";
@@ -3792,7 +3812,7 @@ var watchStalePrsFlow = async (ctx) => {
3792
3812
  };
3793
3813
 
3794
3814
  // src/scripts/writeRunSummary.ts
3795
- import * as fs16 from "fs";
3815
+ import * as fs14 from "fs";
3796
3816
  var writeRunSummary = async (ctx, profile) => {
3797
3817
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
3798
3818
  if (!summaryPath) return;
@@ -3814,7 +3834,7 @@ var writeRunSummary = async (ctx, profile) => {
3814
3834
  if (reason) lines.push(`- **Reason:** ${reason}`);
3815
3835
  lines.push("");
3816
3836
  try {
3817
- fs16.appendFileSync(summaryPath, `${lines.join("\n")}
3837
+ fs14.appendFileSync(summaryPath, `${lines.join("\n")}
3818
3838
  `);
3819
3839
  } catch {
3820
3840
  }
@@ -3962,9 +3982,9 @@ async function runExecutable(profileName, input) {
3962
3982
  data: {},
3963
3983
  output: { exitCode: 0 }
3964
3984
  };
3965
- const ndjsonDir = path14.join(input.cwd, ".kody2");
3985
+ const ndjsonDir = path12.join(input.cwd, ".kody2");
3966
3986
  const invokeAgent = async (prompt) => {
3967
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path14.isAbsolute(p) ? p : path14.resolve(profile.dir, p)).filter((p) => p.length > 0);
3987
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path12.isAbsolute(p) ? p : path12.resolve(profile.dir, p)).filter((p) => p.length > 0);
3968
3988
  const syntheticPath = ctx.data.syntheticPluginPath;
3969
3989
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
3970
3990
  return runAgent({
@@ -3980,6 +4000,7 @@ async function runExecutable(profileName, input) {
3980
4000
  mcpServers: profile.claudeCode.mcpServers,
3981
4001
  pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
3982
4002
  maxTurns: profile.claudeCode.maxTurns,
4003
+ maxThinkingTokens: profile.claudeCode.maxThinkingTokens,
3983
4004
  systemPromptAppend: profile.claudeCode.systemPromptAppend,
3984
4005
  settingSources: profile.claudeCode.settingSources
3985
4006
  });
@@ -4030,17 +4051,17 @@ async function runExecutable(profileName, input) {
4030
4051
  }
4031
4052
  }
4032
4053
  function resolveProfilePath(profileName) {
4033
- const here = path14.dirname(new URL(import.meta.url).pathname);
4054
+ const here = path12.dirname(new URL(import.meta.url).pathname);
4034
4055
  const candidates = [
4035
- path14.join(here, "executables", profileName, "profile.json"),
4056
+ path12.join(here, "executables", profileName, "profile.json"),
4036
4057
  // same-dir sibling (dev)
4037
- path14.join(here, "..", "executables", profileName, "profile.json"),
4058
+ path12.join(here, "..", "executables", profileName, "profile.json"),
4038
4059
  // up one (prod: dist/bin → dist/executables)
4039
- path14.join(here, "..", "src", "executables", profileName, "profile.json")
4060
+ path12.join(here, "..", "src", "executables", profileName, "profile.json")
4040
4061
  // fallback
4041
4062
  ];
4042
4063
  for (const c of candidates) {
4043
- if (fs17.existsSync(c)) return c;
4064
+ if (fs15.existsSync(c)) return c;
4044
4065
  }
4045
4066
  return candidates[0];
4046
4067
  }
@@ -4216,9 +4237,9 @@ function resolveAuthToken(env = process.env) {
4216
4237
  return token;
4217
4238
  }
4218
4239
  function detectPackageManager2(cwd) {
4219
- if (fs18.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4220
- if (fs18.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
4221
- if (fs18.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
4240
+ if (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4241
+ if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
4242
+ if (fs16.existsSync(path13.join(cwd, "bun.lockb"))) return "bun";
4222
4243
  return "npm";
4223
4244
  }
4224
4245
  function shellOut(cmd, args, cwd, stream = true) {
@@ -4298,11 +4319,11 @@ function configureGitIdentity(cwd) {
4298
4319
  }
4299
4320
  function postFailureTail(issueNumber, cwd, reason) {
4300
4321
  if (!issueNumber) return;
4301
- const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
4322
+ const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
4302
4323
  let tail = "";
4303
4324
  try {
4304
- if (fs18.existsSync(logPath)) {
4305
- const content = fs18.readFileSync(logPath, "utf-8");
4325
+ if (fs16.existsSync(logPath)) {
4326
+ const content = fs16.readFileSync(logPath, "utf-8");
4306
4327
  tail = content.slice(-3e3);
4307
4328
  }
4308
4329
  } catch {
@@ -4327,7 +4348,7 @@ async function runCi(argv) {
4327
4348
  return 0;
4328
4349
  }
4329
4350
  const args = parseCiArgs(argv);
4330
- const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
4351
+ const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
4331
4352
  let earlyConfig;
4332
4353
  try {
4333
4354
  earlyConfig = loadConfig(cwd);
@@ -4420,23 +4441,23 @@ var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
4420
4441
  var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
4421
4442
 
4422
4443
  Usage:
4423
- kody2 chat [--session <id>] [--message <text>] [--model <provider/model>]
4444
+ kody2 chat [--session <id>] [--model <provider/model>]
4424
4445
  [--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
4425
4446
 
4426
- All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
4427
- CLI flags take precedence over env. SESSION_ID is required.
4447
+ All inputs may also come from env: SESSION_ID, MODEL, DASHBOARD_URL.
4448
+ CLI flags take precedence over env. SESSION_ID and DASHBOARD_URL are required
4449
+ (the runner long-polls the dashboard for user turns and pushes events back).
4428
4450
 
4429
4451
  Exit codes:
4430
- 0 reply emitted successfully
4431
- 64 bad inputs (missing session, empty history)
4432
- 99 runtime failure (agent crash, LiteLLM failure)
4452
+ 0 session exited cleanly (idle or hard timeout)
4453
+ 64 bad inputs
4454
+ 99 runtime failure (agent crash, pull failure, LiteLLM failure)
4433
4455
  `;
4434
4456
  function parseChatArgs(argv, env = process.env) {
4435
4457
  const result = { errors: [] };
4436
4458
  for (let i = 0; i < argv.length; i++) {
4437
4459
  const arg = argv[i];
4438
4460
  if (arg === "--session") result.sessionId = argv[++i];
4439
- else if (arg === "--message") result.initMessage = argv[++i];
4440
4461
  else if (arg === "--model") result.model = argv[++i];
4441
4462
  else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
4442
4463
  else if (arg === "--cwd") result.cwd = argv[++i];
@@ -4447,34 +4468,18 @@ function parseChatArgs(argv, env = process.env) {
4447
4468
  else if (arg) result.errors.push(`unexpected positional: ${arg}`);
4448
4469
  }
4449
4470
  result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
4450
- result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
4451
4471
  result.model = result.model ?? env.MODEL ?? void 0;
4452
4472
  result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
4453
- for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
4473
+ for (const key of ["sessionId", "model", "dashboardUrl"]) {
4454
4474
  const v = result[key];
4455
4475
  if (typeof v === "string" && v.trim() === "") result[key] = void 0;
4456
4476
  }
4457
- if (!result.sessionId && !result.errors.includes("__HELP__")) {
4458
- result.errors.push("--session <id> (or SESSION_ID env) is required");
4477
+ if (!result.errors.includes("__HELP__")) {
4478
+ if (!result.sessionId) result.errors.push("--session <id> (or SESSION_ID env) is required");
4479
+ if (!result.dashboardUrl) result.errors.push("--dashboard-url <url> (or DASHBOARD_URL env) is required");
4459
4480
  }
4460
4481
  return result;
4461
4482
  }
4462
- function commitChatFiles(cwd, sessionId, verbose) {
4463
- const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
4464
- const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
4465
- const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
4466
- if (paths.length === 0) return;
4467
- const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
4468
- try {
4469
- execFileSync16("git", ["add", ...paths], opts);
4470
- execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
4471
- execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
4472
- } catch (err) {
4473
- const msg = err instanceof Error ? err.message : String(err);
4474
- process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
4475
- `);
4476
- }
4477
- }
4478
4483
  function tryLoadConfig(cwd) {
4479
4484
  try {
4480
4485
  return loadConfig(cwd);
@@ -4482,11 +4487,6 @@ function tryLoadConfig(cwd) {
4482
4487
  return null;
4483
4488
  }
4484
4489
  }
4485
- function buildSink(cwd, sessionId, dashboardUrl) {
4486
- const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
4487
- if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
4488
- return new TeeSink(sinks);
4489
- }
4490
4490
  async function runChat(argv) {
4491
4491
  if (argv.includes("--help") || argv.includes("-h")) {
4492
4492
  process.stdout.write(CHAT_HELP);
@@ -4500,8 +4500,9 @@ async function runChat(argv) {
4500
4500
  ${CHAT_HELP}`);
4501
4501
  return 64;
4502
4502
  }
4503
- const cwd = args.cwd ? path16.resolve(args.cwd) : process.cwd();
4503
+ const cwd = args.cwd ? path14.resolve(args.cwd) : process.cwd();
4504
4504
  const sessionId = args.sessionId;
4505
+ const dashboardUrl = args.dashboardUrl;
4505
4506
  const unpackedSecrets = unpackAllSecrets();
4506
4507
  if (unpackedSecrets > 0) {
4507
4508
  process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
@@ -4527,13 +4528,20 @@ ${CHAT_HELP}`);
4527
4528
  return 99;
4528
4529
  }
4529
4530
  }
4531
+ let sink;
4532
+ try {
4533
+ sink = new HttpSink(dashboardUrl, sessionId);
4534
+ } catch (err) {
4535
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4536
+ `);
4537
+ return 64;
4538
+ }
4530
4539
  let litellm = null;
4531
4540
  try {
4532
4541
  litellm = await startLitellmIfNeeded(model, cwd);
4533
4542
  } catch (err) {
4534
4543
  const msg = err instanceof Error ? err.message : String(err);
4535
- const sink2 = buildSink(cwd, sessionId, args.dashboardUrl);
4536
- await sink2.emit({
4544
+ await sink.emit({
4537
4545
  event: "chat.error",
4538
4546
  payload: { sessionId, error: `litellm startup failed: ${msg}` },
4539
4547
  runId: makeRunId(sessionId, "error"),
@@ -4541,21 +4549,33 @@ ${CHAT_HELP}`);
4541
4549
  });
4542
4550
  return 99;
4543
4551
  }
4544
- const sessionFile = sessionFilePath(cwd, sessionId);
4545
- if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
4546
- const sink = buildSink(cwd, sessionId, args.dashboardUrl);
4552
+ let pull;
4547
4553
  try {
4548
- const result = await runChatTurn({
4554
+ pull = createPullClient({ baseUrl: dashboardUrl, sessionId });
4555
+ } catch (err) {
4556
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4557
+ `);
4558
+ try {
4559
+ litellm?.kill();
4560
+ } catch {
4561
+ }
4562
+ return 64;
4563
+ }
4564
+ process.stdout.write(`\u2192 kody2 chat: session ${sessionId}, model ${model.provider}/${model.model}
4565
+ `);
4566
+ try {
4567
+ const result = await runChatSession({
4549
4568
  sessionId,
4550
- sessionFile,
4551
4569
  cwd,
4552
4570
  model,
4553
4571
  litellmUrl: litellm?.url ?? null,
4554
4572
  sink,
4573
+ pull,
4555
4574
  verbose: args.verbose,
4556
4575
  quiet: args.quiet
4557
4576
  });
4558
- commitChatFiles(cwd, sessionId, args.verbose ?? false);
4577
+ process.stdout.write(`\u2192 kody2 chat: exited (${result.reason ?? "ok"}) after ${result.turnsProcessed} turn(s)
4578
+ `);
4559
4579
  return result.exitCode;
4560
4580
  } finally {
4561
4581
  try {
@@ -87,6 +87,8 @@ export interface ClaudeCodeSpec {
87
87
  permissionMode: "default" | "acceptEdits" | "plan" | "bypassPermissions"
88
88
  /** null = unbounded. */
89
89
  maxTurns: number | null
90
+ /** Extended-thinking token budget. null = SDK default. */
91
+ maxThinkingTokens: number | null
90
92
  /** Text appended on top of Claude Code's baseline system prompt. */
91
93
  systemPromptAppend: string | null
92
94
  /** SDK built-in tools this executable is allowed to use (capability pack). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,18 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
+ "scripts": {
16
+ "kody2": "tsx bin/kody2.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
+ },
15
27
  "dependencies": {
16
28
  "@anthropic-ai/claude-agent-sdk": "0.2.92"
17
29
  },
@@ -31,16 +43,5 @@
31
43
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
32
44
  },
33
45
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
34
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
35
- "scripts": {
36
- "kody2": "tsx bin/kody2.ts",
37
- "build": "tsup && node scripts/copy-assets.cjs",
38
- "test": "vitest run tests/unit tests/int --no-coverage",
39
- "test:e2e": "vitest run tests/e2e --no-coverage",
40
- "test:all": "vitest run tests --no-coverage",
41
- "typecheck": "tsc --noEmit",
42
- "lint": "biome check",
43
- "lint:fix": "biome check --write",
44
- "format": "biome format --write"
45
- }
46
- }
46
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
47
+ }