@kody-ade/kody-engine 0.2.34 → 0.2.36

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.34",
6
+ version: "0.2.36",
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,115 +50,81 @@ var package_default = {
50
50
  };
51
51
 
52
52
  // src/chat-cli.ts
53
- import * as path14 from "path";
53
+ import { execFileSync as execFileSync16 } from "child_process";
54
+ import * as fs19 from "fs";
55
+ import * as path16 from "path";
54
56
 
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
- };
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`);
91
62
  }
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 };
63
+ var FileSink = class {
64
+ constructor(file) {
65
+ this.file = file;
100
66
  }
101
- }
102
-
103
- // src/chat/events.ts
67
+ file;
68
+ async emit(event) {
69
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
70
+ fs.appendFileSync(this.file, `${JSON.stringify(event)}
71
+ `);
72
+ }
73
+ };
104
74
  var HttpSink = class {
105
- constructor(baseUrl, sessionId, token, fetchFn = fetch, logger = {
75
+ constructor(baseUrl, sessionId, logger = {
106
76
  warn: (m) => process.stderr.write(`[kody2:chat] ${m}
107
77
  `)
108
78
  }) {
79
+ this.baseUrl = baseUrl;
109
80
  this.sessionId = sessionId;
110
- this.fetchFn = fetchFn;
111
81
  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;
119
82
  }
83
+ baseUrl;
120
84
  sessionId;
121
- fetchFn;
122
85
  logger;
123
- origin;
124
- token;
125
86
  async emit(event) {
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);
87
+ const url = withSessionParam(this.baseUrl, this.sessionId);
130
88
  try {
131
- const res = await this.fetchFn(url.toString(), {
89
+ const res = await fetch(url, {
132
90
  method: "POST",
133
- headers: {
134
- "content-type": "application/json",
135
- authorization: `Bearer ${this.token}`
136
- },
91
+ headers: { "content-type": "application/json" },
137
92
  body: JSON.stringify(event),
138
93
  signal: AbortSignal.timeout(5e3)
139
94
  });
140
95
  if (!res.ok) {
141
- this.logger.warn(`HttpSink POST ${url.pathname} \u2192 ${res.status}`);
96
+ this.logger.warn(`HttpSink POST ${url} \u2192 ${res.status}`);
142
97
  }
143
98
  } catch (err) {
144
- this.logger.warn(
145
- `HttpSink POST ${url.pathname} failed: ${err instanceof Error ? err.message : String(err)}`
146
- );
99
+ this.logger.warn(`HttpSink POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
147
100
  }
148
101
  }
149
102
  };
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
+ }
150
116
  function makeRunId(sessionId, suffix) {
151
117
  return `chat-${sessionId}-${suffix}`;
152
118
  }
153
119
 
154
120
  // src/agent.ts
155
- import * as fs2 from "fs";
156
- import * as path2 from "path";
121
+ import * as fs3 from "fs";
122
+ import * as path3 from "path";
157
123
  import { query } from "@anthropic-ai/claude-agent-sdk";
158
124
 
159
125
  // src/config.ts
160
- import * as fs from "fs";
161
- import * as path from "path";
126
+ import * as fs2 from "fs";
127
+ import * as path2 from "path";
162
128
  var LITELLM_DEFAULT_PORT = 4e3;
163
129
  var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
