@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.
- package/dist/bin/kody.js +179 -11
- 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.
|
|
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
|
|
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
|
-
|
|
2521
|
+
${capabilityMarker(capabilitySlug)}` : mentioned;
|
|
2449
2522
|
try {
|
|
2450
|
-
|
|
2451
|
-
|
|
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: ${
|
|
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 =
|
|
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.
|
|
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",
|