@kody-ade/kody-engine 0.4.21 → 0.4.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.24",
|
|
7
7
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -1002,32 +1002,6 @@ function getExecutablesRoot() {
|
|
|
1002
1002
|
function getProjectExecutablesRoot() {
|
|
1003
1003
|
return path6.join(process.cwd(), ".kody", "executables");
|
|
1004
1004
|
}
|
|
1005
|
-
function getBuiltinJobsRoot() {
|
|
1006
|
-
const here = path6.dirname(new URL(import.meta.url).pathname);
|
|
1007
|
-
const candidates = [
|
|
1008
|
-
path6.join(here, "jobs"),
|
|
1009
|
-
// dev: src/
|
|
1010
|
-
path6.join(here, "..", "jobs"),
|
|
1011
|
-
// built: dist/bin → dist/jobs
|
|
1012
|
-
path6.join(here, "..", "src", "jobs")
|
|
1013
|
-
// fallback
|
|
1014
|
-
];
|
|
1015
|
-
for (const c of candidates) {
|
|
1016
|
-
if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
|
|
1017
|
-
}
|
|
1018
|
-
return candidates[0];
|
|
1019
|
-
}
|
|
1020
|
-
function listBuiltinJobs(root = getBuiltinJobsRoot()) {
|
|
1021
|
-
if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
|
|
1022
|
-
const out = [];
|
|
1023
|
-
for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
|
|
1024
|
-
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
1025
|
-
const slug = ent.name.slice(0, -3);
|
|
1026
|
-
out.push({ slug, filePath: path6.join(root, ent.name) });
|
|
1027
|
-
}
|
|
1028
|
-
out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
1029
|
-
return out;
|
|
1030
|
-
}
|
|
1031
1005
|
function getExecutableRoots() {
|
|
1032
1006
|
return [getProjectExecutablesRoot(), getExecutablesRoot()];
|
|
1033
1007
|
}
|
|
@@ -1303,119 +1277,150 @@ import { execFileSync as execFileSync28, spawn as spawn5 } from "child_process";
|
|
|
1303
1277
|
import * as fs26 from "fs";
|
|
1304
1278
|
import * as path23 from "path";
|
|
1305
1279
|
|
|
1306
|
-
// src/
|
|
1307
|
-
import { execFileSync as execFileSync3
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
async function checkLitellmHealth(url) {
|
|
1312
|
-
try {
|
|
1313
|
-
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
1314
|
-
return response.ok;
|
|
1315
|
-
} catch {
|
|
1316
|
-
return false;
|
|
1317
|
-
}
|
|
1280
|
+
// src/issue.ts
|
|
1281
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1282
|
+
var API_TIMEOUT_MS = 3e4;
|
|
1283
|
+
function ghToken() {
|
|
1284
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
1318
1285
|
}
|
|
1319
|
-
function
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
"
|
|
1329
|
-
|
|
1330
|
-
""
|
|
1331
|
-
].join("\n");
|
|
1286
|
+
function gh(args, options) {
|
|
1287
|
+
const token = ghToken();
|
|
1288
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
1289
|
+
return execFileSync3("gh", args, {
|
|
1290
|
+
encoding: "utf-8",
|
|
1291
|
+
timeout: API_TIMEOUT_MS,
|
|
1292
|
+
cwd: options?.cwd,
|
|
1293
|
+
env,
|
|
1294
|
+
input: options?.input,
|
|
1295
|
+
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
1296
|
+
}).trim();
|
|
1332
1297
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
}
|
|
1298
|
+
function getIssue(issueNumber, cwd) {
|
|
1299
|
+
const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
|
|
1300
|
+
const parsed = JSON.parse(output);
|
|
1301
|
+
if (typeof parsed?.title !== "string") {
|
|
1302
|
+
throw new Error(`Issue #${issueNumber}: unexpected response shape`);
|
|
1338
1303
|
}
|
|
1339
|
-
|
|
1304
|
+
return {
|
|
1305
|
+
number: parsed.number ?? issueNumber,
|
|
1306
|
+
title: parsed.title,
|
|
1307
|
+
body: parsed.body ?? "",
|
|
1308
|
+
comments: (parsed.comments ?? []).map((c) => ({
|
|
1309
|
+
body: c.body ?? "",
|
|
1310
|
+
author: c.author?.login ?? "unknown",
|
|
1311
|
+
createdAt: c.createdAt ?? ""
|
|
1312
|
+
})),
|
|
1313
|
+
labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function stripKodyMentions(body) {
|
|
1317
|
+
return body.replace(/(@)(kody)/gi, "$1\u200B$2");
|
|
1318
|
+
}
|
|
1319
|
+
function postIssueComment(issueNumber, body, cwd) {
|
|
1340
1320
|
try {
|
|
1341
|
-
|
|
1342
|
-
} catch {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
1348
|
-
}
|
|
1321
|
+
gh(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
process.stderr.write(
|
|
1324
|
+
`[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
1325
|
+
`
|
|
1326
|
+
);
|
|
1349
1327
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1328
|
+
}
|
|
1329
|
+
function truncate2(s, maxBytes) {
|
|
1330
|
+
if (s.length <= maxBytes) return s;
|
|
1331
|
+
return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
|
|
1332
|
+
}
|
|
1333
|
+
function parsePrNumber(url) {
|
|
1334
|
+
const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
|
|
1335
|
+
if (!m) return null;
|
|
1336
|
+
const n = parseInt(m[1], 10);
|
|
1337
|
+
return Number.isFinite(n) ? n : null;
|
|
1338
|
+
}
|
|
1339
|
+
function getPr(prNumber, cwd) {
|
|
1340
|
+
const output = gh(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
|
|
1341
|
+
cwd
|
|
1362
1342
|
});
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
if (await checkLitellmHealth(url)) {
|
|
1367
|
-
return {
|
|
1368
|
-
url,
|
|
1369
|
-
kill: () => {
|
|
1370
|
-
try {
|
|
1371
|
-
child.kill();
|
|
1372
|
-
} catch {
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1343
|
+
const parsed = JSON.parse(output);
|
|
1344
|
+
if (typeof parsed?.title !== "string") {
|
|
1345
|
+
throw new Error(`PR #${prNumber}: unexpected response shape`);
|
|
1377
1346
|
}
|
|
1378
|
-
|
|
1347
|
+
return {
|
|
1348
|
+
number: parsed.number ?? prNumber,
|
|
1349
|
+
title: parsed.title,
|
|
1350
|
+
body: parsed.body ?? "",
|
|
1351
|
+
headRefName: String(parsed.headRefName ?? ""),
|
|
1352
|
+
baseRefName: String(parsed.baseRefName ?? ""),
|
|
1353
|
+
state: String(parsed.state ?? "")
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
function getPrDiff(prNumber, cwd) {
|
|
1379
1357
|
try {
|
|
1380
|
-
|
|
1381
|
-
} catch {
|
|
1358
|
+
return gh(["pr", "diff", String(prNumber)], { cwd });
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
process.stderr.write(
|
|
1361
|
+
`[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
1362
|
+
`
|
|
1363
|
+
);
|
|
1364
|
+
return "";
|
|
1382
1365
|
}
|
|
1366
|
+
}
|
|
1367
|
+
function getPrReviews(prNumber, cwd) {
|
|
1383
1368
|
try {
|
|
1384
|
-
|
|
1369
|
+
const output = gh(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
|
|
1370
|
+
const parsed = JSON.parse(output);
|
|
1371
|
+
if (!Array.isArray(parsed?.reviews)) return [];
|
|
1372
|
+
return parsed.reviews.map(
|
|
1373
|
+
(r) => ({
|
|
1374
|
+
body: r.body ?? "",
|
|
1375
|
+
state: r.state ?? "",
|
|
1376
|
+
author: r.author?.login ?? "unknown",
|
|
1377
|
+
submittedAt: r.submittedAt ?? ""
|
|
1378
|
+
})
|
|
1379
|
+
);
|
|
1385
1380
|
} catch {
|
|
1381
|
+
return [];
|
|
1386
1382
|
}
|
|
1387
|
-
throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
|
|
1388
|
-
${logTail}`);
|
|
1389
1383
|
}
|
|
1390
|
-
function
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
const commentIdx = value.indexOf(" #");
|
|
1404
|
-
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
1405
|
-
if (value) result[match[1]] = value;
|
|
1384
|
+
function getPrComments(prNumber, cwd) {
|
|
1385
|
+
try {
|
|
1386
|
+
const output = gh(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
|
|
1387
|
+
const parsed = JSON.parse(output);
|
|
1388
|
+
if (!Array.isArray(parsed?.comments)) return [];
|
|
1389
|
+
return parsed.comments.map((c) => ({
|
|
1390
|
+
body: c.body ?? "",
|
|
1391
|
+
author: c.author?.login ?? "unknown",
|
|
1392
|
+
createdAt: c.createdAt ?? ""
|
|
1393
|
+
})).filter((c) => c.body.trim().length > 0);
|
|
1394
|
+
} catch {
|
|
1395
|
+
return [];
|
|
1406
1396
|
}
|
|
1407
|
-
return result;
|
|
1408
1397
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1398
|
+
var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
|
|
1399
|
+
function isReviewShaped(body) {
|
|
1400
|
+
return VERDICT_HEADING.test(body);
|
|
1401
|
+
}
|
|
1402
|
+
function getPrLatestReviewBody(prNumber, cwd) {
|
|
1403
|
+
const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
|
|
1404
|
+
const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
|
|
1405
|
+
const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
|
|
1406
|
+
if (all.length > 0) return all[0].body;
|
|
1407
|
+
const pr = getPr(prNumber, cwd);
|
|
1408
|
+
return pr.body;
|
|
1409
|
+
}
|
|
1410
|
+
function postPrReviewComment(prNumber, body, cwd) {
|
|
1411
|
+
try {
|
|
1412
|
+
gh(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
process.stderr.write(
|
|
1415
|
+
`[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
1416
|
+
`
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1414
1419
|
}
|
|
1415
1420
|
|
|
1416
1421
|
// src/profile.ts
|
|
1417
|
-
import * as
|
|
1418
|
-
import * as
|
|
1422
|
+
import * as fs8 from "fs";
|
|
1423
|
+
import * as path7 from "path";
|
|
1419
1424
|
var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
|
|
1420
1425
|
var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
1421
1426
|
var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
|
|
@@ -1431,12 +1436,12 @@ var ProfileError = class extends Error {
|
|
|
1431
1436
|
profilePath;
|
|
1432
1437
|
};
|
|
1433
1438
|
function loadProfile(profilePath) {
|
|
1434
|
-
if (!
|
|
1439
|
+
if (!fs8.existsSync(profilePath)) {
|
|
1435
1440
|
throw new ProfileError(profilePath, "file not found");
|
|
1436
1441
|
}
|
|
1437
1442
|
let raw;
|
|
1438
1443
|
try {
|
|
1439
|
-
raw = JSON.parse(
|
|
1444
|
+
raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
|
|
1440
1445
|
} catch (err) {
|
|
1441
1446
|
throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1442
1447
|
}
|
|
@@ -1475,7 +1480,7 @@ function loadProfile(profilePath) {
|
|
|
1475
1480
|
inputArtifacts: parseInputArtifacts(profilePath, r.input),
|
|
1476
1481
|
outputArtifacts: parseOutputArtifacts(profilePath, r.output),
|
|
1477
1482
|
children,
|
|
1478
|
-
dir:
|
|
1483
|
+
dir: path7.dirname(profilePath)
|
|
1479
1484
|
};
|
|
1480
1485
|
return profile;
|
|
1481
1486
|
}
|
|
@@ -1718,8 +1723,236 @@ function parseScriptList(p, key, raw) {
|
|
|
1718
1723
|
return out;
|
|
1719
1724
|
}
|
|
1720
1725
|
|
|
1726
|
+
// src/lifecycleLabels.ts
|
|
1727
|
+
var KODY_NAMESPACE = "kody";
|
|
1728
|
+
function groupOf(label) {
|
|
1729
|
+
const idx = label.indexOf(":");
|
|
1730
|
+
return idx === -1 ? label : label.slice(0, idx + 1);
|
|
1731
|
+
}
|
|
1732
|
+
function collectProfileLabels() {
|
|
1733
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
1734
|
+
for (const exe of listExecutables()) {
|
|
1735
|
+
let profile;
|
|
1736
|
+
try {
|
|
1737
|
+
profile = loadProfile(exe.profilePath);
|
|
1738
|
+
} catch {
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
|
|
1742
|
+
const spec = extractLabelSpec(entry);
|
|
1743
|
+
if (spec) byLabel.set(spec.label, spec);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return [...byLabel.values()];
|
|
1747
|
+
}
|
|
1748
|
+
function extractLabelSpec(entry) {
|
|
1749
|
+
if (entry.script !== "setLifecycleLabel") return null;
|
|
1750
|
+
const w = entry.with;
|
|
1751
|
+
if (!w) return null;
|
|
1752
|
+
const label = typeof w.label === "string" ? w.label : null;
|
|
1753
|
+
if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
|
|
1754
|
+
return {
|
|
1755
|
+
label,
|
|
1756
|
+
color: typeof w.color === "string" ? w.color : void 0,
|
|
1757
|
+
description: typeof w.description === "string" ? w.description : void 0
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function ensureLabels(cwd) {
|
|
1761
|
+
const result = { created: [], failed: [] };
|
|
1762
|
+
for (const spec of collectProfileLabels()) {
|
|
1763
|
+
try {
|
|
1764
|
+
createLabelInRepo(spec, cwd);
|
|
1765
|
+
result.created.push(spec.label);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
result.failed.push({ label: spec.label, reason: errMsg(err) });
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return result;
|
|
1771
|
+
}
|
|
1772
|
+
function getIssueLabels(issueNumber, cwd) {
|
|
1773
|
+
try {
|
|
1774
|
+
const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
|
|
1775
|
+
return output.split("\n").filter(Boolean);
|
|
1776
|
+
} catch {
|
|
1777
|
+
return [];
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function addLabel(issueNumber, label, cwd) {
|
|
1781
|
+
gh(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
|
|
1782
|
+
}
|
|
1783
|
+
function removeLabel(issueNumber, label, cwd) {
|
|
1784
|
+
try {
|
|
1785
|
+
gh(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
function createLabelInRepo(spec, cwd) {
|
|
1790
|
+
const args = ["label", "create", spec.label, "--force"];
|
|
1791
|
+
if (spec.color) args.push("--color", spec.color);
|
|
1792
|
+
if (spec.description) args.push("--description", spec.description);
|
|
1793
|
+
gh(args, { cwd });
|
|
1794
|
+
}
|
|
1795
|
+
function setKodyLabel(issueNumber, spec, cwd) {
|
|
1796
|
+
const target = spec.label;
|
|
1797
|
+
if (!target.startsWith(KODY_NAMESPACE)) {
|
|
1798
|
+
process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
|
|
1799
|
+
`);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
const targetGroup = groupOf(target);
|
|
1803
|
+
const present = getIssueLabels(issueNumber, cwd);
|
|
1804
|
+
for (const label of present) {
|
|
1805
|
+
if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
|
|
1806
|
+
removeLabel(issueNumber, label, cwd);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
try {
|
|
1810
|
+
addLabel(issueNumber, target, cwd);
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
if (looksLikeMissingLabel(err)) {
|
|
1813
|
+
try {
|
|
1814
|
+
createLabelInRepo(spec, cwd);
|
|
1815
|
+
addLabel(issueNumber, target, cwd);
|
|
1816
|
+
return;
|
|
1817
|
+
} catch (retryErr) {
|
|
1818
|
+
process.stderr.write(
|
|
1819
|
+
`[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
|
|
1820
|
+
`
|
|
1821
|
+
);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
|
|
1826
|
+
`);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function looksLikeMissingLabel(err) {
|
|
1830
|
+
const msg = errMsg(err).toLowerCase();
|
|
1831
|
+
return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
|
|
1832
|
+
}
|
|
1833
|
+
function errMsg(err) {
|
|
1834
|
+
if (err instanceof Error) return err.message;
|
|
1835
|
+
if (typeof err === "object" && err !== null) {
|
|
1836
|
+
const e = err;
|
|
1837
|
+
const stderr = e.stderr?.toString().trim();
|
|
1838
|
+
if (stderr) return stderr;
|
|
1839
|
+
if (e.message) return e.message;
|
|
1840
|
+
}
|
|
1841
|
+
return String(err);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/litellm.ts
|
|
1845
|
+
import { execFileSync as execFileSync4, spawn } from "child_process";
|
|
1846
|
+
import * as fs9 from "fs";
|
|
1847
|
+
import * as os from "os";
|
|
1848
|
+
import * as path8 from "path";
|
|
1849
|
+
async function checkLitellmHealth(url) {
|
|
1850
|
+
try {
|
|
1851
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
1852
|
+
return response.ok;
|
|
1853
|
+
} catch {
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function generateLitellmConfigYaml(model) {
|
|
1858
|
+
const apiKeyVar = providerApiKeyEnvVar(model.provider);
|
|
1859
|
+
return [
|
|
1860
|
+
"model_list:",
|
|
1861
|
+
` - model_name: ${model.model}`,
|
|
1862
|
+
` litellm_params:`,
|
|
1863
|
+
` model: ${model.provider}/${model.model}`,
|
|
1864
|
+
` api_key: os.environ/${apiKeyVar}`,
|
|
1865
|
+
"",
|
|
1866
|
+
"litellm_settings:",
|
|
1867
|
+
" drop_params: true",
|
|
1868
|
+
""
|
|
1869
|
+
].join("\n");
|
|
1870
|
+
}
|
|
1871
|
+
async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
|
|
1872
|
+
if (!needsLitellmProxy(model)) return null;
|
|
1873
|
+
if (await checkLitellmHealth(url)) {
|
|
1874
|
+
return { url, kill: () => {
|
|
1875
|
+
} };
|
|
1876
|
+
}
|
|
1877
|
+
let cmd = "litellm";
|
|
1878
|
+
try {
|
|
1879
|
+
execFileSync4("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
1880
|
+
} catch {
|
|
1881
|
+
try {
|
|
1882
|
+
execFileSync4("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
1883
|
+
cmd = "python3";
|
|
1884
|
+
} catch {
|
|
1885
|
+
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
const configPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
|
|
1889
|
+
fs9.writeFileSync(configPath, generateLitellmConfigYaml(model));
|
|
1890
|
+
const portMatch = url.match(/:(\d+)/);
|
|
1891
|
+
const port = portMatch ? portMatch[1] : "4000";
|
|
1892
|
+
const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
|
|
1893
|
+
const dotenvVars = readDotenvApiKeys(projectDir);
|
|
1894
|
+
const logPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
|
|
1895
|
+
const outFd = fs9.openSync(logPath, "w");
|
|
1896
|
+
const child = spawn(cmd, args, {
|
|
1897
|
+
stdio: ["ignore", outFd, outFd],
|
|
1898
|
+
detached: true,
|
|
1899
|
+
env: stripBlockingEnv({ ...process.env, ...dotenvVars })
|
|
1900
|
+
});
|
|
1901
|
+
fs9.closeSync(outFd);
|
|
1902
|
+
for (let i = 0; i < 30; i++) {
|
|
1903
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1904
|
+
if (await checkLitellmHealth(url)) {
|
|
1905
|
+
return {
|
|
1906
|
+
url,
|
|
1907
|
+
kill: () => {
|
|
1908
|
+
try {
|
|
1909
|
+
child.kill();
|
|
1910
|
+
} catch {
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
let logTail = "";
|
|
1917
|
+
try {
|
|
1918
|
+
logTail = fs9.readFileSync(logPath, "utf-8").slice(-2e3);
|
|
1919
|
+
} catch {
|
|
1920
|
+
}
|
|
1921
|
+
try {
|
|
1922
|
+
child.kill();
|
|
1923
|
+
} catch {
|
|
1924
|
+
}
|
|
1925
|
+
throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
|
|
1926
|
+
${logTail}`);
|
|
1927
|
+
}
|
|
1928
|
+
function readDotenvApiKeys(projectDir) {
|
|
1929
|
+
const dotenvPath = path8.join(projectDir, ".env");
|
|
1930
|
+
if (!fs9.existsSync(dotenvPath)) return {};
|
|
1931
|
+
const result = {};
|
|
1932
|
+
for (const rawLine of fs9.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
1933
|
+
const line = rawLine.trim();
|
|
1934
|
+
if (!line || line.startsWith("#")) continue;
|
|
1935
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
1936
|
+
if (!match) continue;
|
|
1937
|
+
let value = match[2].trim();
|
|
1938
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1939
|
+
value = value.slice(1, -1);
|
|
1940
|
+
}
|
|
1941
|
+
const commentIdx = value.indexOf(" #");
|
|
1942
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
1943
|
+
if (value) result[match[1]] = value;
|
|
1944
|
+
}
|
|
1945
|
+
return result;
|
|
1946
|
+
}
|
|
1947
|
+
function stripBlockingEnv(env) {
|
|
1948
|
+
const out = { ...env };
|
|
1949
|
+
delete out.DATABASE_URL;
|
|
1950
|
+
delete out.AI_BASE_URL;
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1721
1954
|
// src/commit.ts
|
|
1722
|
-
import { execFileSync as
|
|
1955
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
1723
1956
|
import * as fs10 from "fs";
|
|
1724
1957
|
import * as path9 from "path";
|
|
1725
1958
|
var FORBIDDEN_PATH_PREFIXES = [
|
|
@@ -1750,7 +1983,7 @@ var CONVENTIONAL_PREFIXES = [
|
|
|
1750
1983
|
];
|
|
1751
1984
|
function git(args, cwd) {
|
|
1752
1985
|
try {
|
|
1753
|
-
return
|
|
1986
|
+
return execFileSync5("git", args, {
|
|
1754
1987
|
encoding: "utf-8",
|
|
1755
1988
|
timeout: 12e4,
|
|
1756
1989
|
cwd,
|
|
@@ -1809,7 +2042,7 @@ function isForbiddenPath(p) {
|
|
|
1809
2042
|
return false;
|
|
1810
2043
|
}
|
|
1811
2044
|
function listChangedFiles(cwd) {
|
|
1812
|
-
const raw =
|
|
2045
|
+
const raw = execFileSync5("git", ["status", "--porcelain=v1", "-z"], {
|
|
1813
2046
|
encoding: "utf-8",
|
|
1814
2047
|
cwd,
|
|
1815
2048
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
@@ -1821,7 +2054,7 @@ function listChangedFiles(cwd) {
|
|
|
1821
2054
|
}
|
|
1822
2055
|
function listFilesInCommit(ref = "HEAD", cwd) {
|
|
1823
2056
|
try {
|
|
1824
|
-
const raw =
|
|
2057
|
+
const raw = execFileSync5("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
|
|
1825
2058
|
encoding: "utf-8",
|
|
1826
2059
|
cwd,
|
|
1827
2060
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
@@ -1910,14 +2143,14 @@ var abortUnfinishedGitOps2 = async (ctx) => {
|
|
|
1910
2143
|
};
|
|
1911
2144
|
|
|
1912
2145
|
// src/scripts/advanceFlow.ts
|
|
1913
|
-
import { execFileSync as
|
|
2146
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
1914
2147
|
|
|
1915
2148
|
// src/state.ts
|
|
1916
|
-
import { execFileSync as
|
|
2149
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1917
2150
|
var STATE_BEGIN = "<!-- kody:state:v1:begin -->";
|
|
1918
2151
|
var STATE_END = "<!-- kody:state:v1:end -->";
|
|
1919
2152
|
var HISTORY_MAX_ENTRIES = 20;
|
|
1920
|
-
var
|
|
2153
|
+
var API_TIMEOUT_MS2 = 3e4;
|
|
1921
2154
|
function emptyState() {
|
|
1922
2155
|
return {
|
|
1923
2156
|
schemaVersion: 1,
|
|
@@ -1933,15 +2166,15 @@ function emptyState() {
|
|
|
1933
2166
|
history: []
|
|
1934
2167
|
};
|
|
1935
2168
|
}
|
|
1936
|
-
function
|
|
2169
|
+
function ghToken2() {
|
|
1937
2170
|
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
1938
2171
|
}
|
|
1939
|
-
function
|
|
1940
|
-
const token =
|
|
2172
|
+
function gh2(args, input, cwd) {
|
|
2173
|
+
const token = ghToken2();
|
|
1941
2174
|
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
1942
|
-
return
|
|
2175
|
+
return execFileSync6("gh", args, {
|
|
1943
2176
|
encoding: "utf-8",
|
|
1944
|
-
timeout:
|
|
2177
|
+
timeout: API_TIMEOUT_MS2,
|
|
1945
2178
|
cwd,
|
|
1946
2179
|
env,
|
|
1947
2180
|
input,
|
|
@@ -1951,7 +2184,7 @@ function gh(args, input, cwd) {
|
|
|
1951
2184
|
function findStateComment(target, number, cwd) {
|
|
1952
2185
|
const apiPath = target === "issue" ? `repos/{owner}/{repo}/issues/${number}/comments` : `repos/{owner}/{repo}/issues/${number}/comments`;
|
|
1953
2186
|
try {
|
|
1954
|
-
const raw =
|
|
2187
|
+
const raw = gh2(["api", "--paginate", apiPath], void 0, cwd);
|
|
1955
2188
|
const list = JSON.parse(raw);
|
|
1956
2189
|
for (const c of list) {
|
|
1957
2190
|
if (c.body?.includes(STATE_BEGIN)) {
|
|
@@ -2104,10 +2337,10 @@ function writeTaskState(target, number, state, cwd) {
|
|
|
2104
2337
|
const existing = findStateComment(target, number, cwd);
|
|
2105
2338
|
try {
|
|
2106
2339
|
if (existing) {
|
|
2107
|
-
|
|
2340
|
+
gh2(["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], body, cwd);
|
|
2108
2341
|
} else {
|
|
2109
2342
|
const sub = target === "issue" ? "issue" : "pr";
|
|
2110
|
-
|
|
2343
|
+
gh2([sub, "comment", String(number), "--body-file", "-"], body, cwd);
|
|
2111
2344
|
}
|
|
2112
2345
|
} catch (err) {
|
|
2113
2346
|
process.stderr.write(
|
|
@@ -2118,7 +2351,7 @@ function writeTaskState(target, number, state, cwd) {
|
|
|
2118
2351
|
}
|
|
2119
2352
|
|
|
2120
2353
|
// src/scripts/advanceFlow.ts
|
|
2121
|
-
var
|
|
2354
|
+
var API_TIMEOUT_MS3 = 3e4;
|
|
2122
2355
|
var advanceFlow = async (ctx, profile) => {
|
|
2123
2356
|
const state = ctx.data.taskState;
|
|
2124
2357
|
const flow = state?.flow;
|
|
@@ -2142,8 +2375,8 @@ var advanceFlow = async (ctx, profile) => {
|
|
|
2142
2375
|
}
|
|
2143
2376
|
const body = `@kody ${flow.name}`;
|
|
2144
2377
|
try {
|
|
2145
|
-
|
|
2146
|
-
timeout:
|
|
2378
|
+
execFileSync7("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
|
|
2379
|
+
timeout: API_TIMEOUT_MS3,
|
|
2147
2380
|
cwd: ctx.cwd,
|
|
2148
2381
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2149
2382
|
});
|
|
@@ -2251,7 +2484,7 @@ function copyDir(src, dst) {
|
|
|
2251
2484
|
}
|
|
2252
2485
|
|
|
2253
2486
|
// src/coverage.ts
|
|
2254
|
-
import { execFileSync as
|
|
2487
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
2255
2488
|
function patternToRegex(pattern) {
|
|
2256
2489
|
let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
2257
2490
|
s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
|
|
@@ -2269,7 +2502,7 @@ function renderSiblingPath(file, requireSibling) {
|
|
|
2269
2502
|
}
|
|
2270
2503
|
function safeGit(args, cwd) {
|
|
2271
2504
|
try {
|
|
2272
|
-
return
|
|
2505
|
+
return execFileSync8("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
|
|
2273
2506
|
} catch {
|
|
2274
2507
|
return "";
|
|
2275
2508
|
}
|
|
@@ -2632,147 +2865,6 @@ import { execFileSync as execFileSync9 } from "child_process";
|
|
|
2632
2865
|
import * as fs14 from "fs";
|
|
2633
2866
|
import * as path13 from "path";
|
|
2634
2867
|
|
|
2635
|
-
// src/issue.ts
|
|
2636
|
-
import { execFileSync as execFileSync8 } from "child_process";
|
|
2637
|
-
var API_TIMEOUT_MS3 = 3e4;
|
|
2638
|
-
function ghToken2() {
|
|
2639
|
-
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
2640
|
-
}
|
|
2641
|
-
function gh2(args, options) {
|
|
2642
|
-
const token = ghToken2();
|
|
2643
|
-
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
2644
|
-
return execFileSync8("gh", args, {
|
|
2645
|
-
encoding: "utf-8",
|
|
2646
|
-
timeout: API_TIMEOUT_MS3,
|
|
2647
|
-
cwd: options?.cwd,
|
|
2648
|
-
env,
|
|
2649
|
-
input: options?.input,
|
|
2650
|
-
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
2651
|
-
}).trim();
|
|
2652
|
-
}
|
|
2653
|
-
function getIssue(issueNumber, cwd) {
|
|
2654
|
-
const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
|
|
2655
|
-
const parsed = JSON.parse(output);
|
|
2656
|
-
if (typeof parsed?.title !== "string") {
|
|
2657
|
-
throw new Error(`Issue #${issueNumber}: unexpected response shape`);
|
|
2658
|
-
}
|
|
2659
|
-
return {
|
|
2660
|
-
number: parsed.number ?? issueNumber,
|
|
2661
|
-
title: parsed.title,
|
|
2662
|
-
body: parsed.body ?? "",
|
|
2663
|
-
comments: (parsed.comments ?? []).map((c) => ({
|
|
2664
|
-
body: c.body ?? "",
|
|
2665
|
-
author: c.author?.login ?? "unknown",
|
|
2666
|
-
createdAt: c.createdAt ?? ""
|
|
2667
|
-
})),
|
|
2668
|
-
labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
function stripKodyMentions(body) {
|
|
2672
|
-
return body.replace(/(@)(kody)/gi, "$1\u200B$2");
|
|
2673
|
-
}
|
|
2674
|
-
function postIssueComment(issueNumber, body, cwd) {
|
|
2675
|
-
try {
|
|
2676
|
-
gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
|
|
2677
|
-
} catch (err) {
|
|
2678
|
-
process.stderr.write(
|
|
2679
|
-
`[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
2680
|
-
`
|
|
2681
|
-
);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
function truncate2(s, maxBytes) {
|
|
2685
|
-
if (s.length <= maxBytes) return s;
|
|
2686
|
-
return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
|
|
2687
|
-
}
|
|
2688
|
-
function parsePrNumber(url) {
|
|
2689
|
-
const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
|
|
2690
|
-
if (!m) return null;
|
|
2691
|
-
const n = parseInt(m[1], 10);
|
|
2692
|
-
return Number.isFinite(n) ? n : null;
|
|
2693
|
-
}
|
|
2694
|
-
function getPr(prNumber, cwd) {
|
|
2695
|
-
const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
|
|
2696
|
-
cwd
|
|
2697
|
-
});
|
|
2698
|
-
const parsed = JSON.parse(output);
|
|
2699
|
-
if (typeof parsed?.title !== "string") {
|
|
2700
|
-
throw new Error(`PR #${prNumber}: unexpected response shape`);
|
|
2701
|
-
}
|
|
2702
|
-
return {
|
|
2703
|
-
number: parsed.number ?? prNumber,
|
|
2704
|
-
title: parsed.title,
|
|
2705
|
-
body: parsed.body ?? "",
|
|
2706
|
-
headRefName: String(parsed.headRefName ?? ""),
|
|
2707
|
-
baseRefName: String(parsed.baseRefName ?? ""),
|
|
2708
|
-
state: String(parsed.state ?? "")
|
|
2709
|
-
};
|
|
2710
|
-
}
|
|
2711
|
-
function getPrDiff(prNumber, cwd) {
|
|
2712
|
-
try {
|
|
2713
|
-
return gh2(["pr", "diff", String(prNumber)], { cwd });
|
|
2714
|
-
} catch (err) {
|
|
2715
|
-
process.stderr.write(
|
|
2716
|
-
`[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
2717
|
-
`
|
|
2718
|
-
);
|
|
2719
|
-
return "";
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
function getPrReviews(prNumber, cwd) {
|
|
2723
|
-
try {
|
|
2724
|
-
const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
|
|
2725
|
-
const parsed = JSON.parse(output);
|
|
2726
|
-
if (!Array.isArray(parsed?.reviews)) return [];
|
|
2727
|
-
return parsed.reviews.map(
|
|
2728
|
-
(r) => ({
|
|
2729
|
-
body: r.body ?? "",
|
|
2730
|
-
state: r.state ?? "",
|
|
2731
|
-
author: r.author?.login ?? "unknown",
|
|
2732
|
-
submittedAt: r.submittedAt ?? ""
|
|
2733
|
-
})
|
|
2734
|
-
);
|
|
2735
|
-
} catch {
|
|
2736
|
-
return [];
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
function getPrComments(prNumber, cwd) {
|
|
2740
|
-
try {
|
|
2741
|
-
const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
|
|
2742
|
-
const parsed = JSON.parse(output);
|
|
2743
|
-
if (!Array.isArray(parsed?.comments)) return [];
|
|
2744
|
-
return parsed.comments.map((c) => ({
|
|
2745
|
-
body: c.body ?? "",
|
|
2746
|
-
author: c.author?.login ?? "unknown",
|
|
2747
|
-
createdAt: c.createdAt ?? ""
|
|
2748
|
-
})).filter((c) => c.body.trim().length > 0);
|
|
2749
|
-
} catch {
|
|
2750
|
-
return [];
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
|
|
2754
|
-
function isReviewShaped(body) {
|
|
2755
|
-
return VERDICT_HEADING.test(body);
|
|
2756
|
-
}
|
|
2757
|
-
function getPrLatestReviewBody(prNumber, cwd) {
|
|
2758
|
-
const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
|
|
2759
|
-
const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
|
|
2760
|
-
const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
|
|
2761
|
-
if (all.length > 0) return all[0].body;
|
|
2762
|
-
const pr = getPr(prNumber, cwd);
|
|
2763
|
-
return pr.body;
|
|
2764
|
-
}
|
|
2765
|
-
function postPrReviewComment(prNumber, body, cwd) {
|
|
2766
|
-
try {
|
|
2767
|
-
gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
|
|
2768
|
-
} catch (err) {
|
|
2769
|
-
process.stderr.write(
|
|
2770
|
-
`[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
2771
|
-
`
|
|
2772
|
-
);
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
2868
|
// src/scripts/postReviewResult.ts
|
|
2777
2869
|
function detectVerdict(body) {
|
|
2778
2870
|
const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
|
|
@@ -2902,7 +2994,7 @@ function splitReport(text) {
|
|
|
2902
2994
|
function loadManifest(cwd) {
|
|
2903
2995
|
let issuesJson;
|
|
2904
2996
|
try {
|
|
2905
|
-
issuesJson =
|
|
2997
|
+
issuesJson = gh(
|
|
2906
2998
|
["issue", "list", "--label", MANIFEST_LABEL, "--state", "all", "--limit", "1", "--json", "number,body"],
|
|
2907
2999
|
{ cwd }
|
|
2908
3000
|
);
|
|
@@ -2955,7 +3047,7 @@ ${MANIFEST_END}
|
|
|
2955
3047
|
}
|
|
2956
3048
|
function ensureLabel(name, color, description, cwd) {
|
|
2957
3049
|
try {
|
|
2958
|
-
|
|
3050
|
+
gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
|
|
2959
3051
|
} catch {
|
|
2960
3052
|
}
|
|
2961
3053
|
}
|
|
@@ -3010,10 +3102,10 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
|
|
|
3010
3102
|
ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
|
|
3011
3103
|
const body = serializeManifestBody(manifest);
|
|
3012
3104
|
if (number !== null) {
|
|
3013
|
-
|
|
3105
|
+
gh(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
|
|
3014
3106
|
return { number, created: false };
|
|
3015
3107
|
}
|
|
3016
|
-
const out =
|
|
3108
|
+
const out = gh(["issue", "create", "--title", MANIFEST_TITLE, "--label", MANIFEST_LABEL, "--body-file", "-"], {
|
|
3017
3109
|
input: body,
|
|
3018
3110
|
cwd
|
|
3019
3111
|
});
|
|
@@ -3130,7 +3222,7 @@ function createTaskIssue(finding, goalId, manifestNumber, cwd) {
|
|
|
3130
3222
|
for (const l of labels) {
|
|
3131
3223
|
args.push("--label", l);
|
|
3132
3224
|
}
|
|
3133
|
-
const out =
|
|
3225
|
+
const out = gh(args, { input: body, cwd });
|
|
3134
3226
|
const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
|
|
3135
3227
|
const m = url.match(/\/issues\/(\d+)\b/);
|
|
3136
3228
|
if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
|
|
@@ -3187,7 +3279,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
|
|
|
3187
3279
|
const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
|
|
3188
3280
|
let url = "";
|
|
3189
3281
|
try {
|
|
3190
|
-
const out =
|
|
3282
|
+
const out = gh(
|
|
3191
3283
|
["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"],
|
|
3192
3284
|
{ input: finalText, cwd: ctx.cwd }
|
|
3193
3285
|
);
|
|
@@ -4035,7 +4127,7 @@ function parseStateCommentBody(marker, body) {
|
|
|
4035
4127
|
return isStateEnvelope(parsed) ? parsed : null;
|
|
4036
4128
|
}
|
|
4037
4129
|
function listIssueComments(owner, repo, issueNumber, cwd) {
|
|
4038
|
-
const raw =
|
|
4130
|
+
const raw = gh(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
|
|
4039
4131
|
let parsed;
|
|
4040
4132
|
try {
|
|
4041
4133
|
parsed = JSON.parse(raw);
|
|
@@ -4056,7 +4148,7 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
|
|
|
4056
4148
|
}
|
|
4057
4149
|
function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
4058
4150
|
const body = formatStateCommentBody(marker, state);
|
|
4059
|
-
const raw =
|
|
4151
|
+
const raw = gh(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
|
|
4060
4152
|
cwd,
|
|
4061
4153
|
input: JSON.stringify({ body })
|
|
4062
4154
|
});
|
|
@@ -4069,7 +4161,7 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
|
4069
4161
|
}
|
|
4070
4162
|
function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
|
|
4071
4163
|
const body = formatStateCommentBody(marker, state);
|
|
4072
|
-
|
|
4164
|
+
gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
|
|
4073
4165
|
cwd,
|
|
4074
4166
|
input: JSON.stringify({ body })
|
|
4075
4167
|
});
|
|
@@ -4080,7 +4172,7 @@ function updateStateComment(owner, repo, commentId, commentNodeId, marker, state
|
|
|
4080
4172
|
}
|
|
4081
4173
|
function minimizeComment(nodeId, cwd) {
|
|
4082
4174
|
const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
|
|
4083
|
-
|
|
4175
|
+
gh(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
|
|
4084
4176
|
}
|
|
4085
4177
|
|
|
4086
4178
|
// src/scripts/jobState/backend.ts
|
|
@@ -4117,7 +4209,7 @@ var ContentsApiBackend = class {
|
|
|
4117
4209
|
const filePath = stateFilePath(this.jobsDir, slug);
|
|
4118
4210
|
let raw = "";
|
|
4119
4211
|
try {
|
|
4120
|
-
raw =
|
|
4212
|
+
raw = gh(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
|
|
4121
4213
|
} catch (err) {
|
|
4122
4214
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4123
4215
|
if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
|
|
@@ -4161,7 +4253,7 @@ var ContentsApiBackend = class {
|
|
|
4161
4253
|
content: Buffer.from(body, "utf-8").toString("base64")
|
|
4162
4254
|
};
|
|
4163
4255
|
if (typeof loaded.handle === "string") payload.sha = loaded.handle;
|
|
4164
|
-
|
|
4256
|
+
gh(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
|
|
4165
4257
|
cwd: this.cwd,
|
|
4166
4258
|
input: JSON.stringify(payload)
|
|
4167
4259
|
});
|
|
@@ -4507,7 +4599,7 @@ var dispatchJobTicks = async (ctx, _profile, args) => {
|
|
|
4507
4599
|
function listIssuesByLabel(label, cwd) {
|
|
4508
4600
|
let raw = "";
|
|
4509
4601
|
try {
|
|
4510
|
-
raw =
|
|
4602
|
+
raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
|
|
4511
4603
|
cwd
|
|
4512
4604
|
});
|
|
4513
4605
|
} catch {
|
|
@@ -4591,7 +4683,7 @@ function firstLine(s) {
|
|
|
4591
4683
|
}
|
|
4592
4684
|
function findExistingPr(branch, cwd) {
|
|
4593
4685
|
try {
|
|
4594
|
-
const output =
|
|
4686
|
+
const output = gh(
|
|
4595
4687
|
["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,body", "--limit", "1"],
|
|
4596
4688
|
{ cwd }
|
|
4597
4689
|
);
|
|
@@ -4629,7 +4721,7 @@ function ensurePr(opts) {
|
|
|
4629
4721
|
const stripped = existing.url.replace(/^https:\/\/github\.com\//, "");
|
|
4630
4722
|
const [owner, repo] = stripped.split("/");
|
|
4631
4723
|
try {
|
|
4632
|
-
|
|
4724
|
+
gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
|
|
4633
4725
|
cwd: opts.cwd
|
|
4634
4726
|
});
|
|
4635
4727
|
} catch (err) {
|
|
@@ -4651,7 +4743,7 @@ function ensurePr(opts) {
|
|
|
4651
4743
|
"-"
|
|
4652
4744
|
];
|
|
4653
4745
|
if (opts.draft) args.push("--draft");
|
|
4654
|
-
const output =
|
|
4746
|
+
const output = gh(args, { input: body, cwd: opts.cwd });
|
|
4655
4747
|
const url = output.trim();
|
|
4656
4748
|
const match = url.match(/\/pull\/(\d+)$/);
|
|
4657
4749
|
const number = match ? parseInt(match[1], 10) : 0;
|
|
@@ -4738,125 +4830,6 @@ function collectExpectedTests(raw) {
|
|
|
4738
4830
|
|
|
4739
4831
|
// src/scripts/finishFlow.ts
|
|
4740
4832
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
4741
|
-
|
|
4742
|
-
// src/lifecycleLabels.ts
|
|
4743
|
-
var KODY_NAMESPACE = "kody";
|
|
4744
|
-
function groupOf(label) {
|
|
4745
|
-
const idx = label.indexOf(":");
|
|
4746
|
-
return idx === -1 ? label : label.slice(0, idx + 1);
|
|
4747
|
-
}
|
|
4748
|
-
function collectProfileLabels() {
|
|
4749
|
-
const byLabel = /* @__PURE__ */ new Map();
|
|
4750
|
-
for (const exe of listExecutables()) {
|
|
4751
|
-
let profile;
|
|
4752
|
-
try {
|
|
4753
|
-
profile = loadProfile(exe.profilePath);
|
|
4754
|
-
} catch {
|
|
4755
|
-
continue;
|
|
4756
|
-
}
|
|
4757
|
-
for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
|
|
4758
|
-
const spec = extractLabelSpec(entry);
|
|
4759
|
-
if (spec) byLabel.set(spec.label, spec);
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
4762
|
-
return [...byLabel.values()];
|
|
4763
|
-
}
|
|
4764
|
-
function extractLabelSpec(entry) {
|
|
4765
|
-
const w = entry.with;
|
|
4766
|
-
if (!w) return null;
|
|
4767
|
-
const label = typeof w.label === "string" ? w.label : null;
|
|
4768
|
-
if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
|
|
4769
|
-
return {
|
|
4770
|
-
label,
|
|
4771
|
-
color: typeof w.color === "string" ? w.color : void 0,
|
|
4772
|
-
description: typeof w.description === "string" ? w.description : void 0
|
|
4773
|
-
};
|
|
4774
|
-
}
|
|
4775
|
-
function ensureLabels(cwd) {
|
|
4776
|
-
const result = { created: [], failed: [] };
|
|
4777
|
-
for (const spec of collectProfileLabels()) {
|
|
4778
|
-
try {
|
|
4779
|
-
createLabelInRepo(spec, cwd);
|
|
4780
|
-
result.created.push(spec.label);
|
|
4781
|
-
} catch (err) {
|
|
4782
|
-
result.failed.push({ label: spec.label, reason: errMsg(err) });
|
|
4783
|
-
}
|
|
4784
|
-
}
|
|
4785
|
-
return result;
|
|
4786
|
-
}
|
|
4787
|
-
function getIssueLabels(issueNumber, cwd) {
|
|
4788
|
-
try {
|
|
4789
|
-
const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
|
|
4790
|
-
return output.split("\n").filter(Boolean);
|
|
4791
|
-
} catch {
|
|
4792
|
-
return [];
|
|
4793
|
-
}
|
|
4794
|
-
}
|
|
4795
|
-
function addLabel(issueNumber, label, cwd) {
|
|
4796
|
-
gh2(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
|
|
4797
|
-
}
|
|
4798
|
-
function removeLabel(issueNumber, label, cwd) {
|
|
4799
|
-
try {
|
|
4800
|
-
gh2(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
|
|
4801
|
-
} catch {
|
|
4802
|
-
}
|
|
4803
|
-
}
|
|
4804
|
-
function createLabelInRepo(spec, cwd) {
|
|
4805
|
-
const args = ["label", "create", spec.label, "--force"];
|
|
4806
|
-
if (spec.color) args.push("--color", spec.color);
|
|
4807
|
-
if (spec.description) args.push("--description", spec.description);
|
|
4808
|
-
gh2(args, { cwd });
|
|
4809
|
-
}
|
|
4810
|
-
function setKodyLabel(issueNumber, spec, cwd) {
|
|
4811
|
-
const target = spec.label;
|
|
4812
|
-
if (!target.startsWith(KODY_NAMESPACE)) {
|
|
4813
|
-
process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
|
|
4814
|
-
`);
|
|
4815
|
-
return;
|
|
4816
|
-
}
|
|
4817
|
-
const targetGroup = groupOf(target);
|
|
4818
|
-
const present = getIssueLabels(issueNumber, cwd);
|
|
4819
|
-
for (const label of present) {
|
|
4820
|
-
if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
|
|
4821
|
-
removeLabel(issueNumber, label, cwd);
|
|
4822
|
-
}
|
|
4823
|
-
}
|
|
4824
|
-
try {
|
|
4825
|
-
addLabel(issueNumber, target, cwd);
|
|
4826
|
-
} catch (err) {
|
|
4827
|
-
if (looksLikeMissingLabel(err)) {
|
|
4828
|
-
try {
|
|
4829
|
-
createLabelInRepo(spec, cwd);
|
|
4830
|
-
addLabel(issueNumber, target, cwd);
|
|
4831
|
-
return;
|
|
4832
|
-
} catch (retryErr) {
|
|
4833
|
-
process.stderr.write(
|
|
4834
|
-
`[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
|
|
4835
|
-
`
|
|
4836
|
-
);
|
|
4837
|
-
return;
|
|
4838
|
-
}
|
|
4839
|
-
}
|
|
4840
|
-
process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
|
|
4841
|
-
`);
|
|
4842
|
-
}
|
|
4843
|
-
}
|
|
4844
|
-
function looksLikeMissingLabel(err) {
|
|
4845
|
-
const msg = errMsg(err).toLowerCase();
|
|
4846
|
-
return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
|
|
4847
|
-
}
|
|
4848
|
-
function errMsg(err) {
|
|
4849
|
-
if (err instanceof Error) return err.message;
|
|
4850
|
-
if (typeof err === "object" && err !== null) {
|
|
4851
|
-
const e = err;
|
|
4852
|
-
const stderr = e.stderr?.toString().trim();
|
|
4853
|
-
if (stderr) return stderr;
|
|
4854
|
-
if (e.message) return e.message;
|
|
4855
|
-
}
|
|
4856
|
-
return String(err);
|
|
4857
|
-
}
|
|
4858
|
-
|
|
4859
|
-
// src/scripts/finishFlow.ts
|
|
4860
4833
|
var API_TIMEOUT_MS6 = 3e4;
|
|
4861
4834
|
var STATUS_ICON = {
|
|
4862
4835
|
"review-passed": "\u2705",
|
|
@@ -5487,21 +5460,6 @@ function performInit(cwd, force) {
|
|
|
5487
5460
|
wrote.push(QA_GUIDE_REL_PATH);
|
|
5488
5461
|
}
|
|
5489
5462
|
}
|
|
5490
|
-
const builtinJobs = listBuiltinJobs();
|
|
5491
|
-
if (builtinJobs.length > 0) {
|
|
5492
|
-
const jobsDir = path20.join(cwd, ".kody", "jobs");
|
|
5493
|
-
fs22.mkdirSync(jobsDir, { recursive: true });
|
|
5494
|
-
for (const job of builtinJobs) {
|
|
5495
|
-
const rel = path20.join(".kody", "jobs", `${job.slug}.md`);
|
|
5496
|
-
const target = path20.join(cwd, rel);
|
|
5497
|
-
if (fs22.existsSync(target) && !force) {
|
|
5498
|
-
skipped.push(rel);
|
|
5499
|
-
continue;
|
|
5500
|
-
}
|
|
5501
|
-
fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
|
|
5502
|
-
wrote.push(rel);
|
|
5503
|
-
}
|
|
5504
|
-
}
|
|
5505
5463
|
for (const exe of listExecutables()) {
|
|
5506
5464
|
let profile;
|
|
5507
5465
|
try {
|
|
@@ -5871,7 +5829,7 @@ function parsePrNumbers(raw) {
|
|
|
5871
5829
|
}
|
|
5872
5830
|
function fetchPrBlock(prNumber, cwd) {
|
|
5873
5831
|
try {
|
|
5874
|
-
const metaRaw =
|
|
5832
|
+
const metaRaw = gh(["pr", "view", String(prNumber), "--json", "title,state,url,mergedAt,closedAt"], { cwd });
|
|
5875
5833
|
const meta = JSON.parse(metaRaw);
|
|
5876
5834
|
const diff = truncate3(safeGh(["pr", "diff", String(prNumber)], cwd), PER_PR_DIFF_MAX_BYTES);
|
|
5877
5835
|
const commentsRaw = safeGh(["pr", "view", String(prNumber), "--json", "comments,reviews"], cwd);
|
|
@@ -5898,7 +5856,7 @@ _Could not fetch \u2014 ${err instanceof Error ? err.message : String(err)}_`;
|
|
|
5898
5856
|
}
|
|
5899
5857
|
function safeGh(args, cwd) {
|
|
5900
5858
|
try {
|
|
5901
|
-
return
|
|
5859
|
+
return gh(args, { cwd });
|
|
5902
5860
|
} catch {
|
|
5903
5861
|
return "";
|
|
5904
5862
|
}
|
|
@@ -6086,7 +6044,7 @@ function buildIssueTitle(scope, verdict) {
|
|
|
6086
6044
|
}
|
|
6087
6045
|
function ensureLabel2(cwd) {
|
|
6088
6046
|
try {
|
|
6089
|
-
|
|
6047
|
+
gh(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
|
|
6090
6048
|
return true;
|
|
6091
6049
|
} catch {
|
|
6092
6050
|
return false;
|
|
@@ -6095,7 +6053,7 @@ function ensureLabel2(cwd) {
|
|
|
6095
6053
|
function createQaIssue(title, body, hasLabel, cwd) {
|
|
6096
6054
|
const args = ["issue", "create", "--title", title, "--body-file", "-"];
|
|
6097
6055
|
if (hasLabel) args.push("--label", QA_LABEL);
|
|
6098
|
-
const out =
|
|
6056
|
+
const out = gh(args, { input: body, cwd });
|
|
6099
6057
|
const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
|
|
6100
6058
|
const m = url.match(/\/issues\/(\d+)\b/);
|
|
6101
6059
|
if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
|
|
@@ -6869,7 +6827,7 @@ function latestSuccessUrl(deploymentId, cwd) {
|
|
|
6869
6827
|
}
|
|
6870
6828
|
function safeGh2(args, cwd) {
|
|
6871
6829
|
try {
|
|
6872
|
-
return
|
|
6830
|
+
return gh(args, { cwd });
|
|
6873
6831
|
} catch {
|
|
6874
6832
|
return null;
|
|
6875
6833
|
}
|
|
@@ -8251,12 +8209,26 @@ async function runExecutable(profileName, input) {
|
|
|
8251
8209
|
reason: ctx.output.reason
|
|
8252
8210
|
});
|
|
8253
8211
|
} finally {
|
|
8212
|
+
clearStampedLifecycleLabels(profile, ctx);
|
|
8254
8213
|
try {
|
|
8255
8214
|
litellm?.kill();
|
|
8256
8215
|
} catch {
|
|
8257
8216
|
}
|
|
8258
8217
|
}
|
|
8259
8218
|
}
|
|
8219
|
+
function clearStampedLifecycleLabels(profile, ctx) {
|
|
8220
|
+
const target = ctx.args.issue ?? ctx.args.pr;
|
|
8221
|
+
if (typeof target !== "number" || !Number.isFinite(target)) return;
|
|
8222
|
+
for (const entry of profile.scripts.preflight) {
|
|
8223
|
+
if (entry.script !== "setLifecycleLabel") continue;
|
|
8224
|
+
const label = typeof entry.with?.label === "string" ? entry.with.label : void 0;
|
|
8225
|
+
if (!label || !label.startsWith(KODY_NAMESPACE)) continue;
|
|
8226
|
+
try {
|
|
8227
|
+
removeLabel(target, label, ctx.cwd);
|
|
8228
|
+
} catch {
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8260
8232
|
function resolveProfilePath(profileName) {
|
|
8261
8233
|
const found = resolveExecutable(profileName);
|
|
8262
8234
|
if (found) return found;
|
|
@@ -31,7 +31,17 @@
|
|
|
31
31
|
set -euo pipefail
|
|
32
32
|
|
|
33
33
|
goal_id="${KODY_ARG_GOAL:-}"
|
|
34
|
-
|
|
34
|
+
# Default branch: prefer KODY_CFG_GIT_DEFAULTBRANCH (config), then ask the
|
|
35
|
+
# repo via the GitHub API, finally fall back to "main". Past regression: a
|
|
36
|
+
# hardcoded "main" fallback opened goal PRs against `main` for repos whose
|
|
37
|
+
# real default is `dev`, which then needed manual retargeting.
|
|
38
|
+
default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-}"
|
|
39
|
+
if [ -z "$default_branch" ]; then
|
|
40
|
+
default_branch=$(gh api "repos/{owner}/{repo}" --jq .default_branch 2>/dev/null || echo "")
|
|
41
|
+
fi
|
|
42
|
+
if [ -z "$default_branch" ]; then
|
|
43
|
+
default_branch="main"
|
|
44
|
+
fi
|
|
35
45
|
|
|
36
46
|
if [ -z "$goal_id" ]; then
|
|
37
47
|
echo "KODY_REASON=missing --goal"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "watch-stale-prs",
|
|
3
|
+
"role": "watch",
|
|
4
|
+
"describe": "Scheduled: list open PRs untouched for N days and report. No agent invocation.",
|
|
5
|
+
"kind": "scheduled",
|
|
6
|
+
"schedule": "0 8 * * MON",
|
|
7
|
+
"inputs": [],
|
|
8
|
+
"claudeCode": {
|
|
9
|
+
"model": "inherit",
|
|
10
|
+
"permissionMode": "default",
|
|
11
|
+
"maxTurns": null,
|
|
12
|
+
"systemPromptAppend": null,
|
|
13
|
+
"tools": [],
|
|
14
|
+
"hooks": [],
|
|
15
|
+
"skills": [],
|
|
16
|
+
"commands": [],
|
|
17
|
+
"subagents": [],
|
|
18
|
+
"plugins": [],
|
|
19
|
+
"mcpServers": []
|
|
20
|
+
},
|
|
21
|
+
"cliTools": [],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"preflight": [
|
|
24
|
+
{
|
|
25
|
+
"script": "watchStalePrsFlow"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"postflight": []
|
|
29
|
+
}
|
|
30
|
+
}
|
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.24",
|
|
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",
|