164
130
  function parseProviderModel(s) {
@@ -176,13 +142,13 @@ function needsLitellmProxy(model) {
176
142
  return model.provider !== "claude" && model.provider !== "anthropic";
177
143
  }
178
144
  function loadConfig(projectDir = process.cwd()) {
179
- const configPath = path.join(projectDir, "kody.config.json");
180
- if (!fs.existsSync(configPath)) {
145
+ const configPath = path2.join(projectDir, "kody.config.json");
146
+ if (!fs2.existsSync(configPath)) {
181
147
  throw new Error(`kody.config.json not found at ${configPath}`);
182
148
  }
183
149
  let raw;
184
150
  try {
185
- raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
151
+ raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
186
152
  } catch (err) {
187
153
  const msg = err instanceof Error ? err.message : String(err);
188
154
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
@@ -359,10 +325,10 @@ function formatBytes(bytes) {
359
325
  // src/agent.ts
360
326
  var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
361
327
  async function runAgent(opts) {
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" });
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" });
366
332
  const env = {
367
333
  ...process.env,
368
334
  SKIP_HOOKS: "1",
@@ -393,6 +359,9 @@ async function runAgent(opts) {
393
359
  if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
394
360
  queryOptions.maxTurns = opts.maxTurns;
395
361
  }
362
+ if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
363
+ queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
364
+ }
396
365
  if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
397
366
  queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: opts.systemPromptAppend };
398
367
  }
@@ -434,6 +403,53 @@ async function runAgent(opts) {
434
403
  return { outcome, finalText, error: errorMessage, ndjsonPath };
435
404
  }
436
405
 
406
+ // src/chat/session.ts
407
+ import * as fs4 from "fs";
408
+ import * as path4 from "path";
409
+ function sessionFilePath(cwd, sessionId) {
410
+ return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
411
+ }
412
+ function readSession(file) {
413
+ if (!fs4.existsSync(file)) return [];
414
+ const raw = fs4.readFileSync(file, "utf-8").trim();
415
+ if (!raw) return [];
416
+ const turns = [];
417
+ for (const line of raw.split("\n")) {
418
+ if (!line.trim()) continue;
419
+ try {
420
+ const parsed = JSON.parse(line);
421
+ if (parsed.role !== "user" && parsed.role !== "assistant") continue;
422
+ if (typeof parsed.content !== "string") continue;
423
+ turns.push(parsed);
424
+ } catch {
425
+ }
426
+ }
427
+ return turns;
428
+ }
429
+ function appendTurn(file, turn) {
430
+ fs4.mkdirSync(path4.dirname(file), { recursive: true });
431
+ const line = JSON.stringify({
432
+ role: turn.role,
433
+ content: turn.content,
434
+ timestamp: turn.timestamp,
435
+ toolCalls: turn.toolCalls ?? []
436
+ });
437
+ fs4.appendFileSync(file, `${line}
438
+ `);
439
+ }
440
+ function seedInitialMessage(file, message) {
441
+ if (!message.trim()) return false;
442
+ const turns = readSession(file);
443
+ const lastUser = [...turns].reverse().find((t) => t.role === "user");
444
+ if (lastUser && lastUser.content === message) return false;
445
+ appendTurn(file, {
446
+ role: "user",
447
+ content: message,
448
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
449
+ });
450
+ return true;
451
+ }
452
+
437
453
  // src/chat/loop.ts
438
454
  var CHAT_SYSTEM_PROMPT = [
439
455
  "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
@@ -442,15 +458,20 @@ var CHAT_SYSTEM_PROMPT = [
442
458
  "read repository code or execute small checks when it helps you answer \u2014 otherwise",
443
459
  "reply directly. Do not invent file paths, commit SHAs, or command output."
444
460
  ].join("\n");
445
- var DEFAULT_IDLE_TIMEOUT_MS = 3 * 60 * 1e3;
446
- var DEFAULT_HARD_TIMEOUT_MS = 5 * 60 * 60 * 1e3;
447
- var DEFAULT_PULL_TIMEOUT_MS = 25e3;
448
- async function runChatSession(opts) {
449
- const now = opts.now ?? (() => Date.now());
450
- const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
451
- const hardTimeoutMs = opts.hardTimeoutMs ?? DEFAULT_HARD_TIMEOUT_MS;
452
- const pullTimeoutMs = opts.pullTimeoutMs ?? DEFAULT_PULL_TIMEOUT_MS;
453
- const systemPrompt = opts.systemPrompt ?? CHAT_SYSTEM_PROMPT;
461
+ async function runChatTurn(opts) {
462
+ const turns = readSession(opts.sessionFile);
463
+ if (turns.length === 0) {
464
+ const error = "session file is empty \u2014 nothing to reply to";
465
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
466
+ return { exitCode: 64, error };
467
+ }
468
+ const lastTurn = turns[turns.length - 1];
469
+ if (lastTurn.role !== "user") {
470
+ const error = "last turn is not a user message \u2014 assistant already replied";
471
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
472
+ return { exitCode: 64, error };
473
+ }
474
+ const prompt = buildPrompt(turns, opts.systemPrompt ?? CHAT_SYSTEM_PROMPT);
454
475
  const invoke = opts.invokeAgent ?? ((p) => runAgent({
455
476
  prompt: p,
456
477
  model: opts.model,
@@ -459,68 +480,34 @@ async function runChatSession(opts) {
459
480
  verbose: opts.verbose,
460
481
  quiet: opts.quiet
461
482
  }));
462
- const history = [];
463
- let since = 0;
464
- let lastActivityAt = now();
465
- const startedAt = now();
466
- let turnsProcessed = 0;
467
- while (true) {
468
- if (now() - startedAt > hardTimeoutMs) {
469
- await emit(opts.sink, "chat.done", opts.sessionId, "done", {
470
- sessionId: opts.sessionId,
471
- reason: "hard-timeout"
472
- });
473
- return { exitCode: 0, turnsProcessed, reason: "hard-timeout" };
474
- }
475
- let response;
476
- try {
477
- response = await opts.pull(since, pullTimeoutMs);
478
- } catch (err) {
479
- const msg = err instanceof Error ? err.message : String(err);
480
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: `pull failed: ${msg}` });
481
- return { exitCode: 99, turnsProcessed, reason: `pull failed: ${msg}` };
482
- }
483
- if (response.turns.length === 0) {
484
- if (now() - lastActivityAt > idleTimeoutMs) {
485
- await emit(opts.sink, "chat.done", opts.sessionId, "done", {
486
- sessionId: opts.sessionId,
487
- reason: "idle-timeout"
488
- });
489
- return { exitCode: 0, turnsProcessed, reason: "idle-timeout" };
490
- }
491
- continue;
492
- }
493
- const newUserTurns = response.turns.filter((t) => t.role === "user");
494
- for (const t of newUserTurns) history.push(t);
495
- since = response.nextSince;
496
- if (newUserTurns.length === 0) continue;
497
- lastActivityAt = now();
498
- const prompt = buildPrompt(history, systemPrompt);
499
- let result;
500
- try {
501
- result = await invoke(prompt);
502
- } catch (err) {
503
- const msg = err instanceof Error ? err.message : String(err);
504
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: msg });
505
- return { exitCode: 99, turnsProcessed, reason: msg };
506
- }
507
- if (result.outcome !== "completed") {
508
- const error = result.error ?? "agent did not complete";
509
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
510
- return { exitCode: 99, turnsProcessed, reason: error };
511
- }
512
- const reply = result.finalText.trim();
513
- const replyTimestamp = (/* @__PURE__ */ new Date()).toISOString();
514
- history.push({ role: "assistant", content: reply, timestamp: replyTimestamp });
515
- turnsProcessed++;
516
- lastActivityAt = now();
517
- await emit(opts.sink, "chat.message", opts.sessionId, `message-${turnsProcessed}`, {
518
- sessionId: opts.sessionId,
519
- role: "assistant",
520
- content: reply,
521
- timestamp: replyTimestamp
522
- });
483
+ let result;
484
+ try {
485
+ result = await invoke(prompt);
486
+ } catch (err) {
487
+ const error = err instanceof Error ? err.message : String(err);
488
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
489
+ return { exitCode: 99, error };
490
+ }
491
+ if (result.outcome !== "completed") {
492
+ const error = result.error ?? "agent did not complete";
493
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
494
+ return { exitCode: 99, error };
523
495
  }
496
+ const reply = result.finalText.trim();
497
+ const now = (/* @__PURE__ */ new Date()).toISOString();
498
+ appendTurn(opts.sessionFile, {
499
+ role: "assistant",
500
+ content: reply,
501
+ timestamp: now
502
+ });
503
+ await emit(opts.sink, "chat.message", opts.sessionId, "message", {
504
+ sessionId: opts.sessionId,
505
+ role: "assistant",
506
+ content: reply,
507
+ timestamp: now
508
+ });
509
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
510
+ return { exitCode: 0, reply };
524
511
  }
525
512
  function buildPrompt(turns, systemPrompt) {
526
513
  const header = `System: ${systemPrompt}`;
@@ -542,11 +529,11 @@ async function emit(sink, type, sessionId, suffix, payload) {
542
529
 
543
530
  // src/kody2-cli.ts
544
531
  import { execFileSync as execFileSync15 } from "child_process";
545
- import * as fs16 from "fs";
546
- import * as path13 from "path";
532
+ import * as fs18 from "fs";
533
+ import * as path15 from "path";
547
534
 
548
535
  // src/dispatch.ts
549
- import * as fs3 from "fs";
536
+ import * as fs5 from "fs";
550
537
  function autoDispatch(opts) {
551
538
  const explicit = opts?.explicit;
552
539
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
@@ -558,10 +545,10 @@ function autoDispatch(opts) {
558
545
  }
559
546
  const eventName = process.env.GITHUB_EVENT_NAME;
560
547
  const eventPath = process.env.GITHUB_EVENT_PATH;
561
- if (!eventName || !eventPath || !fs3.existsSync(eventPath)) return null;
548
+ if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
562
549
  let event = {};
563
550
  try {
564
- event = JSON.parse(fs3.readFileSync(eventPath, "utf-8"));
551
+ event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
565
552
  } catch {
566
553
  return null;
567
554
  }
@@ -630,14 +617,14 @@ function extractFeedback(afterTag) {
630
617
  }
631
618
 
632
619
  // src/executor.ts
633
- import * as fs15 from "fs";
634
- import * as path12 from "path";
620
+ import * as fs17 from "fs";
621
+ import * as path14 from "path";
635
622
 
636
623
  // src/litellm.ts
637
624
  import { execFileSync, spawn } from "child_process";
638
- import * as fs4 from "fs";
625
+ import * as fs6 from "fs";
639
626
  import * as os from "os";
640
- import * as path3 from "path";
627
+ import * as path5 from "path";
641
628
  async function checkLitellmHealth(url) {
642
629
  try {
643
630
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -677,20 +664,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
677
664
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
678
665
  }
679
666
  }
680
- const configPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
681
- fs4.writeFileSync(configPath, generateLitellmConfigYaml(model));
667
+ const configPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
668
+ fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
682
669
  const portMatch = url.match(/:(\d+)/);
683
670
  const port = portMatch ? portMatch[1] : "4000";
684
671
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
685
672
  const dotenvVars = readDotenvApiKeys(projectDir);
686
- const logPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
687
- const outFd = fs4.openSync(logPath, "w");
673
+ const logPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
674
+ const outFd = fs6.openSync(logPath, "w");
688
675
  const child = spawn(cmd, args, {
689
676
  stdio: ["ignore", outFd, outFd],
690
677
  detached: true,
691
678
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
692
679
  });
693
- fs4.closeSync(outFd);
680
+ fs6.closeSync(outFd);
694
681
  for (let i = 0; i < 30; i++) {
695
682
  await new Promise((r) => setTimeout(r, 2e3));
696
683
  if (await checkLitellmHealth(url)) {
@@ -707,7 +694,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
707
694
  }
708
695
  let logTail = "";
709
696
  try {
710
- logTail = fs4.readFileSync(logPath, "utf-8").slice(-2e3);
697
+ logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
711
698
  } catch {
712
699
  }
713
700
  try {
@@ -718,10 +705,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
718
705
  ${logTail}`);
719
706
  }
720
707
  function readDotenvApiKeys(projectDir) {
721
- const dotenvPath = path3.join(projectDir, ".env");
722
- if (!fs4.existsSync(dotenvPath)) return {};
708
+ const dotenvPath = path5.join(projectDir, ".env");
709
+ if (!fs6.existsSync(dotenvPath)) return {};
723
710
  const result = {};
724
- for (const rawLine of fs4.readFileSync(dotenvPath, "utf-8").split("\n")) {
711
+ for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
725
712
  const line = rawLine.trim();
726
713
  if (!line || line.startsWith("#")) continue;
727
714
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -744,8 +731,8 @@ function stripBlockingEnv(env) {
744
731
  }
745
732
 
746
733
  // src/profile.ts
747
- import * as fs5 from "fs";
748
- import * as path4 from "path";
734
+ import * as fs7 from "fs";
735
+ import * as path6 from "path";
749
736
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
750
737
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
751
738
  var ProfileError = class extends Error {
@@ -758,12 +745,12 @@ var ProfileError = class extends Error {
758
745
  profilePath;
759
746
  };
760
747
  function loadProfile(profilePath) {
761
- if (!fs5.existsSync(profilePath)) {
748
+ if (!fs7.existsSync(profilePath)) {
762
749
  throw new ProfileError(profilePath, "file not found");
763
750
  }
764
751
  let raw;
765
752
  try {
766
- raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
753
+ raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
767
754
  } catch (err) {
768
755
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
769
756
  }
@@ -787,7 +774,7 @@ function loadProfile(profilePath) {
787
774
  outputContract: r.outputContract,
788
775
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
789
776
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
790
- dir: path4.dirname(profilePath)
777
+ dir: path6.dirname(profilePath)
791
778
  };
792
779
  return profile;
793
780
  }
@@ -853,6 +840,7 @@ function parseClaudeCode(p, raw) {
853
840
  model: typeof r.model === "string" ? r.model : "inherit",
854
841
  permissionMode,
855
842
  maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
843
+ maxThinkingTokens: typeof r.maxThinkingTokens === "number" ? r.maxThinkingTokens : null,
856
844
  systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
857
845
  tools,
858
846
  hooks: Array.isArray(r.hooks) ? r.hooks : [],
@@ -966,21 +954,21 @@ function parseScriptList(p, key, raw) {
966
954
  }
967
955
 
968
956
  // src/scripts/buildSyntheticPlugin.ts
969
- import * as fs6 from "fs";
957
+ import * as fs8 from "fs";
970
958
  import * as os2 from "os";
971
- import * as path5 from "path";
959
+ import * as path7 from "path";
972
960
  function getPluginsCatalogRoot() {
973
- const here = path5.dirname(new URL(import.meta.url).pathname);
961
+ const here = path7.dirname(new URL(import.meta.url).pathname);
974
962
  const candidates = [
975
- path5.join(here, "..", "plugins"),
963
+ path7.join(here, "..", "plugins"),
976
964
  // dev: src/scripts → src/plugins
977
- path5.join(here, "..", "..", "plugins"),
965
+ path7.join(here, "..", "..", "plugins"),
978
966
  // built: dist/scripts → dist/plugins
979
- path5.join(here, "..", "..", "src", "plugins")
967
+ path7.join(here, "..", "..", "src", "plugins")
980
968
  // fallback
981
969
  ];
982
970
  for (const c of candidates) {
983
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
971
+ if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
984
972
  }
985
973
  return candidates[0];
986
974
  }
@@ -990,50 +978,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
990
978
  if (!needsSynthetic) return;
991
979
  const catalog = getPluginsCatalogRoot();
992
980
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
993
- const root = path5.join(os2.tmpdir(), `kody2-synth-${runId}`);
994
- fs6.mkdirSync(path5.join(root, ".claude-plugin"), { recursive: true });
981
+ const root = path7.join(os2.tmpdir(), `kody2-synth-${runId}`);
982
+ fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
995
983
  if (cc.skills.length > 0) {
996
- const dst = path5.join(root, "skills");
997
- fs6.mkdirSync(dst, { recursive: true });
984
+ const dst = path7.join(root, "skills");
985
+ fs8.mkdirSync(dst, { recursive: true });
998
986
  for (const name of cc.skills) {
999
- const src = path5.join(catalog, "skills", name);
1000
- if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1001
- copyDir(src, path5.join(dst, name));
987
+ const src = path7.join(catalog, "skills", name);
988
+ if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
989
+ copyDir(src, path7.join(dst, name));
1002
990
  }
1003
991
  }
1004
992
  if (cc.commands.length > 0) {
1005
- const dst = path5.join(root, "commands");
1006
- fs6.mkdirSync(dst, { recursive: true });
993
+ const dst = path7.join(root, "commands");
994
+ fs8.mkdirSync(dst, { recursive: true });
1007
995
  for (const name of cc.commands) {
1008
- const src = path5.join(catalog, "commands", `${name}.md`);
1009
- if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1010
- fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
996
+ const src = path7.join(catalog, "commands", `${name}.md`);
997
+ if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
998
+ fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1011
999
  }
1012
1000
  }
1013
1001
  if (cc.subagents.length > 0) {
1014
- const dst = path5.join(root, "agents");
1015
- fs6.mkdirSync(dst, { recursive: true });
1002
+ const dst = path7.join(root, "agents");
1003
+ fs8.mkdirSync(dst, { recursive: true });
1016
1004
  for (const name of cc.subagents) {
1017
- const src = path5.join(catalog, "agents", `${name}.md`);
1018
- if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1019
- fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
1005
+ const src = path7.join(catalog, "agents", `${name}.md`);
1006
+ if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1007
+ fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1020
1008
  }
1021
1009
  }
1022
1010
  if (cc.hooks.length > 0) {
1023
- const dst = path5.join(root, "hooks");
1024
- fs6.mkdirSync(dst, { recursive: true });
1011
+ const dst = path7.join(root, "hooks");
1012
+ fs8.mkdirSync(dst, { recursive: true });
1025
1013
  const merged = { hooks: {} };
1026
1014
  for (const name of cc.hooks) {
1027
- const src = path5.join(catalog, "hooks", `${name}.json`);
1028
- if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1029
- const parsed = JSON.parse(fs6.readFileSync(src, "utf-8"));
1015
+ const src = path7.join(catalog, "hooks", `${name}.json`);
1016
+ if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1017
+ const parsed = JSON.parse(fs8.readFileSync(src, "utf-8"));
1030
1018
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1031
1019
  if (!Array.isArray(entries)) continue;
1032
1020
  if (!merged.hooks[event]) merged.hooks[event] = [];
1033
1021
  merged.hooks[event].push(...entries);
1034
1022
  }
1035
1023
  }
1036
- fs6.writeFileSync(path5.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1024
+ fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1037
1025
  `);
1038
1026
  }
1039
1027
  const manifest = {
@@ -1044,17 +1032,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1044
1032
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
1045
1033
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
1046
1034
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
1047
- fs6.writeFileSync(path5.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1035
+ fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1048
1036
  `);
1049
1037
  ctx.data.syntheticPluginPath = root;
1050
1038
  };
1051
1039
  function copyDir(src, dst) {
1052
- fs6.mkdirSync(dst, { recursive: true });
1053
- for (const ent of fs6.readdirSync(src, { withFileTypes: true })) {
1054
- const s = path5.join(src, ent.name);
1055
- const d = path5.join(dst, ent.name);
1040
+ fs8.mkdirSync(dst, { recursive: true });
1041
+ for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
1042
+ const s = path7.join(src, ent.name);
1043
+ const d = path7.join(dst, ent.name);
1056
1044
  if (ent.isDirectory()) copyDir(s, d);
1057
- else if (ent.isFile()) fs6.copyFileSync(s, d);
1045
+ else if (ent.isFile()) fs8.copyFileSync(s, d);
1058
1046
  }
1059
1047
  }
1060
1048
 
@@ -1120,18 +1108,18 @@ function formatMissesForFeedback(misses) {
1120
1108
  }
1121
1109
 
1122
1110
  // src/prompt.ts
1123
- import * as fs7 from "fs";
1124
- import * as path6 from "path";
1111
+ import * as fs9 from "fs";
1112
+ import * as path8 from "path";
1125
1113
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1126
1114
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1127
1115
  function loadProjectConventions(projectDir) {
1128
1116
  const out = [];
1129
1117
  for (const rel of CONVENTION_FILES) {
1130
- const abs = path6.join(projectDir, rel);
1131
- if (!fs7.existsSync(abs)) continue;
1118
+ const abs = path8.join(projectDir, rel);
1119
+ if (!fs9.existsSync(abs)) continue;
1132
1120
  let content;
1133
1121
  try {
1134
- content = fs7.readFileSync(abs, "utf-8");
1122
+ content = fs9.readFileSync(abs, "utf-8");
1135
1123
  } catch {
1136
1124
  continue;
1137
1125
  }
@@ -1252,8 +1240,8 @@ import { execFileSync as execFileSync4 } from "child_process";
1252
1240
 
1253
1241
  // src/commit.ts
1254
1242
  import { execFileSync as execFileSync3 } from "child_process";
1255
- import * as fs8 from "fs";
1256
- import * as path7 from "path";
1243
+ import * as fs10 from "fs";
1244
+ import * as path9 from "path";
1257
1245
  var FORBIDDEN_PATH_PREFIXES = [
1258
1246
  ".kody/",
1259
1247
  ".kody-engine/",
@@ -1308,18 +1296,18 @@ function tryGit(args, cwd) {
1308
1296
  }
1309
1297
  function abortUnfinishedGitOps(cwd) {
1310
1298
  const aborted = [];
1311
- const gitDir = path7.join(cwd ?? process.cwd(), ".git");
1312
- if (!fs8.existsSync(gitDir)) return aborted;
1313
- if (fs8.existsSync(path7.join(gitDir, "MERGE_HEAD"))) {
1299
+ const gitDir = path9.join(cwd ?? process.cwd(), ".git");
1300
+ if (!fs10.existsSync(gitDir)) return aborted;
1301
+ if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
1314
1302
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1315
1303
  }
1316
- if (fs8.existsSync(path7.join(gitDir, "CHERRY_PICK_HEAD"))) {
1304
+ if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
1317
1305
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1318
1306
  }
1319
- if (fs8.existsSync(path7.join(gitDir, "REVERT_HEAD"))) {
1307
+ if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
1320
1308
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1321
1309
  }
1322
- if (fs8.existsSync(path7.join(gitDir, "rebase-merge")) || fs8.existsSync(path7.join(gitDir, "rebase-apply"))) {
1310
+ if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
1323
1311
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1324
1312
  }
1325
1313
  try {
@@ -1349,6 +1337,19 @@ function listChangedFiles(cwd) {
1349
1337
  const entries = raw.split("\0").filter((e) => e.length > 0);
1350
1338
  return entries.map((e) => e.slice(3)).filter(Boolean);
1351
1339
  }
1340
+ function listFilesInCommit(ref = "HEAD", cwd) {
1341
+ try {
1342
+ const raw = execFileSync3("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1343
+ encoding: "utf-8",
1344
+ cwd,
1345
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1346
+ stdio: ["pipe", "pipe", "pipe"]
1347
+ });
1348
+ return raw.split("\0").map((s) => s.trim()).filter(Boolean);
1349
+ } catch {
1350
+ return [];
1351
+ }
1352
+ }
1352
1353
  function normalizeCommitMessage(raw) {
1353
1354
  const trimmed = raw.trim().replace(/^['"]|['"]$/g, "").trim();
1354
1355
  if (!trimmed) return "chore: kody2 update";
@@ -1361,7 +1362,7 @@ function normalizeCommitMessage(raw) {
1361
1362
  function commitAndPush(branch, agentMessage, cwd) {
1362
1363
  const allChanged = listChangedFiles(cwd);
1363
1364
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1364
- const mergeHeadExists = fs8.existsSync(path7.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1365
+ const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1365
1366
  if (allowedFiles.length === 0 && !mergeHeadExists) {
1366
1367
  return { committed: false, pushed: false, sha: "", message: "" };
1367
1368
  }
@@ -1433,7 +1434,8 @@ var commitAndPush2 = async (ctx, profile) => {
1433
1434
  try {
1434
1435
  const result = commitAndPush(branch, message, ctx.cwd);
1435
1436
  ctx.data.commitResult = result;
1436
- ctx.data.changedFiles = listChangedFiles(ctx.cwd).filter((f) => !isForbiddenPath(f));
1437
+ const postCommitFiles = result.committed ? listFilesInCommit("HEAD", ctx.cwd) : listChangedFiles(ctx.cwd);
1438
+ ctx.data.changedFiles = postCommitFiles.filter((f) => !isForbiddenPath(f));
1437
1439
  } catch (err) {
1438
1440
  ctx.data.commitCrash = err instanceof Error ? err.message : String(err);
1439
1441
  ctx.data.commitResult = { committed: false, pushed: false };
@@ -1456,20 +1458,20 @@ function defaultCommitMessage(mode, data) {
1456
1458
  }
1457
1459
 
1458
1460
  // src/scripts/composePrompt.ts
1459
- import * as fs9 from "fs";
1460
- import * as path8 from "path";
1461
+ import * as fs11 from "fs";
1462
+ import * as path10 from "path";
1461
1463
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
1462
1464
  var composePrompt = async (ctx, profile) => {
1463
1465
  const explicit = ctx.data.promptTemplate;
1464
1466
  const mode = ctx.args.mode;
1465
1467
  const candidates = [
1466
- explicit ? path8.join(profile.dir, explicit) : null,
1467
- mode ? path8.join(profile.dir, "prompts", `${mode}.md`) : null,
1468
- path8.join(profile.dir, "prompt.md")
1468
+ explicit ? path10.join(profile.dir, explicit) : null,
1469
+ mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
1470
+ path10.join(profile.dir, "prompt.md")
1469
1471
  ].filter(Boolean);
1470
1472
  let templatePath = "";
1471
1473
  for (const c of candidates) {
1472
- if (fs9.existsSync(c)) {
1474
+ if (fs11.existsSync(c)) {
1473
1475
  templatePath = c;
1474
1476
  break;
1475
1477
  }
@@ -1477,7 +1479,7 @@ var composePrompt = async (ctx, profile) => {
1477
1479
  if (!templatePath) {
1478
1480
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
1479
1481
  }
1480
- const template = fs9.readFileSync(templatePath, "utf-8");
1482
+ const template = fs11.readFileSync(templatePath, "utf-8");
1481
1483
  const tokens = {
1482
1484
  ...stringifyAll(ctx.args, "args."),
1483
1485
  ...stringifyAll(ctx.data, ""),
@@ -1952,7 +1954,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
1952
1954
 
1953
1955
  // src/gha.ts
1954
1956
  import { execFileSync as execFileSync7 } from "child_process";
1955
- import * as fs10 from "fs";
1957
+ import * as fs12 from "fs";
1956
1958
  function getRunUrl() {
1957
1959
  const server = process.env.GITHUB_SERVER_URL;
1958
1960
  const repo = process.env.GITHUB_REPOSITORY;
@@ -1963,10 +1965,10 @@ function getRunUrl() {
1963
1965
  function reactToTriggerComment(cwd) {
1964
1966
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
1965
1967
  const eventPath = process.env.GITHUB_EVENT_PATH;
1966
- if (!eventPath || !fs10.existsSync(eventPath)) return;
1968
+ if (!eventPath || !fs12.existsSync(eventPath)) return;
1967
1969
  let event = null;
1968
1970
  try {
1969
- event = JSON.parse(fs10.readFileSync(eventPath, "utf-8"));
1971
+ event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
1970
1972
  } catch {
1971
1973
  return;
1972
1974
  }
@@ -2192,35 +2194,35 @@ function tryPostPr2(prNumber, body, cwd) {
2192
2194
 
2193
2195
  // src/scripts/initFlow.ts
2194
2196
  import { execFileSync as execFileSync9 } from "child_process";
2195
- import * as fs12 from "fs";
2196
- import * as path10 from "path";
2197
+ import * as fs14 from "fs";
2198
+ import * as path12 from "path";
2197
2199
 
2198
2200
  // src/registry.ts
2199
- import * as fs11 from "fs";
2200
- import * as path9 from "path";
2201
+ import * as fs13 from "fs";
2202
+ import * as path11 from "path";
2201
2203
  function getExecutablesRoot() {
2202
- const here = path9.dirname(new URL(import.meta.url).pathname);
2204
+ const here = path11.dirname(new URL(import.meta.url).pathname);
2203
2205
  const candidates = [
2204
- path9.join(here, "executables"),
2206
+ path11.join(here, "executables"),
2205
2207
  // dev: src/
2206
- path9.join(here, "..", "executables"),
2208
+ path11.join(here, "..", "executables"),
2207
2209
  // built: dist/bin → dist/executables
2208
- path9.join(here, "..", "src", "executables")
2210
+ path11.join(here, "..", "src", "executables")
2209
2211
  // fallback
2210
2212
  ];
2211
2213
  for (const c of candidates) {
2212
- if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
2214
+ if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2213
2215
  }
2214
2216
  return candidates[0];
2215
2217
  }
2216
2218
  function listExecutables(root = getExecutablesRoot()) {
2217
- if (!fs11.existsSync(root)) return [];
2218
- const entries = fs11.readdirSync(root, { withFileTypes: true });
2219
+ if (!fs13.existsSync(root)) return [];
2220
+ const entries = fs13.readdirSync(root, { withFileTypes: true });
2219
2221
  const out = [];
2220
2222
  for (const ent of entries) {
2221
2223
  if (!ent.isDirectory()) continue;
2222
- const profilePath = path9.join(root, ent.name, "profile.json");
2223
- if (fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile()) {
2224
+ const profilePath = path11.join(root, ent.name, "profile.json");
2225
+ if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
2224
2226
  out.push({ name: ent.name, profilePath });
2225
2227
  }
2226
2228
  }
@@ -2228,8 +2230,8 @@ function listExecutables(root = getExecutablesRoot()) {
2228
2230
  }
2229
2231
  function hasExecutable(name, root = getExecutablesRoot()) {
2230
2232
  if (!isSafeName(name)) return false;
2231
- const profilePath = path9.join(root, name, "profile.json");
2232
- return fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile();
2233
+ const profilePath = path11.join(root, name, "profile.json");
2234
+ return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
2233
2235
  }
2234
2236
  function isSafeName(name) {
2235
2237
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -2258,9 +2260,9 @@ function parseGenericFlags(argv) {
2258
2260
 
2259
2261
  // src/scripts/initFlow.ts
2260
2262
  function detectPackageManager(cwd) {
2261
- if (fs12.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2262
- if (fs12.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
2263
- if (fs12.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
2263
+ if (fs14.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2264
+ if (fs14.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
2265
+ if (fs14.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
2264
2266
  return "npm";
2265
2267
  }
2266
2268
  function qualityCommandsFor(pm) {
@@ -2381,22 +2383,22 @@ function performInit(cwd, force) {
2381
2383
  const pm = detectPackageManager(cwd);
2382
2384
  const ownerRepo = detectOwnerRepo(cwd);
2383
2385
  const defaultBranch = defaultBranchFromGit(cwd);
2384
- const configPath = path10.join(cwd, "kody.config.json");
2385
- if (fs12.existsSync(configPath) && !force) {
2386
+ const configPath = path12.join(cwd, "kody.config.json");
2387
+ if (fs14.existsSync(configPath) && !force) {
2386
2388
  skipped.push("kody.config.json");
2387
2389
  } else {
2388
2390
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
2389
- fs12.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2391
+ fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2390
2392
  `);
2391
2393
  wrote.push("kody.config.json");
2392
2394
  }
2393
- const workflowDir = path10.join(cwd, ".github", "workflows");
2394
- const workflowPath = path10.join(workflowDir, "kody2.yml");
2395
- if (fs12.existsSync(workflowPath) && !force) {
2395
+ const workflowDir = path12.join(cwd, ".github", "workflows");
2396
+ const workflowPath = path12.join(workflowDir, "kody2.yml");
2397
+ if (fs14.existsSync(workflowPath) && !force) {
2396
2398
  skipped.push(".github/workflows/kody2.yml");
2397
2399
  } else {
2398
- fs12.mkdirSync(workflowDir, { recursive: true });
2399
- fs12.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2400
+ fs14.mkdirSync(workflowDir, { recursive: true });
2401
+ fs14.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2400
2402
  wrote.push(".github/workflows/kody2.yml");
2401
2403
  }
2402
2404
  for (const exe of listExecutables()) {
@@ -2407,12 +2409,12 @@ function performInit(cwd, force) {
2407
2409
  continue;
2408
2410
  }
2409
2411
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
2410
- const target = path10.join(workflowDir, `kody2-${exe.name}.yml`);
2411
- if (fs12.existsSync(target) && !force) {
2412
+ const target = path12.join(workflowDir, `kody2-${exe.name}.yml`);
2413
+ if (fs14.existsSync(target) && !force) {
2412
2414
  skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
2413
2415
  continue;
2414
2416
  }
2415
- fs12.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2417
+ fs14.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2416
2418
  wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
2417
2419
  }
2418
2420
  return { wrote, skipped };
@@ -2919,8 +2921,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
2919
2921
 
2920
2922
  // src/scripts/releaseFlow.ts
2921
2923
  import { execFileSync as execFileSync11, spawnSync } from "child_process";
2922
- import * as fs13 from "fs";
2923
- import * as path11 from "path";
2924
+ import * as fs15 from "fs";
2925
+ import * as path13 from "path";
2924
2926
  function bumpVersion(current, bump) {
2925
2927
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2926
2928
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -2936,12 +2938,12 @@ function bumpVersion(current, bump) {
2936
2938
  return `${major}.${minor}.${patch}`;
2937
2939
  }
2938
2940
  function updateVersionInFile(file, newVersion, cwd) {
2939
- const abs = path11.join(cwd, file);
2940
- if (!fs13.existsSync(abs)) return false;
2941
- const content = fs13.readFileSync(abs, "utf-8");
2941
+ const abs = path13.join(cwd, file);
2942
+ if (!fs15.existsSync(abs)) return false;
2943
+ const content = fs15.readFileSync(abs, "utf-8");
2942
2944
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2943
2945
  if (updated === content) return false;
2944
- fs13.writeFileSync(abs, updated);
2946
+ fs15.writeFileSync(abs, updated);
2945
2947
  return true;
2946
2948
  }
2947
2949
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2989,19 +2991,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2989
2991
  return parts.join("\n");
2990
2992
  }
2991
2993
  function prependChangelog(cwd, entry) {
2992
- const p = path11.join(cwd, "CHANGELOG.md");
2994
+ const p = path13.join(cwd, "CHANGELOG.md");
2993
2995
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2994
- if (fs13.existsSync(p)) {
2995
- const prior = fs13.readFileSync(p, "utf-8");
2996
+ if (fs15.existsSync(p)) {
2997
+ const prior = fs15.readFileSync(p, "utf-8");
2996
2998
  if (/^#\s*Changelog\b/m.test(prior)) {
2997
2999
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2998
- fs13.writeFileSync(p, `${prior.slice(0, idx + 1)}
3000
+ fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
2999
3001
  ${entry}${prior.slice(idx + 1)}`);
3000
3002
  } else {
3001
- fs13.writeFileSync(p, `${header}${entry}${prior}`);
3003
+ fs15.writeFileSync(p, `${header}${entry}${prior}`);
3002
3004
  }
3003
3005
  } else {
3004
- fs13.writeFileSync(p, `${header}${entry}`);
3006
+ fs15.writeFileSync(p, `${header}${entry}`);
3005
3007
  }
3006
3008
  }
3007
3009
  function git3(args, cwd, timeout = 6e4) {
@@ -3052,13 +3054,13 @@ var releaseFlow = async (ctx) => {
3052
3054
  };
3053
3055
  async function runPrepare(args) {
3054
3056
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
3055
- const pkgPath = path11.join(cwd, "package.json");
3056
- if (!fs13.existsSync(pkgPath)) {
3057
+ const pkgPath = path13.join(cwd, "package.json");
3058
+ if (!fs15.existsSync(pkgPath)) {
3057
3059
  ctx.output.exitCode = 99;
3058
3060
  ctx.output.reason = "release prepare: package.json not found";
3059
3061
  return;
3060
3062
  }
3061
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3063
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3062
3064
  if (typeof pkg.version !== "string") {
3063
3065
  ctx.output.exitCode = 99;
3064
3066
  ctx.output.reason = "release prepare: package.json has no version";
@@ -3129,8 +3131,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
3129
3131
  }
3130
3132
  async function runFinalize(args) {
3131
3133
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
3132
- const pkgPath = path11.join(cwd, "package.json");
3133
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3134
+ const pkgPath = path13.join(cwd, "package.json");
3135
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3134
3136
  if (typeof pkg.version !== "string") {
3135
3137
  ctx.output.exitCode = 99;
3136
3138
  ctx.output.reason = "release finalize: package.json has no version";
@@ -3840,7 +3842,7 @@ var watchStalePrsFlow = async (ctx) => {
3840
3842
  };
3841
3843
 
3842
3844
  // src/scripts/writeRunSummary.ts
3843
- import * as fs14 from "fs";
3845
+ import * as fs16 from "fs";
3844
3846
  var writeRunSummary = async (ctx, profile) => {
3845
3847
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
3846
3848
  if (!summaryPath) return;
@@ -3862,7 +3864,7 @@ var writeRunSummary = async (ctx, profile) => {
3862
3864
  if (reason) lines.push(`- **Reason:** ${reason}`);
3863
3865
  lines.push("");
3864
3866
  try {
3865
- fs14.appendFileSync(summaryPath, `${lines.join("\n")}
3867
+ fs16.appendFileSync(summaryPath, `${lines.join("\n")}
3866
3868
  `);
3867
3869
  } catch {
3868
3870
  }
@@ -4010,9 +4012,9 @@ async function runExecutable(profileName, input) {
4010
4012
  data: {},
4011
4013
  output: { exitCode: 0 }
4012
4014
  };
4013
- const ndjsonDir = path12.join(input.cwd, ".kody2");
4015
+ const ndjsonDir = path14.join(input.cwd, ".kody2");
4014
4016
  const invokeAgent = async (prompt) => {
4015
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path12.isAbsolute(p) ? p : path12.resolve(profile.dir, p)).filter((p) => p.length > 0);
4017
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path14.isAbsolute(p) ? p : path14.resolve(profile.dir, p)).filter((p) => p.length > 0);
4016
4018
  const syntheticPath = ctx.data.syntheticPluginPath;
4017
4019
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
4018
4020
  return runAgent({
@@ -4028,6 +4030,7 @@ async function runExecutable(profileName, input) {
4028
4030
  mcpServers: profile.claudeCode.mcpServers,
4029
4031
  pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
4030
4032
  maxTurns: profile.claudeCode.maxTurns,
4033
+ maxThinkingTokens: profile.claudeCode.maxThinkingTokens,
4031
4034
  systemPromptAppend: profile.claudeCode.systemPromptAppend,
4032
4035
  settingSources: profile.claudeCode.settingSources
4033
4036
  });
@@ -4078,17 +4081,17 @@ async function runExecutable(profileName, input) {
4078
4081
  }
4079
4082
  }
4080
4083
  function resolveProfilePath(profileName) {
4081
- const here = path12.dirname(new URL(import.meta.url).pathname);
4084
+ const here = path14.dirname(new URL(import.meta.url).pathname);
4082
4085
  const candidates = [
4083
- path12.join(here, "executables", profileName, "profile.json"),
4086
+ path14.join(here, "executables", profileName, "profile.json"),
4084
4087
  // same-dir sibling (dev)
4085
- path12.join(here, "..", "executables", profileName, "profile.json"),
4088
+ path14.join(here, "..", "executables", profileName, "profile.json"),
4086
4089
  // up one (prod: dist/bin → dist/executables)
4087
- path12.join(here, "..", "src", "executables", profileName, "profile.json")
4090
+ path14.join(here, "..", "src", "executables", profileName, "profile.json")
4088
4091
  // fallback
4089
4092
  ];
4090
4093
  for (const c of candidates) {
4091
- if (fs15.existsSync(c)) return c;
4094
+ if (fs17.existsSync(c)) return c;
4092
4095
  }
4093
4096
  return candidates[0];
4094
4097
  }
@@ -4264,9 +4267,9 @@ function resolveAuthToken(env = process.env) {
4264
4267
  return token;
4265
4268
  }
4266
4269
  function detectPackageManager2(cwd) {
4267
- if (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4268
- if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
4269
- if (fs16.existsSync(path13.join(cwd, "bun.lockb"))) return "bun";
4270
+ if (fs18.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4271
+ if (fs18.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
4272
+ if (fs18.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
4270
4273
  return "npm";
4271
4274
  }
4272
4275
  function shellOut(cmd, args, cwd, stream = true) {
@@ -4346,11 +4349,11 @@ function configureGitIdentity(cwd) {
4346
4349
  }
4347
4350
  function postFailureTail(issueNumber, cwd, reason) {
4348
4351
  if (!issueNumber) return;
4349
- const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
4352
+ const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
4350
4353
  let tail = "";
4351
4354
  try {
4352
- if (fs16.existsSync(logPath)) {
4353
- const content = fs16.readFileSync(logPath, "utf-8");
4355
+ if (fs18.existsSync(logPath)) {
4356
+ const content = fs18.readFileSync(logPath, "utf-8");
4354
4357
  tail = content.slice(-3e3);
4355
4358
  }
4356
4359
  } catch {
@@ -4375,7 +4378,7 @@ async function runCi(argv) {
4375
4378
  return 0;
4376
4379
  }
4377
4380
  const args = parseCiArgs(argv);
4378
- const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
4381
+ const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
4379
4382
  let earlyConfig;
4380
4383
  try {
4381
4384
  earlyConfig = loadConfig(cwd);
@@ -4468,23 +4471,23 @@ var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
4468
4471
  var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
4469
4472
 
4470
4473
  Usage:
4471
- kody2 chat [--session <id>] [--model <provider/model>]
4474
+ kody2 chat [--session <id>] [--message <text>] [--model <provider/model>]
4472
4475
  [--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
4473
4476
 
4474
- All inputs may also come from env: SESSION_ID, MODEL, DASHBOARD_URL.
4475
- CLI flags take precedence over env. SESSION_ID and DASHBOARD_URL are required
4476
- (the runner long-polls the dashboard for user turns and pushes events back).
4477
+ All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
4478
+ CLI flags take precedence over env. SESSION_ID is required.
4477
4479
 
4478
4480
  Exit codes:
4479
- 0 session exited cleanly (idle or hard timeout)
4480
- 64 bad inputs
4481
- 99 runtime failure (agent crash, pull failure, LiteLLM failure)
4481
+ 0 reply emitted successfully
4482
+ 64 bad inputs (missing session, empty history)
4483
+ 99 runtime failure (agent crash, LiteLLM failure)
4482
4484
  `;
4483
4485
  function parseChatArgs(argv, env = process.env) {
4484
4486
  const result = { errors: [] };
4485
4487
  for (let i = 0; i < argv.length; i++) {
4486
4488
  const arg = argv[i];
4487
4489
  if (arg === "--session") result.sessionId = argv[++i];
4490
+ else if (arg === "--message") result.initMessage = argv[++i];
4488
4491
  else if (arg === "--model") result.model = argv[++i];
4489
4492
  else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
4490
4493
  else if (arg === "--cwd") result.cwd = argv[++i];
@@ -4495,18 +4498,34 @@ function parseChatArgs(argv, env = process.env) {
4495
4498
  else if (arg) result.errors.push(`unexpected positional: ${arg}`);
4496
4499
  }
4497
4500
  result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
4501
+ result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
4498
4502
  result.model = result.model ?? env.MODEL ?? void 0;
4499
4503
  result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
4500
- for (const key of ["sessionId", "model", "dashboardUrl"]) {
4504
+ for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
4501
4505
  const v = result[key];
4502
4506
  if (typeof v === "string" && v.trim() === "") result[key] = void 0;
4503
4507
  }
4504
- if (!result.errors.includes("__HELP__")) {
4505
- if (!result.sessionId) result.errors.push("--session <id> (or SESSION_ID env) is required");
4506
- if (!result.dashboardUrl) result.errors.push("--dashboard-url <url> (or DASHBOARD_URL env) is required");
4508
+ if (!result.sessionId && !result.errors.includes("__HELP__")) {
4509
+ result.errors.push("--session <id> (or SESSION_ID env) is required");
4507
4510
  }
4508
4511
  return result;
4509
4512
  }
4513
+ function commitChatFiles(cwd, sessionId, verbose) {
4514
+ const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
4515
+ const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
4516
+ const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
4517
+ if (paths.length === 0) return;
4518
+ const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
4519
+ try {
4520
+ execFileSync16("git", ["add", ...paths], opts);
4521
+ execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
4522
+ execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
4523
+ } catch (err) {
4524
+ const msg = err instanceof Error ? err.message : String(err);
4525
+ process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
4526
+ `);
4527
+ }
4528
+ }
4510
4529
  function tryLoadConfig(cwd) {
4511
4530
  try {
4512
4531
  return loadConfig(cwd);
@@ -4514,6 +4533,11 @@ function tryLoadConfig(cwd) {
4514
4533
  return null;
4515
4534
  }
4516
4535
  }
4536
+ function buildSink(cwd, sessionId, dashboardUrl) {
4537
+ const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
4538
+ if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
4539
+ return new TeeSink(sinks);
4540
+ }
4517
4541
  async function runChat(argv) {
4518
4542
  if (argv.includes("--help") || argv.includes("-h")) {
4519
4543
  process.stdout.write(CHAT_HELP);
@@ -4527,9 +4551,8 @@ async function runChat(argv) {
4527
4551
  ${CHAT_HELP}`);
4528
4552
  return 64;
4529
4553
  }
4530
- const cwd = args.cwd ? path14.resolve(args.cwd) : process.cwd();
4554
+ const cwd = args.cwd ? path16.resolve(args.cwd) : process.cwd();
4531
4555
  const sessionId = args.sessionId;
4532
- const dashboardUrl = args.dashboardUrl;
4533
4556
  const unpackedSecrets = unpackAllSecrets();
4534
4557
  if (unpackedSecrets > 0) {
4535
4558
  process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
@@ -4555,20 +4578,13 @@ ${CHAT_HELP}`);
4555
4578
  return 99;
4556
4579
  }
4557
4580
  }
4558
- let sink;
4559
- try {
4560
- sink = new HttpSink(dashboardUrl, sessionId);
4561
- } catch (err) {
4562
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4563
- `);
4564
- return 64;
4565
- }
4566
4581
  let litellm = null;
4567
4582
  try {
4568
4583
  litellm = await startLitellmIfNeeded(model, cwd);
4569
4584
  } catch (err) {
4570
4585
  const msg = err instanceof Error ? err.message : String(err);
4571
- await sink.emit({
4586
+ const sink2 = buildSink(cwd, sessionId, args.dashboardUrl);
4587
+ await sink2.emit({
4572
4588
  event: "chat.error",
4573
4589
  payload: { sessionId, error: `litellm startup failed: ${msg}` },
4574
4590
  runId: makeRunId(sessionId, "error"),
@@ -4576,33 +4592,21 @@ ${CHAT_HELP}`);
4576
4592
  });
4577
4593
  return 99;
4578
4594
  }
4579
- let pull;
4595
+ const sessionFile = sessionFilePath(cwd, sessionId);
4596
+ if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
4597
+ const sink = buildSink(cwd, sessionId, args.dashboardUrl);
4580
4598
  try {
4581
- pull = createPullClient({ baseUrl: dashboardUrl, sessionId });
4582
- } catch (err) {
4583
- process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4584
- `);
4585
- try {
4586
- litellm?.kill();
4587
- } catch {
4588
- }
4589
- return 64;
4590
- }
4591
- process.stdout.write(`\u2192 kody2 chat: session ${sessionId}, model ${model.provider}/${model.model}
4592
- `);
4593
- try {
4594
- const result = await runChatSession({
4599
+ const result = await runChatTurn({
4595
4600
  sessionId,
4601
+ sessionFile,
4596
4602
  cwd,
4597
4603
  model,
4598
4604
  litellmUrl: litellm?.url ?? null,
4599
4605
  sink,
4600
- pull,
4601
4606
  verbose: args.verbose,
4602
4607
  quiet: args.quiet
4603
4608
  });
4604
- process.stdout.write(`\u2192 kody2 chat: exited (${result.reason ?? "ok"}) after ${result.turnsProcessed} turn(s)
4605
- `);
4609
+ commitChatFiles(cwd, sessionId, args.verbose ?? false);
4606
4610
  return result.exitCode;
4607
4611
  } finally {
4608
4612
  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.34",
3
+ "version": "0.2.36",
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",