@kody-ade/kody-engine 0.4.287 → 0.4.288

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin/kody.js +179 -11
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.287",
18
+ version: "0.4.288",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -671,11 +671,51 @@ function parseProviderModel(s) {
671
671
  }
672
672
  return { provider: s.slice(0, slash), model: s.slice(slash + 1) };
673
673
  }
674
+ function optionalRuntimeString(raw, key) {
675
+ const value = raw[key];
676
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
677
+ }
678
+ function parseModelRuntimeConfig(modelSpec, rawConfig) {
679
+ const fallback = parseProviderModel(modelSpec);
680
+ const raw = rawConfig?.trim();
681
+ if (!raw) return fallback;
682
+ let parsed;
683
+ try {
684
+ parsed = JSON.parse(raw);
685
+ } catch (err) {
686
+ const msg = err instanceof Error ? err.message : String(err);
687
+ throw new Error(`KODY_MODEL_CONFIG is invalid JSON: ${msg}`);
688
+ }
689
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
690
+ throw new Error("KODY_MODEL_CONFIG must be a JSON object");
691
+ }
692
+ const record2 = parsed;
693
+ const modelName = optionalRuntimeString(record2, "modelName");
694
+ if (!modelName) {
695
+ throw new Error("KODY_MODEL_CONFIG.modelName is required");
696
+ }
697
+ const protocol = optionalRuntimeString(record2, "protocol");
698
+ const baseURL = optionalRuntimeString(record2, "baseURL");
699
+ const apiKeyEnvVar = optionalRuntimeString(record2, "apiKeyEnvVar");
700
+ const spec = optionalRuntimeString(record2, "spec");
701
+ const provider = optionalRuntimeString(record2, "provider") ?? fallback.provider;
702
+ const out = {
703
+ provider,
704
+ model: modelName
705
+ };
706
+ if (protocol) out.protocol = protocol;
707
+ if (baseURL) out.baseURL = baseURL;
708
+ if (apiKeyEnvVar) out.apiKeyEnvVar = apiKeyEnvVar;
709
+ if (spec) out.spec = spec;
710
+ if (protocol === "openai") out.litellmProvider = "openai";
711
+ return out;
712
+ }
674
713
  function providerApiKeyEnvVar(provider) {
675
714
  if (provider === "anthropic" || provider === "claude") return "ANTHROPIC_API_KEY";
676
715
  return `${provider.toUpperCase()}_API_KEY`;
677
716
  }
678
717
  function needsLitellmProxy(model) {
718
+ if (model.protocol === "anthropic") return false;
679
719
  return model.provider !== "claude" && model.provider !== "anthropic";
680
720
  }
681
721
  function loadConfig(projectDir = process.cwd()) {
@@ -2441,14 +2481,50 @@ function listRepairCandidates(repoSlug) {
2441
2481
  function dispatchVerb(workflowFile, repoSlug, capability, prNumber) {
2442
2482
  return dispatchWorkflow(workflowFile, capability, prNumber, repoSlug);
2443
2483
  }
2444
- function postRecommendation(prNumber, mention, message, capabilitySlug) {
2484
+ function capabilityMarker(slug2) {
2485
+ return `<!-- kody-capability: ${slug2} -->`;
2486
+ }
2487
+ function normalizeRecommendationIntent(body) {
2488
+ const marker = body.match(/<!--\s*kody-intent:\s*([\s\S]*?)-->/i);
2489
+ if (marker?.[1]) return marker[1].trim().replace(/\s+/g, " ").toLowerCase();
2490
+ const legacy = body.match(/(?:^|\n)\s*(?:kody-cmd:\s*|@kody\s+)([a-z][\w-]*(?:\s+--pr\s+\d+)?)/i);
2491
+ if (legacy?.[1]) return legacy[1].trim().replace(/\s+/g, " ").toLowerCase();
2492
+ return null;
2493
+ }
2494
+ function containsExecutableKodyCommand(body) {
2495
+ return /@kody\b/i.test(body) || /\bkody-cmd\s*:/i.test(body);
2496
+ }
2497
+ function recommendationAlreadyExists(repoSlug, prNumber, body, capabilitySlug) {
2498
+ const requestedIntent = normalizeRecommendationIntent(body);
2499
+ const requestedText = body.trim().replace(/\s+/g, " ");
2500
+ const raw = gh(["issue", "view", String(prNumber), "-R", repoSlug, "--json", "comments"]);
2501
+ const parsed = JSON.parse(raw);
2502
+ return (parsed.comments ?? []).some((comment) => {
2503
+ const existing = comment.body ?? "";
2504
+ if (capabilitySlug && !existing.includes(capabilityMarker(capabilitySlug))) return false;
2505
+ const existingIntent = normalizeRecommendationIntent(existing);
2506
+ if (requestedIntent && existingIntent) return requestedIntent === existingIntent;
2507
+ if (capabilitySlug && requestedIntent) return true;
2508
+ return existing.trim().replace(/\s+/g, " ").includes(requestedText);
2509
+ });
2510
+ }
2511
+ function postRecommendation(repoSlug, prNumber, mention, message, capabilitySlug) {
2512
+ if (containsExecutableKodyCommand(message)) {
2513
+ return {
2514
+ ok: false,
2515
+ error: "recommendation body contains executable Kody command text; use inert kody-intent metadata"
2516
+ };
2517
+ }
2445
2518
  const mentioned = mention ? `${mention} ${message}` : message;
2446
2519
  const body = capabilitySlug ? `${mentioned}
2447
2520
 
2448
- <!-- kody-capability: ${capabilitySlug} -->` : mentioned;
2521
+ ${capabilityMarker(capabilitySlug)}` : mentioned;
2449
2522
  try {
2450
- gh(["pr", "comment", String(prNumber), "--body", body]);
2451
- return { ok: true };
2523
+ if (recommendationAlreadyExists(repoSlug, prNumber, body, capabilitySlug)) {
2524
+ return { ok: true, posted: false };
2525
+ }
2526
+ gh(["issue", "comment", String(prNumber), "-R", repoSlug, "--body-file", "-"], { input: body });
2527
+ return { ok: true, posted: true };
2452
2528
  } catch (err) {
2453
2529
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
2454
2530
  }
@@ -2689,8 +2765,8 @@ function capabilityToolDefinitions(opts) {
2689
2765
  handler: async (args) => {
2690
2766
  const pr = Number(args.pr);
2691
2767
  const body = String(args.body ?? "");
2692
- const result = postRecommendation(pr, opts.operatorMention, body, opts.capabilitySlug);
2693
- const text = result.ok ? `Recommendation posted on PR #${pr}.` : `Recommendation failed on PR #${pr}: ${result.error}`;
2768
+ const result = postRecommendation(opts.repoSlug, pr, opts.operatorMention, body, opts.capabilitySlug);
2769
+ const text = result.ok ? result.posted ? `Recommendation posted on PR #${pr}.` : `Recommendation already exists on PR #${pr}; skipped.` : `Recommendation failed on PR #${pr}: ${result.error}`;
2694
2770
  return { content: [{ type: "text", text }] };
2695
2771
  }
2696
2772
  };
@@ -5854,13 +5930,16 @@ function resolveLitellmTimeoutMs() {
5854
5930
  return DEFAULT_LITELLM_STARTUP_TIMEOUT_SEC * 1e3;
5855
5931
  }
5856
5932
  function generateLitellmConfigYaml(model) {
5857
- const apiKeyVar = providerApiKeyEnvVar(model.provider);
5933
+ const apiKeyVar = model.apiKeyEnvVar ?? providerApiKeyEnvVar(model.provider);
5934
+ const litellmProvider = model.litellmProvider ?? model.provider;
5935
+ const providerParams = model.baseURL ? [["api_base", model.baseURL]] : [];
5858
5936
  return [
5859
5937
  "model_list:",
5860
5938
  ` - model_name: ${model.model}`,
5861
5939
  ` litellm_params:`,
5862
- ` model: ${model.provider}/${model.model}`,
5940
+ ` model: ${litellmProvider}/${model.model}`,
5863
5941
  ` api_key: os.environ/${apiKeyVar}`,
5942
+ ...providerParams.map(([key, value]) => ` ${key}: ${value}`),
5864
5943
  "",
5865
5944
  "litellm_settings:",
5866
5945
  " drop_params: true",
@@ -19385,6 +19464,16 @@ var CHAT_SYSTEM_PROMPT = [
19385
19464
  "Do not invent file paths, commit SHAs, line numbers, or command output. If you",
19386
19465
  "cite something concrete, you must have just read or run it in this session."
19387
19466
  ].join("\n");
19467
+ var OPENAI_CHAT_SYSTEM_PROMPT = [
19468
+ "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
19469
+ "user's latest message using the full conversation below as context. Keep replies",
19470
+ "short and simple. Use plain terms, not jargon.",
19471
+ "",
19472
+ "This chat is running through an OpenAI-compatible model endpoint. Answer directly",
19473
+ "from the conversation and provided context. Do not claim to run shell commands,",
19474
+ "edit files, inspect the repository, or use tools unless the user has supplied",
19475
+ "the relevant content in the chat."
19476
+ ].join("\n");
19388
19477
  var CROSS_REPO_PROMPT = [
19389
19478
  "# Working across repositories",
19390
19479
  "You are NOT limited to the repository at your current working directory. You",
@@ -19446,7 +19535,7 @@ async function runChatTurn(opts) {
19446
19535
  return { exitCode: 64, error };
19447
19536
  }
19448
19537
  const { turns: promptTurns, imagePaths } = prepareAttachments(turns, opts.cwd, opts.sessionId);
19449
- const basePrompt = opts.systemPrompt ?? CHAT_SYSTEM_PROMPT;
19538
+ const basePrompt = opts.systemPrompt ?? (opts.model.protocol === "openai" ? OPENAI_CHAT_SYSTEM_PROMPT : CHAT_SYSTEM_PROMPT);
19450
19539
  const catalog = buildExecutableCatalog();
19451
19540
  const taskArtifactsPaths = prepareTaskArtifactsDir(opts.cwd, opts.sessionId);
19452
19541
  const artifactAddendum = taskArtifactsPromptAddendum({
@@ -19480,6 +19569,14 @@ async function runChatTurn(opts) {
19480
19569
  artifactAddendum
19481
19570
  ].filter((s) => typeof s === "string" && s.length > 0).join("\n\n");
19482
19571
  const prompt = buildPrompt(promptTurns);
19572
+ if (opts.model.protocol === "openai" && opts.litellmUrl) {
19573
+ return runOpenAIChatTurn({
19574
+ opts,
19575
+ turns: promptTurns,
19576
+ systemPrompt,
19577
+ sessionFile: opts.sessionFile
19578
+ });
19579
+ }
19483
19580
  let progressSeq = 0;
19484
19581
  const invoke = opts.invokeAgent ?? ((p) => runAgent({
19485
19582
  prompt: p,
@@ -19582,6 +19679,74 @@ async function runChatTurn(opts) {
19582
19679
  }
19583
19680
  return { exitCode: 0, reply };
19584
19681
  }
19682
+ async function runOpenAIChatTurn(args) {
19683
+ const { opts, turns, systemPrompt, sessionFile } = args;
19684
+ const doFetch = opts.fetchImpl ?? fetch;
19685
+ const url = `${opts.litellmUrl.replace(/\/+$/, "")}/v1/chat/completions`;
19686
+ try {
19687
+ const response = await doFetch(url, {
19688
+ method: "POST",
19689
+ headers: { "Content-Type": "application/json" },
19690
+ body: JSON.stringify({
19691
+ model: opts.model.model,
19692
+ messages: [
19693
+ { role: "system", content: systemPrompt },
19694
+ ...turns.map((turn) => ({
19695
+ role: turn.role,
19696
+ content: turn.content
19697
+ }))
19698
+ ],
19699
+ stream: false
19700
+ })
19701
+ });
19702
+ if (!response.ok) {
19703
+ const text = await response.text().catch(() => "");
19704
+ const error = `OpenAI-compatible model request failed ${response.status}${text ? `: ${text.slice(0, 500)}` : ""}`;
19705
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
19706
+ return { exitCode: 99, error };
19707
+ }
19708
+ const payload = await response.json();
19709
+ const reply = extractOpenAIReply(payload).trim();
19710
+ if (!reply) {
19711
+ const error = typeof payload.error?.message === "string" ? payload.error.message : "OpenAI-compatible model completed without producing a reply";
19712
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
19713
+ return { exitCode: 99, error };
19714
+ }
19715
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19716
+ appendTurn(sessionFile, {
19717
+ role: "assistant",
19718
+ content: reply,
19719
+ timestamp: now
19720
+ });
19721
+ await emit(opts.sink, "chat.message", opts.sessionId, "message", {
19722
+ sessionId: opts.sessionId,
19723
+ role: "assistant",
19724
+ content: reply,
19725
+ timestamp: now
19726
+ });
19727
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
19728
+ return { exitCode: 0, reply };
19729
+ } catch (err) {
19730
+ const error = err instanceof Error ? err.message : String(err);
19731
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
19732
+ return { exitCode: 99, error };
19733
+ }
19734
+ }
19735
+ function extractOpenAIReply(payload) {
19736
+ const content = payload.choices?.[0]?.message?.content;
19737
+ if (typeof content === "string") return content;
19738
+ if (Array.isArray(content)) {
19739
+ return content.map((part) => {
19740
+ if (typeof part === "string") return part;
19741
+ if (part && typeof part === "object" && "text" in part) {
19742
+ const text = part.text;
19743
+ return typeof text === "string" ? text : "";
19744
+ }
19745
+ return "";
19746
+ }).join("");
19747
+ }
19748
+ return "";
19749
+ }
19585
19750
  function buildPrompt(turns) {
19586
19751
  const body = turns.map((t) => `${t.role === "user" ? "User" : "Assistant"}: ${t.content}`).join("\n\n");
19587
19752
  return `${body}
@@ -21424,7 +21589,10 @@ async function brainServe(opts) {
21424
21589
  }
21425
21590
  const apiKey = getApiKey();
21426
21591
  const port = Number(process.env.PORT ?? DEFAULT_PORT);
21427
- const model = parseProviderModel(process.env.MODEL?.trim() || "claude/claude-haiku-4-5-20251001");
21592
+ const model = parseModelRuntimeConfig(
21593
+ process.env.MODEL?.trim() || "claude/claude-haiku-4-5-20251001",
21594
+ process.env.KODY_MODEL_CONFIG
21595
+ );
21428
21596
  const usesProxy = needsLitellmProxy(model);
21429
21597
  let handle = null;
21430
21598
  if (usesProxy) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.287",
3
+ "version": "0.4.288",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",