@kody-ade/kody-engine 0.4.277 → 0.4.279
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 +768 -437
- package/package.json +24 -23
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.279",
|
|
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",
|
|
@@ -147,23 +147,106 @@ var init_claudeBinary = __esm({
|
|
|
147
147
|
// src/issue.ts
|
|
148
148
|
import { execFileSync } from "child_process";
|
|
149
149
|
function ghToken(preferRepoToken = false) {
|
|
150
|
-
const
|
|
150
|
+
const githubToken2 = process.env.GITHUB_TOKEN?.trim();
|
|
151
151
|
const kodyToken = process.env.KODY_TOKEN?.trim();
|
|
152
152
|
const ghToken3 = process.env.GH_TOKEN?.trim();
|
|
153
153
|
const ghPat = process.env.GH_PAT?.trim();
|
|
154
|
-
return preferRepoToken ?
|
|
154
|
+
return preferRepoToken ? githubToken2 || kodyToken || ghToken3 || ghPat : ghPat || ghToken3 || kodyToken || githubToken2;
|
|
155
155
|
}
|
|
156
156
|
function gh(args, options) {
|
|
157
157
|
const token = ghToken(options?.preferRepoToken);
|
|
158
158
|
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
159
|
+
const maxRetries = envInt("KODY_GH_RATE_LIMIT_MAX_RETRIES", DEFAULT_RATE_LIMIT_RETRIES);
|
|
160
|
+
const maxWaitMs = envInt("KODY_GH_RATE_LIMIT_MAX_WAIT_MS", DEFAULT_RATE_LIMIT_MAX_WAIT_MS);
|
|
161
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
162
|
+
try {
|
|
163
|
+
return execFileSync("gh", args, {
|
|
164
|
+
encoding: "utf-8",
|
|
165
|
+
timeout: API_TIMEOUT_MS,
|
|
166
|
+
cwd: options?.cwd,
|
|
167
|
+
env,
|
|
168
|
+
input: options?.input,
|
|
169
|
+
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
170
|
+
}).trim();
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (!isRateLimitError(err) || attempt >= maxRetries) throw err;
|
|
173
|
+
const waitMs = rateLimitWaitMs(token, options?.cwd, attempt + 1);
|
|
174
|
+
if (waitMs > maxWaitMs) {
|
|
175
|
+
process.stderr.write(
|
|
176
|
+
`[kody gh] rate limit on ${formatGhArgs(args)}; reset wait ${formatDuration(waitMs)} exceeds max ${formatDuration(maxWaitMs)}
|
|
177
|
+
`
|
|
178
|
+
);
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
`[kody gh] rate limit on ${formatGhArgs(args)}; retry ${attempt + 1}/${maxRetries} after ${formatDuration(waitMs)}
|
|
183
|
+
`
|
|
184
|
+
);
|
|
185
|
+
sleepSync(waitMs);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function envInt(name, fallback) {
|
|
190
|
+
const raw = process.env[name]?.trim();
|
|
191
|
+
if (!raw) return fallback;
|
|
192
|
+
const value = Number(raw);
|
|
193
|
+
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
|
|
194
|
+
}
|
|
195
|
+
function errorMessage(err) {
|
|
196
|
+
if (err instanceof Error) {
|
|
197
|
+
const stderr = err.stderr;
|
|
198
|
+
if (Buffer.isBuffer(stderr)) return `${err.message}
|
|
199
|
+
${stderr.toString("utf-8")}`;
|
|
200
|
+
if (typeof stderr === "string") return `${err.message}
|
|
201
|
+
${stderr}`;
|
|
202
|
+
return err.message;
|
|
203
|
+
}
|
|
204
|
+
return String(err);
|
|
205
|
+
}
|
|
206
|
+
function isRateLimitError(err) {
|
|
207
|
+
const msg = errorMessage(err);
|
|
208
|
+
return /rate limit exceeded|secondary rate limit|abuse detection/i.test(msg) && /HTTP 403|rate limit/i.test(msg);
|
|
209
|
+
}
|
|
210
|
+
function rateLimitWaitMs(token, cwd, retryNumber) {
|
|
211
|
+
const resetMs = readCoreRateLimitResetMs(token, cwd);
|
|
212
|
+
if (typeof resetMs === "number") {
|
|
213
|
+
return Math.max(0, resetMs - Date.now() + 1e3);
|
|
214
|
+
}
|
|
215
|
+
const base = envInt("KODY_GH_RATE_LIMIT_BASE_DELAY_MS", DEFAULT_RATE_LIMIT_BASE_DELAY_MS);
|
|
216
|
+
return base * 2 ** Math.max(0, retryNumber - 1);
|
|
217
|
+
}
|
|
218
|
+
function readCoreRateLimitResetMs(token, cwd) {
|
|
219
|
+
try {
|
|
220
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
221
|
+
const raw = execFileSync("gh", ["api", "rate_limit", "--jq", ".resources.core.reset"], {
|
|
222
|
+
encoding: "utf-8",
|
|
223
|
+
timeout: API_TIMEOUT_MS,
|
|
224
|
+
cwd,
|
|
225
|
+
env,
|
|
226
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
227
|
+
}).trim();
|
|
228
|
+
const seconds = Number(raw);
|
|
229
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function sleepSync(ms) {
|
|
235
|
+
if (ms <= 0) return;
|
|
236
|
+
const buffer = new SharedArrayBuffer(4);
|
|
237
|
+
const view = new Int32Array(buffer);
|
|
238
|
+
Atomics.wait(view, 0, 0, ms);
|
|
239
|
+
}
|
|
240
|
+
function formatGhArgs(args) {
|
|
241
|
+
return `gh ${args.slice(0, 4).join(" ")}${args.length > 4 ? " ..." : ""}`;
|
|
242
|
+
}
|
|
243
|
+
function formatDuration(ms) {
|
|
244
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
245
|
+
const seconds = Math.ceil(ms / 1e3);
|
|
246
|
+
if (seconds < 60) return `${seconds}s`;
|
|
247
|
+
const minutes = Math.floor(seconds / 60);
|
|
248
|
+
const rest = seconds % 60;
|
|
249
|
+
return rest ? `${minutes}m${rest}s` : `${minutes}m`;
|
|
167
250
|
}
|
|
168
251
|
function getIssue(issueNumber, cwd) {
|
|
169
252
|
const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels,url"], {
|
|
@@ -333,11 +416,14 @@ function postPrReviewComment(prNumber, body, cwd) {
|
|
|
333
416
|
);
|
|
334
417
|
}
|
|
335
418
|
}
|
|
336
|
-
var API_TIMEOUT_MS, BotDispatchCommentError, DEFAULT_COMMENT_LIMIT, DEFAULT_COMMENT_MAX_BYTES, VERDICT_HEADING;
|
|
419
|
+
var API_TIMEOUT_MS, DEFAULT_RATE_LIMIT_RETRIES, DEFAULT_RATE_LIMIT_BASE_DELAY_MS, DEFAULT_RATE_LIMIT_MAX_WAIT_MS, BotDispatchCommentError, DEFAULT_COMMENT_LIMIT, DEFAULT_COMMENT_MAX_BYTES, VERDICT_HEADING;
|
|
337
420
|
var init_issue = __esm({
|
|
338
421
|
"src/issue.ts"() {
|
|
339
422
|
"use strict";
|
|
340
423
|
API_TIMEOUT_MS = 3e4;
|
|
424
|
+
DEFAULT_RATE_LIMIT_RETRIES = 2;
|
|
425
|
+
DEFAULT_RATE_LIMIT_BASE_DELAY_MS = 1e3;
|
|
426
|
+
DEFAULT_RATE_LIMIT_MAX_WAIT_MS = 65 * 60 * 1e3;
|
|
341
427
|
BotDispatchCommentError = class extends Error {
|
|
342
428
|
constructor(slug2) {
|
|
343
429
|
super(
|
|
@@ -446,8 +532,11 @@ function branchApiPath(config, targetPath) {
|
|
|
446
532
|
}
|
|
447
533
|
function ensureStateBranch(config, cwd) {
|
|
448
534
|
const parsed = parseStateRepo(config);
|
|
535
|
+
const cacheKey3 = `${parsed.owner}/${parsed.repo}:${STATE_BRANCH}`;
|
|
536
|
+
if (ensuredStateBranches.has(cacheKey3)) return;
|
|
449
537
|
try {
|
|
450
538
|
gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
|
|
539
|
+
ensuredStateBranches.add(cacheKey3);
|
|
451
540
|
return;
|
|
452
541
|
} catch (err) {
|
|
453
542
|
if (!is404(err)) throw err;
|
|
@@ -466,6 +555,7 @@ function ensureStateBranch(config, cwd) {
|
|
|
466
555
|
} catch (err) {
|
|
467
556
|
if (!isAlreadyExists(err)) throw err;
|
|
468
557
|
}
|
|
558
|
+
ensuredStateBranches.add(cacheKey3);
|
|
469
559
|
}
|
|
470
560
|
function readStateText(config, cwd, filePath) {
|
|
471
561
|
const targetPath = stateRepoPath(config, filePath);
|
|
@@ -539,11 +629,13 @@ function listStateDirectory(config, cwd, dirPath) {
|
|
|
539
629
|
const parsed = JSON.parse(raw);
|
|
540
630
|
return Array.isArray(parsed) ? parsed : [];
|
|
541
631
|
}
|
|
632
|
+
var ensuredStateBranches;
|
|
542
633
|
var init_stateRepo = __esm({
|
|
543
634
|
"src/stateRepo.ts"() {
|
|
544
635
|
"use strict";
|
|
545
636
|
init_issue();
|
|
546
637
|
init_stateBranch();
|
|
638
|
+
ensuredStateBranches = /* @__PURE__ */ new Set();
|
|
547
639
|
}
|
|
548
640
|
});
|
|
549
641
|
|
|
@@ -1116,7 +1208,7 @@ var init_events = __esm({
|
|
|
1116
1208
|
// src/verify.ts
|
|
1117
1209
|
import { spawn } from "child_process";
|
|
1118
1210
|
function runCommand(command, cwd) {
|
|
1119
|
-
return new Promise((
|
|
1211
|
+
return new Promise((resolve9) => {
|
|
1120
1212
|
const start = Date.now();
|
|
1121
1213
|
const child = spawn(command, {
|
|
1122
1214
|
cwd,
|
|
@@ -1145,11 +1237,11 @@ function runCommand(command, cwd) {
|
|
|
1145
1237
|
child.on("exit", (code) => {
|
|
1146
1238
|
clearTimeout(timer);
|
|
1147
1239
|
const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
|
|
1148
|
-
|
|
1240
|
+
resolve9({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
|
|
1149
1241
|
});
|
|
1150
1242
|
child.on("error", (err) => {
|
|
1151
1243
|
clearTimeout(timer);
|
|
1152
|
-
|
|
1244
|
+
resolve9({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
|
|
1153
1245
|
});
|
|
1154
1246
|
});
|
|
1155
1247
|
}
|
|
@@ -1413,6 +1505,300 @@ var init_submitMcp = __esm({
|
|
|
1413
1505
|
}
|
|
1414
1506
|
});
|
|
1415
1507
|
|
|
1508
|
+
// src/dashboardCmsMcp.ts
|
|
1509
|
+
var dashboardCmsMcp_exports = {};
|
|
1510
|
+
__export(dashboardCmsMcp_exports, {
|
|
1511
|
+
DASHBOARD_CMS_MCP_TOOL_NAMES: () => DASHBOARD_CMS_MCP_TOOL_NAMES,
|
|
1512
|
+
buildDashboardCmsMcpServer: () => buildDashboardCmsMcpServer,
|
|
1513
|
+
dashboardCmsToolDefinitions: () => dashboardCmsToolDefinitions
|
|
1514
|
+
});
|
|
1515
|
+
import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
1516
|
+
import { z as z2 } from "zod";
|
|
1517
|
+
function dashboardBaseUrl(opts) {
|
|
1518
|
+
const raw = opts.dashboardUrl?.trim() || process.env.KODY_CMS_DASHBOARD_URL?.trim() || process.env.KODY_DASHBOARD_URL?.trim() || process.env.DASHBOARD_URL?.trim() || "";
|
|
1519
|
+
if (!raw) return null;
|
|
1520
|
+
try {
|
|
1521
|
+
return new URL(raw).origin;
|
|
1522
|
+
} catch {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function dashboardCmsToken(opts) {
|
|
1527
|
+
return opts.token?.trim() || process.env.KODY_CMS_TOKEN?.trim() || process.env.KODY_DASHBOARD_TOKEN?.trim() || process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || process.env.GH_PAT?.trim() || null;
|
|
1528
|
+
}
|
|
1529
|
+
function cmsHeaders(opts) {
|
|
1530
|
+
const token = dashboardCmsToken(opts);
|
|
1531
|
+
const [owner, repo] = opts.repoSlug.split("/");
|
|
1532
|
+
if (!token) {
|
|
1533
|
+
return {
|
|
1534
|
+
ok: false,
|
|
1535
|
+
error: "missing_cms_token",
|
|
1536
|
+
message: "Set KODY_CMS_TOKEN, KODY_DASHBOARD_TOKEN, KODY_TOKEN, GH_TOKEN, GITHUB_TOKEN, or GH_PAT."
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
if (!owner || !repo) {
|
|
1540
|
+
return { ok: false, error: "invalid_repo", message: `Invalid repo slug: ${opts.repoSlug}` };
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
ok: true,
|
|
1544
|
+
headers: {
|
|
1545
|
+
"Content-Type": "application/json",
|
|
1546
|
+
"x-kody-token": token,
|
|
1547
|
+
"x-kody-owner": owner,
|
|
1548
|
+
"x-kody-repo": repo,
|
|
1549
|
+
...opts.storeRepoUrl?.trim() ? { "x-kody-store-repo-url": opts.storeRepoUrl.trim() } : {},
|
|
1550
|
+
...opts.storeRef?.trim() ? { "x-kody-store-ref": opts.storeRef.trim() } : {}
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
async function callDashboardCms(opts, path50, init = {}) {
|
|
1555
|
+
const baseUrl = dashboardBaseUrl(opts);
|
|
1556
|
+
if (!baseUrl) {
|
|
1557
|
+
return {
|
|
1558
|
+
ok: false,
|
|
1559
|
+
error: "missing_dashboard_url",
|
|
1560
|
+
message: "Set KODY_CMS_DASHBOARD_URL or KODY_DASHBOARD_URL to the Dashboard origin."
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
const headerResult = cmsHeaders(opts);
|
|
1564
|
+
if (!headerResult.ok) return headerResult;
|
|
1565
|
+
try {
|
|
1566
|
+
const res = await fetch(`${baseUrl}${path50}`, {
|
|
1567
|
+
...init,
|
|
1568
|
+
headers: {
|
|
1569
|
+
...headerResult.headers,
|
|
1570
|
+
...init.headers
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
1574
|
+
const data = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => "");
|
|
1575
|
+
if (!res.ok) {
|
|
1576
|
+
return {
|
|
1577
|
+
ok: false,
|
|
1578
|
+
status: res.status,
|
|
1579
|
+
error: stringFieldFromRecord(data, "error") ?? "cms_request_failed",
|
|
1580
|
+
message: stringFieldFromRecord(data, "message") ?? `Dashboard CMS request failed with ${res.status}.`,
|
|
1581
|
+
data
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
return { ok: true, status: res.status, data };
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
return {
|
|
1587
|
+
ok: false,
|
|
1588
|
+
error: "cms_request_error",
|
|
1589
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
function cmsToolResponse(result) {
|
|
1594
|
+
return {
|
|
1595
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1596
|
+
...result.ok ? {} : { isError: true }
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function cmsQuery(args) {
|
|
1600
|
+
const params = new URLSearchParams();
|
|
1601
|
+
const q = stringArg(args.q);
|
|
1602
|
+
if (q) params.set("q", q);
|
|
1603
|
+
const limit = numberArg(args.limit);
|
|
1604
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
1605
|
+
const offset = numberArg(args.offset);
|
|
1606
|
+
if (offset !== void 0) params.set("offset", String(offset));
|
|
1607
|
+
if (args.filters && typeof args.filters === "object" && !Array.isArray(args.filters)) {
|
|
1608
|
+
params.set("filters", JSON.stringify(args.filters));
|
|
1609
|
+
}
|
|
1610
|
+
if (Array.isArray(args.sort)) {
|
|
1611
|
+
const sort = args.sort.flatMap((entry) => {
|
|
1612
|
+
if (!entry || typeof entry !== "object") return [];
|
|
1613
|
+
const field = stringArg(entry.field);
|
|
1614
|
+
if (!field) return [];
|
|
1615
|
+
const direction = entry.direction === "asc" ? "asc" : "desc";
|
|
1616
|
+
return [`${field}:${direction}`];
|
|
1617
|
+
}).join(",");
|
|
1618
|
+
if (sort) params.set("sort", sort);
|
|
1619
|
+
}
|
|
1620
|
+
const value = params.toString();
|
|
1621
|
+
return value ? `?${value}` : "";
|
|
1622
|
+
}
|
|
1623
|
+
function stringArg(value) {
|
|
1624
|
+
return typeof value === "string" ? value.trim() : "";
|
|
1625
|
+
}
|
|
1626
|
+
function numberArg(value) {
|
|
1627
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
1628
|
+
const n = Number(value);
|
|
1629
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1630
|
+
}
|
|
1631
|
+
function documentArg(value) {
|
|
1632
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1633
|
+
return value;
|
|
1634
|
+
}
|
|
1635
|
+
function normalizeCmsDocumentIdInput(input) {
|
|
1636
|
+
const trimmed = stripWrappingQuotes(input.trim());
|
|
1637
|
+
const withoutQuery = trimmed.split(/[?#]/, 1)[0] ?? trimmed;
|
|
1638
|
+
const path50 = parseDocumentPath(withoutQuery);
|
|
1639
|
+
return path50 ?? parseDocumentIdSegment(withoutQuery) ?? withoutQuery;
|
|
1640
|
+
}
|
|
1641
|
+
function stripWrappingQuotes(value) {
|
|
1642
|
+
let current = value;
|
|
1643
|
+
for (; ; ) {
|
|
1644
|
+
const next = current.replace(/^[`'"]+|[`'"]+$/g, "").trim();
|
|
1645
|
+
if (next === current) return current;
|
|
1646
|
+
current = next;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function parseDocumentPath(value) {
|
|
1650
|
+
const path50 = value.startsWith("http://") || value.startsWith("https://") ? urlPathname(value) : value;
|
|
1651
|
+
if (!path50?.includes("/content/entries/")) return null;
|
|
1652
|
+
const parts = path50.split("/").filter(Boolean).map(decodePathPart);
|
|
1653
|
+
const entriesIndex = parts.findIndex((part, index) => part === "content" && parts[index + 1] === "entries");
|
|
1654
|
+
const idPart = parts[entriesIndex + 3];
|
|
1655
|
+
if (!idPart || idPart === "new") return null;
|
|
1656
|
+
return idPart === "edit" ? parts[entriesIndex + 2] ?? null : idPart;
|
|
1657
|
+
}
|
|
1658
|
+
function parseDocumentIdSegment(value) {
|
|
1659
|
+
const parts = value.split("/").filter(Boolean).map(decodePathPart);
|
|
1660
|
+
if (parts.length < 2) return null;
|
|
1661
|
+
const lastPart = parts[parts.length - 1];
|
|
1662
|
+
if (!lastPart || lastPart === "new") return null;
|
|
1663
|
+
return lastPart === "edit" ? parts[parts.length - 2] ?? null : lastPart;
|
|
1664
|
+
}
|
|
1665
|
+
function urlPathname(value) {
|
|
1666
|
+
try {
|
|
1667
|
+
return new URL(value).pathname;
|
|
1668
|
+
} catch {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function decodePathPart(value) {
|
|
1673
|
+
try {
|
|
1674
|
+
return decodeURIComponent(value);
|
|
1675
|
+
} catch {
|
|
1676
|
+
return value;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function stringFieldFromRecord(value, field) {
|
|
1680
|
+
return value && typeof value === "object" && !Array.isArray(value) && typeof value[field] === "string" ? String(value[field]) : void 0;
|
|
1681
|
+
}
|
|
1682
|
+
function dashboardCmsToolDefinitions(opts) {
|
|
1683
|
+
const cmsListCollectionsTool = {
|
|
1684
|
+
name: "cms_list_collections",
|
|
1685
|
+
description: "List configured Dashboard CMS collections and their supported operations. Read-only.",
|
|
1686
|
+
inputSchema: {},
|
|
1687
|
+
handler: async () => cmsToolResponse(await callDashboardCms(opts, "/api/kody/cms"))
|
|
1688
|
+
};
|
|
1689
|
+
const cmsListDocumentsTool = {
|
|
1690
|
+
name: "cms_list_documents",
|
|
1691
|
+
description: "List or search Dashboard CMS documents from one configured collection. Read-only.",
|
|
1692
|
+
inputSchema: {
|
|
1693
|
+
collection: z2.string().min(1).describe("CMS collection name."),
|
|
1694
|
+
q: z2.string().optional().describe("Optional search query."),
|
|
1695
|
+
filters: z2.record(z2.string(), z2.unknown()).optional().describe("Optional filter object keyed by field."),
|
|
1696
|
+
sort: z2.array(
|
|
1697
|
+
z2.object({
|
|
1698
|
+
field: z2.string().min(1),
|
|
1699
|
+
direction: z2.enum(["asc", "desc"]).default("desc")
|
|
1700
|
+
})
|
|
1701
|
+
).optional(),
|
|
1702
|
+
limit: z2.number().int().min(1).max(100).optional(),
|
|
1703
|
+
offset: z2.number().int().min(0).optional()
|
|
1704
|
+
},
|
|
1705
|
+
handler: async (args) => {
|
|
1706
|
+
const collection = encodeURIComponent(stringArg(args.collection));
|
|
1707
|
+
return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}${cmsQuery(args)}`));
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
const cmsGetDocumentTool = {
|
|
1711
|
+
name: "cms_get_document",
|
|
1712
|
+
description: "Get one Dashboard CMS document by collection and id. Use a raw id or the content entry URL copied from the Dashboard.",
|
|
1713
|
+
inputSchema: {
|
|
1714
|
+
collection: z2.string().min(1).describe("CMS collection name."),
|
|
1715
|
+
id: z2.string().min(1).describe("Document id. Use cmsDocumentId from cms_list_documents when available.")
|
|
1716
|
+
},
|
|
1717
|
+
handler: async (args) => {
|
|
1718
|
+
const collection = encodeURIComponent(stringArg(args.collection));
|
|
1719
|
+
const id = encodeURIComponent(normalizeCmsDocumentIdInput(stringArg(args.id)));
|
|
1720
|
+
return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`));
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
const cmsCreateDocumentTool = {
|
|
1724
|
+
name: "cms_create_document",
|
|
1725
|
+
description: "Create one Dashboard CMS document when the caller allows writes and the CMS collection allows create.",
|
|
1726
|
+
inputSchema: {
|
|
1727
|
+
collection: z2.string().min(1).describe("CMS collection name."),
|
|
1728
|
+
data: z2.record(z2.string(), z2.unknown()).describe("Document fields to create.")
|
|
1729
|
+
},
|
|
1730
|
+
handler: async (args) => {
|
|
1731
|
+
const refusal = opts.assertWriteAllowed?.();
|
|
1732
|
+
if (refusal) return { content: [{ type: "text", text: refusal }] };
|
|
1733
|
+
const collection = encodeURIComponent(stringArg(args.collection));
|
|
1734
|
+
return cmsToolResponse(
|
|
1735
|
+
await callDashboardCms(opts, `/api/kody/cms/${collection}`, {
|
|
1736
|
+
method: "POST",
|
|
1737
|
+
body: JSON.stringify(documentArg(args.data))
|
|
1738
|
+
})
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
const cmsUpdateDocumentTool = {
|
|
1743
|
+
name: "cms_update_document",
|
|
1744
|
+
description: "Update one Dashboard CMS document when the caller allows writes and the CMS collection allows update.",
|
|
1745
|
+
inputSchema: {
|
|
1746
|
+
collection: z2.string().min(1).describe("CMS collection name."),
|
|
1747
|
+
id: z2.string().min(1).describe("Document id."),
|
|
1748
|
+
data: z2.record(z2.string(), z2.unknown()).describe("Partial document fields to update.")
|
|
1749
|
+
},
|
|
1750
|
+
handler: async (args) => {
|
|
1751
|
+
const refusal = opts.assertWriteAllowed?.();
|
|
1752
|
+
if (refusal) return { content: [{ type: "text", text: refusal }] };
|
|
1753
|
+
const collection = encodeURIComponent(stringArg(args.collection));
|
|
1754
|
+
const id = encodeURIComponent(normalizeCmsDocumentIdInput(stringArg(args.id)));
|
|
1755
|
+
return cmsToolResponse(
|
|
1756
|
+
await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`, {
|
|
1757
|
+
method: "PATCH",
|
|
1758
|
+
body: JSON.stringify(documentArg(args.data))
|
|
1759
|
+
})
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
return [
|
|
1764
|
+
cmsListCollectionsTool,
|
|
1765
|
+
cmsListDocumentsTool,
|
|
1766
|
+
cmsGetDocumentTool,
|
|
1767
|
+
cmsCreateDocumentTool,
|
|
1768
|
+
cmsUpdateDocumentTool
|
|
1769
|
+
];
|
|
1770
|
+
}
|
|
1771
|
+
function buildDashboardCmsMcpServer(opts) {
|
|
1772
|
+
const tools = dashboardCmsToolDefinitions(opts).map(
|
|
1773
|
+
(def) => tool3(
|
|
1774
|
+
def.name,
|
|
1775
|
+
def.description,
|
|
1776
|
+
def.inputSchema,
|
|
1777
|
+
async (args) => def.handler(args)
|
|
1778
|
+
)
|
|
1779
|
+
);
|
|
1780
|
+
return {
|
|
1781
|
+
server: createSdkMcpServer3({
|
|
1782
|
+
name: "kody-cms",
|
|
1783
|
+
version: "0.1.0",
|
|
1784
|
+
tools
|
|
1785
|
+
})
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
var DASHBOARD_CMS_MCP_TOOL_NAMES;
|
|
1789
|
+
var init_dashboardCmsMcp = __esm({
|
|
1790
|
+
"src/dashboardCmsMcp.ts"() {
|
|
1791
|
+
"use strict";
|
|
1792
|
+
DASHBOARD_CMS_MCP_TOOL_NAMES = [
|
|
1793
|
+
"cms_list_collections",
|
|
1794
|
+
"cms_list_documents",
|
|
1795
|
+
"cms_get_document",
|
|
1796
|
+
"cms_create_document",
|
|
1797
|
+
"cms_update_document"
|
|
1798
|
+
];
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
|
|
1416
1802
|
// src/capabilityFolders.ts
|
|
1417
1803
|
import * as fs4 from "fs";
|
|
1418
1804
|
import * as path6 from "path";
|
|
@@ -1983,8 +2369,8 @@ __export(capabilityMcp_exports, {
|
|
|
1983
2369
|
readCheckRuns: () => readCheckRuns,
|
|
1984
2370
|
readThread: () => readThread
|
|
1985
2371
|
});
|
|
1986
|
-
import { createSdkMcpServer as
|
|
1987
|
-
import { z as
|
|
2372
|
+
import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
|
|
2373
|
+
import { z as z3 } from "zod";
|
|
1988
2374
|
function summarizeCiStatus(rollup) {
|
|
1989
2375
|
if (!Array.isArray(rollup) || rollup.length === 0) return "UNKNOWN";
|
|
1990
2376
|
let hasRunning = false;
|
|
@@ -2168,141 +2554,60 @@ ${marker}` });
|
|
|
2168
2554
|
} catch (err) {
|
|
2169
2555
|
return { error: err instanceof Error ? err.message : String(err) };
|
|
2170
2556
|
}
|
|
2171
|
-
}
|
|
2172
|
-
function dispatchWorkflow(workflowFile, capability, issueNumber, repoSlug) {
|
|
2173
|
-
const expected = expectedDispatchTarget(capability);
|
|
2174
|
-
if (repoSlug && expected) {
|
|
2175
|
-
const target = readDispatchTargetKind(repoSlug, issueNumber);
|
|
2176
|
-
if (!target.ok) return target;
|
|
2177
|
-
if (expected === "issue" && target.kind === "pr") {
|
|
2178
|
-
return {
|
|
2179
|
-
ok: false,
|
|
2180
|
-
error: `refusing to dispatch ${capability} on PR #${issueNumber}; dispatch the source issue or use a PR action`
|
|
2181
|
-
};
|
|
2182
|
-
}
|
|
2183
|
-
if (expected === "pr" && target.kind === "issue") {
|
|
2184
|
-
return {
|
|
2185
|
-
ok: false,
|
|
2186
|
-
error: `refusing to dispatch ${capability} on issue #${issueNumber}; expected a PR target`
|
|
2187
|
-
};
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
try {
|
|
2191
|
-
gh(["workflow", "run", workflowFile, "-f", `capability=${capability}`, "-f", `issue_number=${issueNumber}`]);
|
|
2192
|
-
return { ok: true };
|
|
2193
|
-
} catch (err) {
|
|
2194
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
function expectedDispatchTarget(capability) {
|
|
2198
|
-
const route = resolveCapabilityAction(capability);
|
|
2199
|
-
if (!route) return null;
|
|
2200
|
-
const inputs = getProfileInputs(route.executable);
|
|
2201
|
-
const numeric = inputs?.find((input) => input.type === "int" && input.required);
|
|
2202
|
-
if (numeric?.name === "issue") return "issue";
|
|
2203
|
-
if (numeric?.name === "pr") return "pr";
|
|
2204
|
-
return null;
|
|
2205
|
-
}
|
|
2206
|
-
function readDispatchTargetKind(repoSlug, issueNumber) {
|
|
2207
|
-
try {
|
|
2208
|
-
const raw = gh(["api", `repos/${repoSlug}/issues/${issueNumber}`]);
|
|
2209
|
-
const parsed = JSON.parse(raw);
|
|
2210
|
-
return { ok: true, kind: parsed.pull_request ? "pr" : "issue" };
|
|
2211
|
-
} catch (err) {
|
|
2212
|
-
return {
|
|
2213
|
-
ok: false,
|
|
2214
|
-
error: `could not verify target #${issueNumber}: ${err instanceof Error ? err.message : String(err)}`
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
function isDispatchGated(capability, mode) {
|
|
2219
|
-
if (mode === "auto") return false;
|
|
2220
|
-
if (capability && GATE_EXEMPT_DUTIES.has(capability)) return false;
|
|
2221
|
-
return true;
|
|
2222
|
-
}
|
|
2223
|
-
function trustRefusal(capabilitySlug) {
|
|
2224
|
-
return `Not dispatched: capability \`${capabilitySlug ?? "?"}\` is in ASK mode (not trusted for autonomy). Do NOT retry the dispatch. Instead notify the operator (use recommend_to_operator, or rely on the tracking issue that already @-mentions them), then submit_state. To let this capability act on its own, grant it Auto on the dashboard Trust page.`;
|
|
2225
|
-
}
|
|
2226
|
-
function dashboardBaseUrl() {
|
|
2227
|
-
const raw = process.env.KODY_CMS_DASHBOARD_URL?.trim() || process.env.KODY_DASHBOARD_URL?.trim() || process.env.DASHBOARD_URL?.trim() || "";
|
|
2228
|
-
if (!raw) return null;
|
|
2229
|
-
try {
|
|
2230
|
-
const parsed = new URL(raw);
|
|
2231
|
-
return parsed.origin;
|
|
2232
|
-
} catch {
|
|
2233
|
-
return null;
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
function dashboardCmsToken() {
|
|
2237
|
-
return process.env.KODY_CMS_TOKEN?.trim() || process.env.KODY_DASHBOARD_TOKEN?.trim() || process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || process.env.GH_PAT?.trim() || null;
|
|
2238
|
-
}
|
|
2239
|
-
function cmsHeaders(opts) {
|
|
2240
|
-
const token = dashboardCmsToken();
|
|
2241
|
-
const [owner, repo] = opts.repoSlug.split("/");
|
|
2242
|
-
if (!token) {
|
|
2243
|
-
return {
|
|
2244
|
-
ok: false,
|
|
2245
|
-
error: "missing_cms_token",
|
|
2246
|
-
message: "Set KODY_CMS_TOKEN, KODY_DASHBOARD_TOKEN, KODY_TOKEN, GH_TOKEN, GITHUB_TOKEN, or GH_PAT."
|
|
2247
|
-
};
|
|
2248
|
-
}
|
|
2249
|
-
if (!owner || !repo) {
|
|
2250
|
-
return { ok: false, error: "invalid_repo", message: `Invalid repo slug: ${opts.repoSlug}` };
|
|
2251
|
-
}
|
|
2252
|
-
return {
|
|
2253
|
-
ok: true,
|
|
2254
|
-
headers: {
|
|
2255
|
-
"Content-Type": "application/json",
|
|
2256
|
-
"x-kody-token": token,
|
|
2257
|
-
"x-kody-owner": owner,
|
|
2258
|
-
"x-kody-repo": repo
|
|
2259
|
-
}
|
|
2260
|
-
};
|
|
2261
|
-
}
|
|
2262
|
-
async function callDashboardCms(opts, path50, init = {}) {
|
|
2263
|
-
const baseUrl = dashboardBaseUrl();
|
|
2264
|
-
if (!baseUrl) {
|
|
2265
|
-
return {
|
|
2266
|
-
ok: false,
|
|
2267
|
-
error: "missing_dashboard_url",
|
|
2268
|
-
message: "Set KODY_CMS_DASHBOARD_URL or KODY_DASHBOARD_URL to the Dashboard origin."
|
|
2269
|
-
};
|
|
2270
|
-
}
|
|
2271
|
-
const headerResult = cmsHeaders(opts);
|
|
2272
|
-
if (!headerResult.ok) return headerResult;
|
|
2273
|
-
try {
|
|
2274
|
-
const res = await fetch(`${baseUrl}${path50}`, {
|
|
2275
|
-
...init,
|
|
2276
|
-
headers: {
|
|
2277
|
-
...headerResult.headers,
|
|
2278
|
-
...init.headers
|
|
2279
|
-
}
|
|
2280
|
-
});
|
|
2281
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
2282
|
-
const data = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => "");
|
|
2283
|
-
if (!res.ok) {
|
|
2557
|
+
}
|
|
2558
|
+
function dispatchWorkflow(workflowFile, capability, issueNumber, repoSlug) {
|
|
2559
|
+
const expected = expectedDispatchTarget(capability);
|
|
2560
|
+
if (repoSlug && expected) {
|
|
2561
|
+
const target = readDispatchTargetKind(repoSlug, issueNumber);
|
|
2562
|
+
if (!target.ok) return target;
|
|
2563
|
+
if (expected === "issue" && target.kind === "pr") {
|
|
2284
2564
|
return {
|
|
2285
2565
|
ok: false,
|
|
2286
|
-
|
|
2287
|
-
error: stringFieldFromRecord(data, "error") ?? "cms_request_failed",
|
|
2288
|
-
message: stringFieldFromRecord(data, "message") ?? `Dashboard CMS request failed with ${res.status}.`,
|
|
2289
|
-
data
|
|
2566
|
+
error: `refusing to dispatch ${capability} on PR #${issueNumber}; dispatch the source issue or use a PR action`
|
|
2290
2567
|
};
|
|
2291
2568
|
}
|
|
2292
|
-
|
|
2569
|
+
if (expected === "pr" && target.kind === "issue") {
|
|
2570
|
+
return {
|
|
2571
|
+
ok: false,
|
|
2572
|
+
error: `refusing to dispatch ${capability} on issue #${issueNumber}; expected a PR target`
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
try {
|
|
2577
|
+
gh(["workflow", "run", workflowFile, "-f", `capability=${capability}`, "-f", `issue_number=${issueNumber}`]);
|
|
2578
|
+
return { ok: true };
|
|
2579
|
+
} catch (err) {
|
|
2580
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
function expectedDispatchTarget(capability) {
|
|
2584
|
+
const route = resolveCapabilityAction(capability);
|
|
2585
|
+
if (!route) return null;
|
|
2586
|
+
const inputs = getProfileInputs(route.executable);
|
|
2587
|
+
const numeric = inputs?.find((input) => input.type === "int" && input.required);
|
|
2588
|
+
if (numeric?.name === "issue") return "issue";
|
|
2589
|
+
if (numeric?.name === "pr") return "pr";
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
function readDispatchTargetKind(repoSlug, issueNumber) {
|
|
2593
|
+
try {
|
|
2594
|
+
const raw = gh(["api", `repos/${repoSlug}/issues/${issueNumber}`]);
|
|
2595
|
+
const parsed = JSON.parse(raw);
|
|
2596
|
+
return { ok: true, kind: parsed.pull_request ? "pr" : "issue" };
|
|
2293
2597
|
} catch (err) {
|
|
2294
2598
|
return {
|
|
2295
2599
|
ok: false,
|
|
2296
|
-
error:
|
|
2297
|
-
message: err instanceof Error ? err.message : String(err)
|
|
2600
|
+
error: `could not verify target #${issueNumber}: ${err instanceof Error ? err.message : String(err)}`
|
|
2298
2601
|
};
|
|
2299
2602
|
}
|
|
2300
2603
|
}
|
|
2301
|
-
function
|
|
2302
|
-
return
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2604
|
+
function isDispatchGated(capability, mode) {
|
|
2605
|
+
if (mode === "auto") return false;
|
|
2606
|
+
if (capability && GATE_EXEMPT_DUTIES.has(capability)) return false;
|
|
2607
|
+
return true;
|
|
2608
|
+
}
|
|
2609
|
+
function trustRefusal(capabilitySlug) {
|
|
2610
|
+
return `Not dispatched: capability \`${capabilitySlug ?? "?"}\` is in ASK mode (not trusted for autonomy). Do NOT retry the dispatch. Instead notify the operator (use recommend_to_operator, or rely on the tracking issue that already @-mentions them), then submit_state. To let this capability act on its own, grant it Auto on the dashboard Trust page.`;
|
|
2306
2611
|
}
|
|
2307
2612
|
function assertCmsWriteAllowed(opts) {
|
|
2308
2613
|
if (isDispatchGated(opts.capabilitySlug, readCapabilityTrustMode(opts.state, opts.repoSlug, opts.capabilitySlug))) {
|
|
@@ -2310,45 +2615,6 @@ function assertCmsWriteAllowed(opts) {
|
|
|
2310
2615
|
}
|
|
2311
2616
|
return null;
|
|
2312
2617
|
}
|
|
2313
|
-
function cmsQuery(args) {
|
|
2314
|
-
const params = new URLSearchParams();
|
|
2315
|
-
const q = stringArg(args.q);
|
|
2316
|
-
if (q) params.set("q", q);
|
|
2317
|
-
const limit = numberArg(args.limit);
|
|
2318
|
-
if (limit !== void 0) params.set("limit", String(limit));
|
|
2319
|
-
const offset = numberArg(args.offset);
|
|
2320
|
-
if (offset !== void 0) params.set("offset", String(offset));
|
|
2321
|
-
if (args.filters && typeof args.filters === "object" && !Array.isArray(args.filters)) {
|
|
2322
|
-
params.set("filters", JSON.stringify(args.filters));
|
|
2323
|
-
}
|
|
2324
|
-
if (Array.isArray(args.sort)) {
|
|
2325
|
-
const sort = args.sort.flatMap((entry) => {
|
|
2326
|
-
if (!entry || typeof entry !== "object") return [];
|
|
2327
|
-
const field = stringArg(entry.field);
|
|
2328
|
-
if (!field) return [];
|
|
2329
|
-
const direction = entry.direction === "asc" ? "asc" : "desc";
|
|
2330
|
-
return [`${field}:${direction}`];
|
|
2331
|
-
}).join(",");
|
|
2332
|
-
if (sort) params.set("sort", sort);
|
|
2333
|
-
}
|
|
2334
|
-
const value = params.toString();
|
|
2335
|
-
return value ? `?${value}` : "";
|
|
2336
|
-
}
|
|
2337
|
-
function stringArg(value) {
|
|
2338
|
-
return typeof value === "string" ? value.trim() : "";
|
|
2339
|
-
}
|
|
2340
|
-
function numberArg(value) {
|
|
2341
|
-
if (value === void 0 || value === null || value === "") return void 0;
|
|
2342
|
-
const n = Number(value);
|
|
2343
|
-
return Number.isFinite(n) ? n : void 0;
|
|
2344
|
-
}
|
|
2345
|
-
function documentArg(value) {
|
|
2346
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2347
|
-
return value;
|
|
2348
|
-
}
|
|
2349
|
-
function stringFieldFromRecord(value, field) {
|
|
2350
|
-
return value && typeof value === "object" && !Array.isArray(value) && typeof value[field] === "string" ? String(value[field]) : void 0;
|
|
2351
|
-
}
|
|
2352
2618
|
function capabilityToolDefinitions(opts) {
|
|
2353
2619
|
const workflowFile = opts.workflowFile ?? "kody.yml";
|
|
2354
2620
|
const listTool = {
|
|
@@ -2371,7 +2637,7 @@ function capabilityToolDefinitions(opts) {
|
|
|
2371
2637
|
name: `${verb.replace("-", "_")}_pr`,
|
|
2372
2638
|
description: describe,
|
|
2373
2639
|
inputSchema: {
|
|
2374
|
-
pr:
|
|
2640
|
+
pr: z3.number().int().positive().describe("PR number to repair.")
|
|
2375
2641
|
},
|
|
2376
2642
|
handler: async (args) => {
|
|
2377
2643
|
const pr = Number(args.pr);
|
|
@@ -2399,8 +2665,8 @@ function capabilityToolDefinitions(opts) {
|
|
|
2399
2665
|
name: "recommend_to_operator",
|
|
2400
2666
|
description: "Post ONE comment on a PR with the operator @-mention prepended. Use this when a capability is in ASK mode and you want the operator to confirm via the dashboard inbox. The mention handle is substituted from kody.config.json `github.operators` \u2014 do not type it yourself.",
|
|
2401
2667
|
inputSchema: {
|
|
2402
|
-
pr:
|
|
2403
|
-
body:
|
|
2668
|
+
pr: z3.number().int().positive().describe("PR number to comment on."),
|
|
2669
|
+
body: z3.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
|
|
2404
2670
|
},
|
|
2405
2671
|
handler: async (args) => {
|
|
2406
2672
|
const pr = Number(args.pr);
|
|
@@ -2414,7 +2680,7 @@ function capabilityToolDefinitions(opts) {
|
|
|
2414
2680
|
name: "read_ledger",
|
|
2415
2681
|
description: "Read any sentinel-fenced JSON manifest stored on a labeled issue. Returns `{found, issueNumber, payload}` where payload is the parsed JSON between `<!-- <label>:start -->` and `<!-- <label>:end -->` sentinels.",
|
|
2416
2682
|
inputSchema: {
|
|
2417
|
-
label:
|
|
2683
|
+
label: z3.string().min(1).describe("GitHub issue label that identifies the manifest issue.")
|
|
2418
2684
|
},
|
|
2419
2685
|
handler: async (args) => {
|
|
2420
2686
|
const label = String(args.label ?? "");
|
|
@@ -2426,8 +2692,8 @@ function capabilityToolDefinitions(opts) {
|
|
|
2426
2692
|
name: "read_check_runs",
|
|
2427
2693
|
description: "Read CI for a branch or commit ref (e.g. 'dev'). Returns {sha, state, failing:[{name,conclusion,detailsUrl}], pending:[{name,status}]}. state is RED (\u22651 check has a terminal-failure conclusion: failure/timed_out/startup_failure/action_required), PENDING (none failed but some still running), or GREEN (all completed, none failed). Kody's own job check-runs (run/kody/capability-tick/\u2026) are excluded by default. This reads the commit's authoritative check-runs \u2014 use it instead of guessing CI health from a run list.",
|
|
2428
2694
|
inputSchema: {
|
|
2429
|
-
ref:
|
|
2430
|
-
ignoreNames:
|
|
2695
|
+
ref: z3.string().min(1).describe("Branch name or commit SHA to read CI for (e.g. 'dev')."),
|
|
2696
|
+
ignoreNames: z3.array(z3.string()).optional().describe("Check names to exclude (default: Kody's own job names).")
|
|
2431
2697
|
},
|
|
2432
2698
|
handler: async (args) => {
|
|
2433
2699
|
const ref = String(args.ref ?? "");
|
|
@@ -2440,8 +2706,8 @@ function capabilityToolDefinitions(opts) {
|
|
|
2440
2706
|
name: "read_thread",
|
|
2441
2707
|
description: "Read an issue or PR's recent comments + labels + title/state. Returns {number, title, state, labels:[...], comments:[{author, createdAt, body}]} (newest last, body truncated). Use this to read a verdict a dispatched check posted back \u2014 e.g. qa-engineer's report or ui-review's PASS/CONCERNS/FAIL \u2014 on a later tick. Read-only; works for both issues AND PRs.",
|
|
2442
2708
|
inputSchema: {
|
|
2443
|
-
number:
|
|
2444
|
-
limit:
|
|
2709
|
+
number: z3.number().int().positive().describe("Issue or PR number to read."),
|
|
2710
|
+
limit: z3.number().int().positive().optional().describe("Max recent comments to return (default 10).")
|
|
2445
2711
|
},
|
|
2446
2712
|
handler: async (args) => {
|
|
2447
2713
|
const number = Number(args.number);
|
|
@@ -2454,11 +2720,11 @@ function capabilityToolDefinitions(opts) {
|
|
|
2454
2720
|
name: "ensure_issue",
|
|
2455
2721
|
description: "Idempotently ensure ONE open tracking issue exists for `key`. Searches OPEN issues (issues API, not the laggy search index) for `key`'s hidden marker; if found, returns {created:false, number} and creates NOTHING; otherwise creates the issue (title + body, marker appended) and returns {created:true, number}. This is the anti-duplication primitive: use one stable `key` per recurring finding so re-ticks reuse the same issue. Only take follow-up actions (dispatch/comment) when created===true.",
|
|
2456
2722
|
inputSchema: {
|
|
2457
|
-
key:
|
|
2723
|
+
key: z3.string().min(1).describe(
|
|
2458
2724
|
"Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."
|
|
2459
2725
|
),
|
|
2460
|
-
title:
|
|
2461
|
-
body:
|
|
2726
|
+
title: z3.string().min(1).describe("Issue title (used only on first creation)."),
|
|
2727
|
+
body: z3.string().min(1).describe(
|
|
2462
2728
|
"Issue body markdown (used only on first creation). Include the operator mention verbatim if the capability body has one."
|
|
2463
2729
|
)
|
|
2464
2730
|
},
|
|
@@ -2474,9 +2740,9 @@ function capabilityToolDefinitions(opts) {
|
|
|
2474
2740
|
name: "ensure_comment",
|
|
2475
2741
|
description: "Idempotently post ONE comment on an issue for `key`. If a comment carrying `key`'s marker already exists on the issue, returns {posted:false}; otherwise posts the comment (marker appended) and returns {posted:true}. Use for a notify/audit comment that must appear exactly once.",
|
|
2476
2742
|
inputSchema: {
|
|
2477
|
-
issue:
|
|
2478
|
-
key:
|
|
2479
|
-
body:
|
|
2743
|
+
issue: z3.number().int().positive().describe("Issue number to comment on."),
|
|
2744
|
+
key: z3.string().min(1).describe("Stable dedup identity for this comment (e.g. 'dev-ci-red:dispatched')."),
|
|
2745
|
+
body: z3.string().min(1).describe("Comment body markdown.")
|
|
2480
2746
|
},
|
|
2481
2747
|
handler: async (args) => {
|
|
2482
2748
|
const issue = Number(args.issue);
|
|
@@ -2490,9 +2756,9 @@ function capabilityToolDefinitions(opts) {
|
|
|
2490
2756
|
name: "dispatch_workflow",
|
|
2491
2757
|
description: "Dispatch a kody.yml workflow_dispatch run for a capability action against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({capability:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
|
|
2492
2758
|
inputSchema: {
|
|
2493
|
-
capability:
|
|
2494
|
-
executable:
|
|
2495
|
-
issueNumber:
|
|
2759
|
+
capability: z3.string().min(1).optional().describe("Capability action to run (e.g. 'run')."),
|
|
2760
|
+
executable: z3.string().min(1).optional().describe("Deprecated alias for capability."),
|
|
2761
|
+
issueNumber: z3.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
|
|
2496
2762
|
},
|
|
2497
2763
|
handler: async (args) => {
|
|
2498
2764
|
const capability = String(args.capability ?? args.executable ?? "");
|
|
@@ -2505,86 +2771,10 @@ function capabilityToolDefinitions(opts) {
|
|
|
2505
2771
|
return { content: [{ type: "text", text }] };
|
|
2506
2772
|
}
|
|
2507
2773
|
};
|
|
2508
|
-
const
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
handler: async () => cmsToolResponse(await callDashboardCms(opts, "/api/kody/cms"))
|
|
2513
|
-
};
|
|
2514
|
-
const cmsListDocumentsTool = {
|
|
2515
|
-
name: "cms_list_documents",
|
|
2516
|
-
description: "List or search Dashboard CMS documents from one configured collection. Read-only.",
|
|
2517
|
-
inputSchema: {
|
|
2518
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2519
|
-
q: z2.string().optional().describe("Optional search query."),
|
|
2520
|
-
filters: z2.record(z2.string(), z2.unknown()).optional().describe("Optional filter object keyed by field."),
|
|
2521
|
-
sort: z2.array(
|
|
2522
|
-
z2.object({
|
|
2523
|
-
field: z2.string().min(1),
|
|
2524
|
-
direction: z2.enum(["asc", "desc"]).default("desc")
|
|
2525
|
-
})
|
|
2526
|
-
).optional(),
|
|
2527
|
-
limit: z2.number().int().min(1).max(100).optional(),
|
|
2528
|
-
offset: z2.number().int().min(0).optional()
|
|
2529
|
-
},
|
|
2530
|
-
handler: async (args) => {
|
|
2531
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2532
|
-
return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}${cmsQuery(args)}`));
|
|
2533
|
-
}
|
|
2534
|
-
};
|
|
2535
|
-
const cmsGetDocumentTool = {
|
|
2536
|
-
name: "cms_get_document",
|
|
2537
|
-
description: "Get one Dashboard CMS document by collection and id. Read-only.",
|
|
2538
|
-
inputSchema: {
|
|
2539
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2540
|
-
id: z2.string().min(1).describe("Document id.")
|
|
2541
|
-
},
|
|
2542
|
-
handler: async (args) => {
|
|
2543
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2544
|
-
const id = encodeURIComponent(stringArg(args.id));
|
|
2545
|
-
return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`));
|
|
2546
|
-
}
|
|
2547
|
-
};
|
|
2548
|
-
const cmsCreateDocumentTool = {
|
|
2549
|
-
name: "cms_create_document",
|
|
2550
|
-
description: "Create one Dashboard CMS document when the capability is trusted and the CMS collection allows create.",
|
|
2551
|
-
inputSchema: {
|
|
2552
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2553
|
-
data: z2.record(z2.string(), z2.unknown()).describe("Document fields to create.")
|
|
2554
|
-
},
|
|
2555
|
-
handler: async (args) => {
|
|
2556
|
-
const refusal = assertCmsWriteAllowed(opts);
|
|
2557
|
-
if (refusal) return { content: [{ type: "text", text: refusal }] };
|
|
2558
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2559
|
-
return cmsToolResponse(
|
|
2560
|
-
await callDashboardCms(opts, `/api/kody/cms/${collection}`, {
|
|
2561
|
-
method: "POST",
|
|
2562
|
-
body: JSON.stringify(documentArg(args.data))
|
|
2563
|
-
})
|
|
2564
|
-
);
|
|
2565
|
-
}
|
|
2566
|
-
};
|
|
2567
|
-
const cmsUpdateDocumentTool = {
|
|
2568
|
-
name: "cms_update_document",
|
|
2569
|
-
description: "Update one Dashboard CMS document when the capability is trusted and the CMS collection allows update.",
|
|
2570
|
-
inputSchema: {
|
|
2571
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2572
|
-
id: z2.string().min(1).describe("Document id."),
|
|
2573
|
-
data: z2.record(z2.string(), z2.unknown()).describe("Partial document fields to update.")
|
|
2574
|
-
},
|
|
2575
|
-
handler: async (args) => {
|
|
2576
|
-
const refusal = assertCmsWriteAllowed(opts);
|
|
2577
|
-
if (refusal) return { content: [{ type: "text", text: refusal }] };
|
|
2578
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2579
|
-
const id = encodeURIComponent(stringArg(args.id));
|
|
2580
|
-
return cmsToolResponse(
|
|
2581
|
-
await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`, {
|
|
2582
|
-
method: "PATCH",
|
|
2583
|
-
body: JSON.stringify(documentArg(args.data))
|
|
2584
|
-
})
|
|
2585
|
-
);
|
|
2586
|
-
}
|
|
2587
|
-
};
|
|
2774
|
+
const cmsTools = dashboardCmsToolDefinitions({
|
|
2775
|
+
repoSlug: opts.repoSlug,
|
|
2776
|
+
assertWriteAllowed: () => assertCmsWriteAllowed(opts)
|
|
2777
|
+
});
|
|
2588
2778
|
return [
|
|
2589
2779
|
listTool,
|
|
2590
2780
|
syncTool,
|
|
@@ -2597,24 +2787,20 @@ function capabilityToolDefinitions(opts) {
|
|
|
2597
2787
|
ensureIssueTool,
|
|
2598
2788
|
ensureCommentTool,
|
|
2599
2789
|
dispatchTool,
|
|
2600
|
-
|
|
2601
|
-
cmsListDocumentsTool,
|
|
2602
|
-
cmsGetDocumentTool,
|
|
2603
|
-
cmsCreateDocumentTool,
|
|
2604
|
-
cmsUpdateDocumentTool
|
|
2790
|
+
...cmsTools
|
|
2605
2791
|
];
|
|
2606
2792
|
}
|
|
2607
2793
|
function buildCapabilityMcpServer(opts) {
|
|
2608
2794
|
const definitions = capabilityToolDefinitions(opts);
|
|
2609
2795
|
const tools = definitions.map(
|
|
2610
|
-
(def) =>
|
|
2796
|
+
(def) => tool4(
|
|
2611
2797
|
def.name,
|
|
2612
2798
|
def.description,
|
|
2613
2799
|
def.inputSchema,
|
|
2614
2800
|
async (args) => def.handler(args)
|
|
2615
2801
|
)
|
|
2616
2802
|
);
|
|
2617
|
-
const server =
|
|
2803
|
+
const server = createSdkMcpServer4({
|
|
2618
2804
|
name: "kody-capability",
|
|
2619
2805
|
version: "0.1.0",
|
|
2620
2806
|
tools
|
|
@@ -2625,6 +2811,7 @@ var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, THREAD_BODY_MAX, CHECK_
|
|
|
2625
2811
|
var init_capabilityMcp = __esm({
|
|
2626
2812
|
"src/capabilityMcp.ts"() {
|
|
2627
2813
|
"use strict";
|
|
2814
|
+
init_dashboardCmsMcp();
|
|
2628
2815
|
init_issue();
|
|
2629
2816
|
init_registry();
|
|
2630
2817
|
init_stateRepo();
|
|
@@ -2649,11 +2836,7 @@ var init_capabilityMcp = __esm({
|
|
|
2649
2836
|
"ensure_issue",
|
|
2650
2837
|
"ensure_comment",
|
|
2651
2838
|
"dispatch_workflow",
|
|
2652
|
-
|
|
2653
|
-
"cms_list_documents",
|
|
2654
|
-
"cms_get_document",
|
|
2655
|
-
"cms_create_document",
|
|
2656
|
-
"cms_update_document"
|
|
2839
|
+
...DASHBOARD_CMS_MCP_TOOL_NAMES
|
|
2657
2840
|
];
|
|
2658
2841
|
}
|
|
2659
2842
|
});
|
|
@@ -2701,7 +2884,7 @@ var init_repoWorkspace = __esm({
|
|
|
2701
2884
|
defaultCloneRepo = (repo, token, dir) => {
|
|
2702
2885
|
fs7.mkdirSync(path9.dirname(dir), { recursive: true });
|
|
2703
2886
|
const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
2704
|
-
return new Promise((
|
|
2887
|
+
return new Promise((resolve9, reject) => {
|
|
2705
2888
|
const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
|
|
2706
2889
|
stdio: "inherit"
|
|
2707
2890
|
});
|
|
@@ -2717,7 +2900,7 @@ var init_repoWorkspace = __esm({
|
|
|
2717
2900
|
spawnSync("git", ["-C", dir, "config", "user.email", email]);
|
|
2718
2901
|
} catch {
|
|
2719
2902
|
}
|
|
2720
|
-
|
|
2903
|
+
resolve9();
|
|
2721
2904
|
});
|
|
2722
2905
|
child.on("error", reject);
|
|
2723
2906
|
});
|
|
@@ -2731,8 +2914,8 @@ __export(fetchRepoMcp_exports, {
|
|
|
2731
2914
|
buildFetchRepoMcpServer: () => buildFetchRepoMcpServer,
|
|
2732
2915
|
fetchRepoToolDefinition: () => fetchRepoToolDefinition
|
|
2733
2916
|
});
|
|
2734
|
-
import { createSdkMcpServer as
|
|
2735
|
-
import { z as
|
|
2917
|
+
import { createSdkMcpServer as createSdkMcpServer5, tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
|
|
2918
|
+
import { z as z4 } from "zod";
|
|
2736
2919
|
function fetchRepoToolDefinition(opts) {
|
|
2737
2920
|
return {
|
|
2738
2921
|
name: "fetch_repo",
|
|
@@ -2767,10 +2950,10 @@ Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in you
|
|
|
2767
2950
|
}
|
|
2768
2951
|
function buildFetchRepoMcpServer(opts) {
|
|
2769
2952
|
const def = fetchRepoToolDefinition(opts);
|
|
2770
|
-
const fetchTool =
|
|
2953
|
+
const fetchTool = tool5(def.name, def.description, def.inputSchema, async (args) => {
|
|
2771
2954
|
return def.handler({ repo: String(args.repo ?? "") });
|
|
2772
2955
|
});
|
|
2773
|
-
return
|
|
2956
|
+
return createSdkMcpServer5({
|
|
2774
2957
|
name: "kody-fetch-repo",
|
|
2775
2958
|
version: "0.1.0",
|
|
2776
2959
|
tools: [fetchTool]
|
|
@@ -2783,7 +2966,7 @@ var init_fetchRepoMcp = __esm({
|
|
|
2783
2966
|
init_repoWorkspace();
|
|
2784
2967
|
DESCRIPTION3 = 'Clone another GitHub repository into your workspace so you can read and work on it. Pass `repo` as "owner/name" (e.g. "A-Guy-educ/A-Guy"). Returns the absolute path of the clone \u2014 then use your Read/Grep/Glob/Bash tools at that path to inspect it. Already-fetched repos are reused instantly. Use this whenever the user asks about a repository other than your current one \u2014 you are NOT limited to a single repo.';
|
|
2785
2968
|
INPUT_SCHEMA3 = {
|
|
2786
|
-
repo:
|
|
2969
|
+
repo: z4.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
|
|
2787
2970
|
};
|
|
2788
2971
|
}
|
|
2789
2972
|
});
|
|
@@ -2867,7 +3050,7 @@ async function runAgent(opts) {
|
|
|
2867
3050
|
const turnTimeoutMs = resolveTurnTimeoutMs(opts);
|
|
2868
3051
|
let outcome = "failed";
|
|
2869
3052
|
let outcomeKind = "generic_failed";
|
|
2870
|
-
let
|
|
3053
|
+
let errorMessage2;
|
|
2871
3054
|
let tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
2872
3055
|
let messageCount = 0;
|
|
2873
3056
|
let finalText = "";
|
|
@@ -2883,7 +3066,7 @@ async function runAgent(opts) {
|
|
|
2883
3066
|
const resultTexts = [];
|
|
2884
3067
|
outcome = "failed";
|
|
2885
3068
|
outcomeKind = "generic_failed";
|
|
2886
|
-
|
|
3069
|
+
errorMessage2 = void 0;
|
|
2887
3070
|
tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
2888
3071
|
messageCount = 0;
|
|
2889
3072
|
let sawMutatingTool = false;
|
|
@@ -2941,6 +3124,24 @@ async function runAgent(opts) {
|
|
|
2941
3124
|
});
|
|
2942
3125
|
mcpEntries.push(["kody-capability", dutyHandle.server]);
|
|
2943
3126
|
}
|
|
3127
|
+
if (opts.enableDashboardCmsTool) {
|
|
3128
|
+
const { buildDashboardCmsMcpServer: buildDashboardCmsMcpServer2, DASHBOARD_CMS_MCP_TOOL_NAMES: DASHBOARD_CMS_MCP_TOOL_NAMES2 } = await Promise.resolve().then(() => (init_dashboardCmsMcp(), dashboardCmsMcp_exports));
|
|
3129
|
+
if (!opts.cmsRepoSlug) {
|
|
3130
|
+
throw new Error("enableDashboardCmsTool requires cmsRepoSlug (owner/name)");
|
|
3131
|
+
}
|
|
3132
|
+
const cmsHandle = buildDashboardCmsMcpServer2({
|
|
3133
|
+
repoSlug: opts.cmsRepoSlug,
|
|
3134
|
+
dashboardUrl: opts.cmsDashboardUrl,
|
|
3135
|
+
token: opts.cmsToken,
|
|
3136
|
+
storeRepoUrl: opts.cmsStoreRepoUrl,
|
|
3137
|
+
storeRef: opts.cmsStoreRef
|
|
3138
|
+
});
|
|
3139
|
+
mcpEntries.push(["kody-cms", cmsHandle.server]);
|
|
3140
|
+
for (const toolName of DASHBOARD_CMS_MCP_TOOL_NAMES2) {
|
|
3141
|
+
;
|
|
3142
|
+
queryOptions.allowedTools.push(`mcp__kody-cms__${toolName}`);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2944
3145
|
if (opts.enableFetchRepoTool && opts.reposRoot) {
|
|
2945
3146
|
const { buildFetchRepoMcpServer: buildFetchRepoMcpServer2 } = await Promise.resolve().then(() => (init_fetchRepoMcp(), fetchRepoMcp_exports));
|
|
2946
3147
|
const fetchServer = buildFetchRepoMcpServer2({
|
|
@@ -3007,10 +3208,10 @@ async function runAgent(opts) {
|
|
|
3007
3208
|
let timer;
|
|
3008
3209
|
let next;
|
|
3009
3210
|
if (turnTimeoutMs > 0) {
|
|
3010
|
-
const timeoutPromise = new Promise((
|
|
3211
|
+
const timeoutPromise = new Promise((resolve9) => {
|
|
3011
3212
|
timer = setTimeout(() => {
|
|
3012
3213
|
timedOut = true;
|
|
3013
|
-
|
|
3214
|
+
resolve9({ done: true, value: void 0 });
|
|
3014
3215
|
}, turnTimeoutMs);
|
|
3015
3216
|
});
|
|
3016
3217
|
next = await Promise.race([nextPromise, timeoutPromise]);
|
|
@@ -3021,7 +3222,7 @@ async function runAgent(opts) {
|
|
|
3021
3222
|
if (timedOut) {
|
|
3022
3223
|
outcome = "failed";
|
|
3023
3224
|
outcomeKind = "stalled";
|
|
3024
|
-
|
|
3225
|
+
errorMessage2 = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
|
|
3025
3226
|
if (typeof iterator.return === "function") {
|
|
3026
3227
|
try {
|
|
3027
3228
|
await iterator.return(void 0);
|
|
@@ -3125,17 +3326,17 @@ async function runAgent(opts) {
|
|
|
3125
3326
|
} else {
|
|
3126
3327
|
outcome = "failed";
|
|
3127
3328
|
outcomeKind = classifySubtype(m.subtype);
|
|
3128
|
-
|
|
3329
|
+
errorMessage2 = `result subtype: ${m.subtype ?? "unknown"}`;
|
|
3129
3330
|
}
|
|
3130
3331
|
}
|
|
3131
3332
|
}
|
|
3132
3333
|
} catch (e) {
|
|
3133
3334
|
if (sawTerminalSuccess) {
|
|
3134
|
-
|
|
3335
|
+
errorMessage2 = e instanceof Error ? e.message : String(e);
|
|
3135
3336
|
} else {
|
|
3136
3337
|
outcome = "failed";
|
|
3137
3338
|
outcomeKind = "model_error";
|
|
3138
|
-
|
|
3339
|
+
errorMessage2 = e instanceof Error ? e.message : String(e);
|
|
3139
3340
|
}
|
|
3140
3341
|
} finally {
|
|
3141
3342
|
try {
|
|
@@ -3153,7 +3354,7 @@ async function runAgent(opts) {
|
|
|
3153
3354
|
if (outcome === "completed" && sawLoginRequired) {
|
|
3154
3355
|
outcome = "failed";
|
|
3155
3356
|
outcomeKind = "model_error";
|
|
3156
|
-
|
|
3357
|
+
errorMessage2 = "Claude Code reported it is not logged in; refusing to mark agent run successful";
|
|
3157
3358
|
}
|
|
3158
3359
|
if (outcome === "completed" && !sawMutatingTool) {
|
|
3159
3360
|
const backendDead = opts.isBackendHealthy ? !await opts.isBackendHealthy() : false;
|
|
@@ -3162,14 +3363,14 @@ async function runAgent(opts) {
|
|
|
3162
3363
|
outcome = "failed";
|
|
3163
3364
|
outcomeKind = "model_error";
|
|
3164
3365
|
noWorkSuccess = true;
|
|
3165
|
-
|
|
3366
|
+
errorMessage2 = errorMessage2 ?? (backendDead ? "model backend unreachable after a reported success \u2014 proxy crashed mid-request (hollow success)" : "session reported success but produced no model output (0 output tokens) \u2014 backend likely unreachable");
|
|
3166
3367
|
}
|
|
3167
3368
|
}
|
|
3168
|
-
const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(
|
|
3369
|
+
const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(errorMessage2) || noWorkSuccess);
|
|
3169
3370
|
if (!shouldRetry) break;
|
|
3170
3371
|
const delayMs = CONNECTION_RETRY_BASE_MS * 2 ** attempt;
|
|
3171
3372
|
process.stderr.write(
|
|
3172
|
-
`[kody agent] transient connection error (attempt ${attempt + 1}/${MAX_CONNECTION_RETRIES + 1}); retrying in ${Math.round(delayMs / 1e3)}s: ${
|
|
3373
|
+
`[kody agent] transient connection error (attempt ${attempt + 1}/${MAX_CONNECTION_RETRIES + 1}); retrying in ${Math.round(delayMs / 1e3)}s: ${errorMessage2}
|
|
3173
3374
|
`
|
|
3174
3375
|
);
|
|
3175
3376
|
if (opts.ensureBackend) {
|
|
@@ -3188,7 +3389,7 @@ async function runAgent(opts) {
|
|
|
3188
3389
|
outcomeKind,
|
|
3189
3390
|
finalText,
|
|
3190
3391
|
...submittedState ? { submittedState } : {},
|
|
3191
|
-
error:
|
|
3392
|
+
error: errorMessage2,
|
|
3192
3393
|
ndjsonPath,
|
|
3193
3394
|
durationMs: Date.now() - startedAt,
|
|
3194
3395
|
tokens,
|
|
@@ -5813,7 +6014,7 @@ var init_litellm = __esm({
|
|
|
5813
6014
|
|
|
5814
6015
|
// src/pushWithRetry.ts
|
|
5815
6016
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
5816
|
-
function
|
|
6017
|
+
function sleepSync2(ms) {
|
|
5817
6018
|
if (ms <= 0) return;
|
|
5818
6019
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5819
6020
|
}
|
|
@@ -5874,7 +6075,7 @@ function pushWithRetry(opts = {}) {
|
|
|
5874
6075
|
};
|
|
5875
6076
|
}
|
|
5876
6077
|
const delay = Math.min(baseBackoff * 2 ** (attempt - 1), MAX_BACKOFF_MS);
|
|
5877
|
-
|
|
6078
|
+
sleepSync2(delay);
|
|
5878
6079
|
}
|
|
5879
6080
|
return {
|
|
5880
6081
|
ok: false,
|
|
@@ -6119,6 +6320,13 @@ function jobMetaFromData(data) {
|
|
|
6119
6320
|
why: typeof data.jobWhy === "string" ? data.jobWhy : void 0
|
|
6120
6321
|
};
|
|
6121
6322
|
}
|
|
6323
|
+
function applyStandaloneTerminalState(state, ctx, profile) {
|
|
6324
|
+
if (profile.lifecycleConfig?.finalize !== true || state.flow?.issueNumber) return;
|
|
6325
|
+
const delivered = ctx.output.exitCode === 0 && !!state.core.prUrl;
|
|
6326
|
+
state.core.phase = delivered ? "shipped" : "failed";
|
|
6327
|
+
state.core.status = delivered ? "succeeded" : "failed";
|
|
6328
|
+
state.core.currentExecutable = null;
|
|
6329
|
+
}
|
|
6122
6330
|
function synthesizeAction(ctx) {
|
|
6123
6331
|
const ok = ctx.output.exitCode === 0;
|
|
6124
6332
|
return {
|
|
@@ -6149,6 +6357,7 @@ var init_saveTaskState = __esm({
|
|
|
6149
6357
|
});
|
|
6150
6358
|
if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
|
|
6151
6359
|
if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
|
|
6360
|
+
applyStandaloneTerminalState(next, ctx, profile);
|
|
6152
6361
|
writeTaskState(target, number, next, ctx.cwd, ctx.config);
|
|
6153
6362
|
ctx.data.taskState = next;
|
|
6154
6363
|
ctx.data.taskStateRendered = renderStateComment(next);
|
|
@@ -11478,12 +11687,9 @@ var init_finalizeTerminal = __esm({
|
|
|
11478
11687
|
const issueNumber = ctx.args.issue;
|
|
11479
11688
|
const targetNumber = ctx.data.commentTargetNumber ?? issueNumber;
|
|
11480
11689
|
if (!targetNumber) return;
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
} catch {
|
|
11485
|
-
prUrl = void 0;
|
|
11486
|
-
}
|
|
11690
|
+
const cachedState = ctx.data.taskState;
|
|
11691
|
+
let state = cachedState;
|
|
11692
|
+
const prUrl = cachedState?.core.prUrl ?? ctx.output.prUrl ?? ctx.data.prResult?.url;
|
|
11487
11693
|
const delivered = ctx.output.exitCode === 0 && !!prUrl;
|
|
11488
11694
|
const spec = delivered ? DONE : FAILED;
|
|
11489
11695
|
const phase = delivered ? "shipped" : "failed";
|
|
@@ -11491,12 +11697,28 @@ var init_finalizeTerminal = __esm({
|
|
|
11491
11697
|
if (issueNumber) setKodyLabel(issueNumber, spec, ctx.cwd);
|
|
11492
11698
|
const prNumber = prUrl ? parsePrNumber(prUrl) : null;
|
|
11493
11699
|
if (prNumber && prNumber !== issueNumber) setKodyLabel(prNumber, spec, ctx.cwd);
|
|
11700
|
+
if (!state) {
|
|
11701
|
+
try {
|
|
11702
|
+
state = readTaskState(target, targetNumber, ctx.cwd, ctx.config);
|
|
11703
|
+
} catch {
|
|
11704
|
+
state = void 0;
|
|
11705
|
+
}
|
|
11706
|
+
}
|
|
11707
|
+
if (!state) return;
|
|
11708
|
+
const alreadyTerminal = state.core.phase === phase && state.core.status === status && state.core.currentExecutable === null;
|
|
11709
|
+
if (alreadyTerminal) return;
|
|
11710
|
+
const next = {
|
|
11711
|
+
...state,
|
|
11712
|
+
core: {
|
|
11713
|
+
...state.core,
|
|
11714
|
+
phase,
|
|
11715
|
+
status,
|
|
11716
|
+
currentExecutable: null
|
|
11717
|
+
}
|
|
11718
|
+
};
|
|
11719
|
+
ctx.data.taskState = next;
|
|
11494
11720
|
try {
|
|
11495
|
-
|
|
11496
|
-
state.core.phase = phase;
|
|
11497
|
-
state.core.status = status;
|
|
11498
|
-
state.core.currentExecutable = null;
|
|
11499
|
-
writeTaskState(target, targetNumber, state, ctx.cwd, ctx.config);
|
|
11721
|
+
writeTaskState(target, targetNumber, next, ctx.cwd, ctx.config);
|
|
11500
11722
|
} catch (err) {
|
|
11501
11723
|
process.stderr.write(
|
|
11502
11724
|
`[kody finalizeTerminal] failed to write terminal state on ${target} #${targetNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -12436,7 +12658,7 @@ function retryDelaysMs() {
|
|
|
12436
12658
|
}
|
|
12437
12659
|
function sleep(ms) {
|
|
12438
12660
|
if (ms <= 0) return Promise.resolve();
|
|
12439
|
-
return new Promise((
|
|
12661
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
12440
12662
|
}
|
|
12441
12663
|
async function fetchGoalStateWithRetry(config, goalId, cwd) {
|
|
12442
12664
|
let state = fetchGoalState(config, goalId, cwd);
|
|
@@ -13161,17 +13383,22 @@ var init_mirrorStateToPr = __esm({
|
|
|
13161
13383
|
const issueNumber = ctx.data.commentTargetNumber;
|
|
13162
13384
|
const issueTarget = ctx.data.commentTargetType;
|
|
13163
13385
|
if (!issueNumber || issueTarget !== "issue") return;
|
|
13164
|
-
|
|
13386
|
+
let state = ctx.data.taskState;
|
|
13387
|
+
const prUrl = ctx.output.prUrl ?? ctx.data.prResult?.url ?? state?.core.prUrl;
|
|
13165
13388
|
if (!prUrl) return;
|
|
13166
13389
|
const prNumber = parsePrNumber4(prUrl);
|
|
13167
13390
|
if (!prNumber) return;
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
13391
|
+
if (!state) {
|
|
13392
|
+
try {
|
|
13393
|
+
state = readTaskState("issue", issueNumber, ctx.cwd, ctx.config);
|
|
13394
|
+
} catch {
|
|
13395
|
+
return;
|
|
13396
|
+
}
|
|
13397
|
+
}
|
|
13398
|
+
if (prUrl && !state.core.prUrl) {
|
|
13399
|
+
state = { ...state, core: { ...state.core, prUrl } };
|
|
13400
|
+
ctx.data.taskState = state;
|
|
13173
13401
|
}
|
|
13174
|
-
if (prUrl && !state.core.prUrl) state.core.prUrl = prUrl;
|
|
13175
13402
|
try {
|
|
13176
13403
|
writeTaskState("pr", prNumber, state, ctx.cwd, ctx.config);
|
|
13177
13404
|
} catch (err) {
|
|
@@ -15099,7 +15326,7 @@ var init_previewBuildHelpers = __esm({
|
|
|
15099
15326
|
// src/scripts/previewBuildRun.ts
|
|
15100
15327
|
import { spawn as spawn4 } from "child_process";
|
|
15101
15328
|
async function runCmd(cmd, args, opts = {}) {
|
|
15102
|
-
await new Promise((
|
|
15329
|
+
await new Promise((resolve9, reject) => {
|
|
15103
15330
|
const child = spawn4(cmd, args, {
|
|
15104
15331
|
cwd: opts.cwd,
|
|
15105
15332
|
env: { ...process.env, ...opts.env ?? {} },
|
|
@@ -15111,7 +15338,7 @@ async function runCmd(cmd, args, opts = {}) {
|
|
|
15111
15338
|
}
|
|
15112
15339
|
child.on("error", reject);
|
|
15113
15340
|
child.on("close", (code) => {
|
|
15114
|
-
if (code === 0)
|
|
15341
|
+
if (code === 0) resolve9();
|
|
15115
15342
|
else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
|
|
15116
15343
|
});
|
|
15117
15344
|
});
|
|
@@ -16235,7 +16462,7 @@ function stripAnsi2(s) {
|
|
|
16235
16462
|
return s.replace(ANSI_RE2, "");
|
|
16236
16463
|
}
|
|
16237
16464
|
function runCommand2(command, cwd) {
|
|
16238
|
-
return new Promise((
|
|
16465
|
+
return new Promise((resolve9) => {
|
|
16239
16466
|
const child = spawn5(command, {
|
|
16240
16467
|
cwd,
|
|
16241
16468
|
shell: true,
|
|
@@ -16262,11 +16489,11 @@ function runCommand2(command, cwd) {
|
|
|
16262
16489
|
}, TEST_TIMEOUT_MS);
|
|
16263
16490
|
child.on("exit", (code) => {
|
|
16264
16491
|
clearTimeout(timer);
|
|
16265
|
-
|
|
16492
|
+
resolve9({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
|
|
16266
16493
|
});
|
|
16267
16494
|
child.on("error", (err) => {
|
|
16268
16495
|
clearTimeout(timer);
|
|
16269
|
-
|
|
16496
|
+
resolve9({ exitCode: -1, output: err.message });
|
|
16270
16497
|
});
|
|
16271
16498
|
});
|
|
16272
16499
|
}
|
|
@@ -16671,21 +16898,21 @@ function lineStream(stream) {
|
|
|
16671
16898
|
tryDeliver();
|
|
16672
16899
|
});
|
|
16673
16900
|
return {
|
|
16674
|
-
next: (timeoutMs) => new Promise((
|
|
16901
|
+
next: (timeoutMs) => new Promise((resolve9) => {
|
|
16675
16902
|
if (queue.length > 0) {
|
|
16676
|
-
|
|
16903
|
+
resolve9(queue.shift());
|
|
16677
16904
|
return;
|
|
16678
16905
|
}
|
|
16679
16906
|
if (ended) {
|
|
16680
|
-
|
|
16907
|
+
resolve9(null);
|
|
16681
16908
|
return;
|
|
16682
16909
|
}
|
|
16683
|
-
waiter =
|
|
16910
|
+
waiter = resolve9;
|
|
16684
16911
|
const t = setTimeout(
|
|
16685
16912
|
() => {
|
|
16686
|
-
if (waiter ===
|
|
16913
|
+
if (waiter === resolve9) {
|
|
16687
16914
|
waiter = null;
|
|
16688
|
-
|
|
16915
|
+
resolve9(null);
|
|
16689
16916
|
}
|
|
16690
16917
|
},
|
|
16691
16918
|
Math.max(0, timeoutMs)
|
|
@@ -17068,42 +17295,111 @@ var init_scripts = __esm({
|
|
|
17068
17295
|
});
|
|
17069
17296
|
|
|
17070
17297
|
// src/stateWorkspace.ts
|
|
17298
|
+
import { execFileSync as execFileSync24 } from "child_process";
|
|
17299
|
+
import * as crypto3 from "crypto";
|
|
17071
17300
|
import * as fs43 from "fs";
|
|
17301
|
+
import * as os7 from "os";
|
|
17072
17302
|
import * as path41 from "path";
|
|
17073
17303
|
function writeLocalFile(cwd, relativePath, content) {
|
|
17074
17304
|
const fullPath = path41.join(cwd, relativePath);
|
|
17075
17305
|
fs43.mkdirSync(path41.dirname(fullPath), { recursive: true });
|
|
17076
17306
|
fs43.writeFileSync(fullPath, content);
|
|
17077
17307
|
}
|
|
17078
|
-
function
|
|
17079
|
-
const
|
|
17080
|
-
|
|
17081
|
-
|
|
17082
|
-
|
|
17083
|
-
|
|
17084
|
-
|
|
17085
|
-
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17090
|
-
|
|
17091
|
-
}
|
|
17308
|
+
function copyPath(source, target) {
|
|
17309
|
+
const st = fs43.lstatSync(source);
|
|
17310
|
+
fs43.rmSync(target, { recursive: true, force: true });
|
|
17311
|
+
if (st.isSymbolicLink()) return;
|
|
17312
|
+
fs43.mkdirSync(path41.dirname(target), { recursive: true });
|
|
17313
|
+
fs43.cpSync(source, target, { recursive: true, force: true });
|
|
17314
|
+
}
|
|
17315
|
+
function overlayDirectoryChildren(cwd, sourceDir, localDir) {
|
|
17316
|
+
if (!fs43.existsSync(sourceDir)) return;
|
|
17317
|
+
for (const entry of fs43.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
17318
|
+
const source = path41.join(sourceDir, entry.name);
|
|
17319
|
+
const target = path41.join(cwd, localDir, entry.name);
|
|
17320
|
+
copyPath(source, target);
|
|
17092
17321
|
}
|
|
17093
17322
|
}
|
|
17094
17323
|
function hydrateStateWorkspace(config, cwd) {
|
|
17324
|
+
if (process.env.VITEST && process.env[TEST_FETCH_ENV] !== "1") return;
|
|
17325
|
+
const parsed = parseStateRepo(config);
|
|
17326
|
+
const hydrateKey = `${path41.resolve(cwd)}|${parsed.owner}/${parsed.repo}|${parsed.basePath}|${STATE_BRANCH}`;
|
|
17327
|
+
if (hydratedWorkspaces.has(hydrateKey)) return;
|
|
17328
|
+
const snapshotRoot = fetchStateSnapshot(parsed);
|
|
17095
17329
|
for (const mapping of DIR_MAPPINGS) {
|
|
17096
|
-
|
|
17330
|
+
overlayDirectoryChildren(cwd, path41.join(snapshotRoot, mapping.stateDir), mapping.localDir);
|
|
17097
17331
|
}
|
|
17098
17332
|
for (const mapping of FILE_MAPPINGS) {
|
|
17099
|
-
const
|
|
17100
|
-
if (
|
|
17333
|
+
const source = path41.join(snapshotRoot, mapping.statePath);
|
|
17334
|
+
if (fs43.existsSync(source) && !fs43.lstatSync(source).isSymbolicLink() && fs43.statSync(source).isFile()) {
|
|
17335
|
+
writeLocalFile(cwd, mapping.localPath, fs43.readFileSync(source, "utf-8"));
|
|
17336
|
+
}
|
|
17337
|
+
}
|
|
17338
|
+
hydratedWorkspaces.add(hydrateKey);
|
|
17339
|
+
}
|
|
17340
|
+
function fetchStateSnapshot(parsed) {
|
|
17341
|
+
const cacheDir = path41.join(cacheRoot2(), cacheKey2(parsed));
|
|
17342
|
+
const url = `https://github.com/${parsed.owner}/${parsed.repo}.git`;
|
|
17343
|
+
try {
|
|
17344
|
+
fs43.mkdirSync(path41.dirname(cacheDir), { recursive: true });
|
|
17345
|
+
if (!fs43.existsSync(path41.join(cacheDir, ".git"))) {
|
|
17346
|
+
fs43.rmSync(cacheDir, { recursive: true, force: true });
|
|
17347
|
+
runGit3(["clone", "--no-checkout", "--filter=blob:none", url, cacheDir]);
|
|
17348
|
+
}
|
|
17349
|
+
runGit3(["-C", cacheDir, "remote", "set-url", "origin", url]);
|
|
17350
|
+
runGit3(["-C", cacheDir, "fetch", "--depth=1", "origin", STATE_BRANCH]);
|
|
17351
|
+
runGit3(["-C", cacheDir, "sparse-checkout", "init", "--cone"]);
|
|
17352
|
+
runGit3(["-C", cacheDir, "sparse-checkout", "set", parsed.basePath]);
|
|
17353
|
+
runGit3(["-C", cacheDir, "checkout", "--force", "--detach", "FETCH_HEAD"]);
|
|
17354
|
+
runGit3(["-C", cacheDir, "clean", "-fdx"]);
|
|
17355
|
+
} catch (err) {
|
|
17356
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17357
|
+
throw new Error(
|
|
17358
|
+
`stateWorkspace: failed to fetch ${parsed.owner}/${parsed.repo}:${parsed.basePath}@${STATE_BRANCH}: ${msg}`
|
|
17359
|
+
);
|
|
17360
|
+
}
|
|
17361
|
+
return path41.join(cacheDir, parsed.basePath);
|
|
17362
|
+
}
|
|
17363
|
+
function cacheRoot2() {
|
|
17364
|
+
return process.env[CACHE_ENV2]?.trim() || path41.join(os7.homedir(), ".cache", "kody", "state-repo");
|
|
17365
|
+
}
|
|
17366
|
+
function cacheKey2(parsed) {
|
|
17367
|
+
return crypto3.createHash("sha256").update(`${parsed.owner}/${parsed.repo}#${STATE_BRANCH}#${parsed.basePath}`).digest("hex").slice(0, 24);
|
|
17368
|
+
}
|
|
17369
|
+
function runGit3(args) {
|
|
17370
|
+
try {
|
|
17371
|
+
execFileSync24("git", args, {
|
|
17372
|
+
encoding: "utf-8",
|
|
17373
|
+
env: githubAuthEnv(),
|
|
17374
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
17375
|
+
timeout: 12e4
|
|
17376
|
+
});
|
|
17377
|
+
} catch (err) {
|
|
17378
|
+
const stderr = err.stderr;
|
|
17379
|
+
const detail = Buffer.isBuffer(stderr) ? stderr.toString("utf-8").trim() : typeof stderr === "string" ? stderr.trim() : "";
|
|
17380
|
+
throw new Error(detail || `git ${args[0] ?? "command"} failed`);
|
|
17101
17381
|
}
|
|
17102
17382
|
}
|
|
17103
|
-
|
|
17383
|
+
function githubAuthEnv() {
|
|
17384
|
+
const token = githubToken();
|
|
17385
|
+
if (!token) return process.env;
|
|
17386
|
+
const encoded = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
17387
|
+
const existingCount = /^\d+$/.test(process.env.GIT_CONFIG_COUNT ?? "") ? Number(process.env.GIT_CONFIG_COUNT) : 0;
|
|
17388
|
+
return {
|
|
17389
|
+
...process.env,
|
|
17390
|
+
GIT_CONFIG_COUNT: String(existingCount + 1),
|
|
17391
|
+
[`GIT_CONFIG_KEY_${existingCount}`]: "http.https://github.com/.extraheader",
|
|
17392
|
+
[`GIT_CONFIG_VALUE_${existingCount}`]: `AUTHORIZATION: basic ${encoded}`
|
|
17393
|
+
};
|
|
17394
|
+
}
|
|
17395
|
+
function githubToken() {
|
|
17396
|
+
return process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || process.env.GH_PAT?.trim() || void 0;
|
|
17397
|
+
}
|
|
17398
|
+
var DIR_MAPPINGS, FILE_MAPPINGS, CACHE_ENV2, TEST_FETCH_ENV, hydratedWorkspaces;
|
|
17104
17399
|
var init_stateWorkspace = __esm({
|
|
17105
17400
|
"src/stateWorkspace.ts"() {
|
|
17106
17401
|
"use strict";
|
|
17402
|
+
init_stateBranch();
|
|
17107
17403
|
init_stateRepo();
|
|
17108
17404
|
DIR_MAPPINGS = [
|
|
17109
17405
|
{ stateDir: "executables", localDir: path41.join(".kody", "executables") },
|
|
@@ -17117,11 +17413,14 @@ var init_stateWorkspace = __esm({
|
|
|
17117
17413
|
{ statePath: "variables.json", localPath: path41.join(".kody", "variables.json") },
|
|
17118
17414
|
{ statePath: "secrets.enc", localPath: path41.join(".kody", "secrets.enc") }
|
|
17119
17415
|
];
|
|
17416
|
+
CACHE_ENV2 = "KODY_STATE_REPO_CACHE";
|
|
17417
|
+
TEST_FETCH_ENV = "KODY_STATE_WORKSPACE_FETCH_FOR_TESTS";
|
|
17418
|
+
hydratedWorkspaces = /* @__PURE__ */ new Set();
|
|
17120
17419
|
}
|
|
17121
17420
|
});
|
|
17122
17421
|
|
|
17123
17422
|
// src/tools.ts
|
|
17124
|
-
import { execFileSync as
|
|
17423
|
+
import { execFileSync as execFileSync25 } from "child_process";
|
|
17125
17424
|
function verifyCliTools(tools, cwd) {
|
|
17126
17425
|
const out = [];
|
|
17127
17426
|
for (const t of tools) out.push(verifyOne(t, cwd));
|
|
@@ -17135,30 +17434,30 @@ function firstRequiredFailure(results, tools) {
|
|
|
17135
17434
|
}
|
|
17136
17435
|
return null;
|
|
17137
17436
|
}
|
|
17138
|
-
function verifyOne(
|
|
17139
|
-
const result = { name:
|
|
17140
|
-
const checkRes = runShell(
|
|
17437
|
+
function verifyOne(tool6, cwd) {
|
|
17438
|
+
const result = { name: tool6.name, present: false, verified: false };
|
|
17439
|
+
const checkRes = runShell(tool6.install.checkCommand, cwd);
|
|
17141
17440
|
let present = checkRes.ok;
|
|
17142
|
-
if (!present &&
|
|
17143
|
-
runShell(
|
|
17144
|
-
present = runShell(
|
|
17441
|
+
if (!present && tool6.install.installCommand) {
|
|
17442
|
+
runShell(tool6.install.installCommand, cwd, 12e4);
|
|
17443
|
+
present = runShell(tool6.install.checkCommand, cwd).ok;
|
|
17145
17444
|
}
|
|
17146
17445
|
result.present = present;
|
|
17147
17446
|
if (!present) {
|
|
17148
|
-
result.error = `tool "${
|
|
17447
|
+
result.error = `tool "${tool6.name}" not on PATH (check: ${tool6.install.checkCommand})`;
|
|
17149
17448
|
return result;
|
|
17150
17449
|
}
|
|
17151
|
-
const verifyRes = runShell(
|
|
17450
|
+
const verifyRes = runShell(tool6.verify, cwd);
|
|
17152
17451
|
result.verified = verifyRes.ok;
|
|
17153
17452
|
if (!verifyRes.ok) {
|
|
17154
17453
|
const tail = formatStderrTail(verifyRes.stderr, verifyRes.stdout);
|
|
17155
|
-
result.error = `tool "${
|
|
17454
|
+
result.error = `tool "${tool6.name}" failed verify: ${tool6.verify}${tail ? ` \u2014 ${tail}` : ""}`;
|
|
17156
17455
|
}
|
|
17157
17456
|
return result;
|
|
17158
17457
|
}
|
|
17159
17458
|
function runShell(cmd, cwd, timeoutMs = 3e4) {
|
|
17160
17459
|
try {
|
|
17161
|
-
const stdout =
|
|
17460
|
+
const stdout = execFileSync25("sh", ["-c", cmd], {
|
|
17162
17461
|
cwd,
|
|
17163
17462
|
stdio: ["ignore", "pipe", "pipe"],
|
|
17164
17463
|
timeout: timeoutMs,
|
|
@@ -17904,14 +18203,14 @@ async function runShellEntry(entry, ctx, profile) {
|
|
|
17904
18203
|
let killTimer;
|
|
17905
18204
|
let escalateTimer;
|
|
17906
18205
|
const result = await new Promise(
|
|
17907
|
-
(
|
|
18206
|
+
(resolve9) => {
|
|
17908
18207
|
let settled = false;
|
|
17909
18208
|
const settle = (code, signal, spawnErr) => {
|
|
17910
18209
|
if (settled) return;
|
|
17911
18210
|
settled = true;
|
|
17912
18211
|
if (killTimer) clearTimeout(killTimer);
|
|
17913
18212
|
if (escalateTimer) clearTimeout(escalateTimer);
|
|
17914
|
-
|
|
18213
|
+
resolve9({ code, signal, spawnErr });
|
|
17915
18214
|
};
|
|
17916
18215
|
child.on("error", (err) => settle(null, null, err));
|
|
17917
18216
|
child.on("close", (code, signal) => settle(code, signal));
|
|
@@ -18864,6 +19163,12 @@ var CROSS_REPO_PROMPT = [
|
|
|
18864
19163
|
"instantly. When the user asks about a different repo \u2014 or to compare repos \u2014",
|
|
18865
19164
|
"fetch it instead of saying you are scoped to a single repo."
|
|
18866
19165
|
].join("\n");
|
|
19166
|
+
var DASHBOARD_CMS_PROMPT = [
|
|
19167
|
+
"# Dashboard CMS",
|
|
19168
|
+
"Use Dashboard CMS tools for CMS content. The Dashboard is the source of truth.",
|
|
19169
|
+
"Do not query Mongo directly for CMS content and do not infer CMS data from repository files.",
|
|
19170
|
+
"Use cms_list_documents first when you need to find content, then pass the raw id or cmsDocumentId to cms_get_document."
|
|
19171
|
+
].join("\n");
|
|
18867
19172
|
function buildExecutableCatalog() {
|
|
18868
19173
|
let discovered;
|
|
18869
19174
|
try {
|
|
@@ -18922,6 +19227,8 @@ async function runChatTurn(opts) {
|
|
|
18922
19227
|
const memoryBlock = readMemoryIndexBlock(opts.cwd);
|
|
18923
19228
|
const instructionsBlock = readInstructionsBlock(opts.cwd);
|
|
18924
19229
|
const crossRepoBlock = opts.reposRoot ? CROSS_REPO_PROMPT : null;
|
|
19230
|
+
const dashboardCmsEnabled = Boolean(opts.cmsDashboardUrl && opts.cmsRepoSlug && opts.cmsToken);
|
|
19231
|
+
const dashboardCmsBlock = dashboardCmsEnabled ? DASHBOARD_CMS_PROMPT : null;
|
|
18925
19232
|
const imageBlock = imagePaths.length > 0 ? [
|
|
18926
19233
|
"# Attached images",
|
|
18927
19234
|
"The user attached one or more images on this turn. They are saved as",
|
|
@@ -18936,6 +19243,7 @@ async function runChatTurn(opts) {
|
|
|
18936
19243
|
memoryBlock,
|
|
18937
19244
|
instructionsBlock,
|
|
18938
19245
|
crossRepoBlock,
|
|
19246
|
+
dashboardCmsBlock,
|
|
18939
19247
|
imageBlock,
|
|
18940
19248
|
catalog,
|
|
18941
19249
|
artifactAddendum
|
|
@@ -18963,6 +19271,14 @@ async function runChatTurn(opts) {
|
|
|
18963
19271
|
reposRoot: opts.reposRoot,
|
|
18964
19272
|
repoToken: opts.repoToken
|
|
18965
19273
|
} : {},
|
|
19274
|
+
...dashboardCmsEnabled ? {
|
|
19275
|
+
enableDashboardCmsTool: true,
|
|
19276
|
+
cmsDashboardUrl: opts.cmsDashboardUrl,
|
|
19277
|
+
cmsRepoSlug: opts.cmsRepoSlug,
|
|
19278
|
+
cmsToken: opts.cmsToken,
|
|
19279
|
+
cmsStoreRepoUrl: opts.cmsStoreRepoUrl,
|
|
19280
|
+
cmsStoreRef: opts.cmsStoreRef
|
|
19281
|
+
} : {},
|
|
18966
19282
|
onProgress: async (ev) => {
|
|
18967
19283
|
progressSeq += 1;
|
|
18968
19284
|
if (ev.kind === "thinking") {
|
|
@@ -19061,7 +19377,9 @@ function readMemoryIndexBlock(cwd) {
|
|
|
19061
19377
|
}
|
|
19062
19378
|
const trimmed = raw.trim();
|
|
19063
19379
|
if (!trimmed) return "";
|
|
19064
|
-
const body = trimmed.length > MAX_INDEX_BYTES ? trimmed.slice(0, MAX_INDEX_BYTES)
|
|
19380
|
+
const body = trimmed.length > MAX_INDEX_BYTES ? `${trimmed.slice(0, MAX_INDEX_BYTES)}
|
|
19381
|
+
|
|
19382
|
+
_\u2026 (memory index truncated; use recall_search to read more)_` : trimmed;
|
|
19065
19383
|
return [
|
|
19066
19384
|
"# Project memory index (state repo `memory/INDEX.md`)",
|
|
19067
19385
|
"",
|
|
@@ -19131,7 +19449,7 @@ _\u2026 (instructions truncated)_` : trimmed;
|
|
|
19131
19449
|
init_config();
|
|
19132
19450
|
|
|
19133
19451
|
// src/kody-cli.ts
|
|
19134
|
-
import { execFileSync as
|
|
19452
|
+
import { execFileSync as execFileSync26 } from "child_process";
|
|
19135
19453
|
import * as fs46 from "fs";
|
|
19136
19454
|
import * as path45 from "path";
|
|
19137
19455
|
|
|
@@ -19788,7 +20106,7 @@ function recoverCheckoutToken(env = process.env, cwd = process.cwd()) {
|
|
|
19788
20106
|
if (env.GITHUB_TOKEN?.trim()) return env.GITHUB_TOKEN.trim();
|
|
19789
20107
|
let header = "";
|
|
19790
20108
|
try {
|
|
19791
|
-
header =
|
|
20109
|
+
header = execFileSync26("git", ["config", "--local", "--get", "http.https://github.com/.extraheader"], {
|
|
19792
20110
|
cwd,
|
|
19793
20111
|
encoding: "utf-8",
|
|
19794
20112
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -19850,7 +20168,7 @@ function shouldChainScheduledWatch(match) {
|
|
|
19850
20168
|
}
|
|
19851
20169
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
19852
20170
|
try {
|
|
19853
|
-
|
|
20171
|
+
execFileSync26(cmd, args, {
|
|
19854
20172
|
cwd,
|
|
19855
20173
|
stdio: stream ? "inherit" : "pipe",
|
|
19856
20174
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
|
|
@@ -19863,7 +20181,7 @@ function shellOut(cmd, args, cwd, stream = true) {
|
|
|
19863
20181
|
}
|
|
19864
20182
|
function isOnPath(bin) {
|
|
19865
20183
|
try {
|
|
19866
|
-
|
|
20184
|
+
execFileSync26("which", [bin], { stdio: "pipe" });
|
|
19867
20185
|
return true;
|
|
19868
20186
|
} catch {
|
|
19869
20187
|
return false;
|
|
@@ -19904,7 +20222,7 @@ function installLitellmIfNeeded(cwd) {
|
|
|
19904
20222
|
} catch {
|
|
19905
20223
|
}
|
|
19906
20224
|
try {
|
|
19907
|
-
|
|
20225
|
+
execFileSync26("python3", ["-c", "import litellm"], { stdio: "pipe" });
|
|
19908
20226
|
process.stdout.write("\u2192 kody: litellm already installed\n");
|
|
19909
20227
|
return 0;
|
|
19910
20228
|
} catch {
|
|
@@ -19914,16 +20232,16 @@ function installLitellmIfNeeded(cwd) {
|
|
|
19914
20232
|
}
|
|
19915
20233
|
function configureGitIdentity(cwd) {
|
|
19916
20234
|
try {
|
|
19917
|
-
const name =
|
|
20235
|
+
const name = execFileSync26("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
19918
20236
|
if (name) return;
|
|
19919
20237
|
} catch {
|
|
19920
20238
|
}
|
|
19921
20239
|
try {
|
|
19922
|
-
|
|
20240
|
+
execFileSync26("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
|
|
19923
20241
|
} catch {
|
|
19924
20242
|
}
|
|
19925
20243
|
try {
|
|
19926
|
-
|
|
20244
|
+
execFileSync26("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
|
|
19927
20245
|
cwd,
|
|
19928
20246
|
stdio: "pipe"
|
|
19929
20247
|
});
|
|
@@ -20533,17 +20851,17 @@ function authOk(req, expected) {
|
|
|
20533
20851
|
return false;
|
|
20534
20852
|
}
|
|
20535
20853
|
function readJsonBody(req) {
|
|
20536
|
-
return new Promise((
|
|
20854
|
+
return new Promise((resolve9, reject) => {
|
|
20537
20855
|
const chunks = [];
|
|
20538
20856
|
req.on("data", (c) => chunks.push(c));
|
|
20539
20857
|
req.on("end", () => {
|
|
20540
20858
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
20541
20859
|
if (!raw.trim()) {
|
|
20542
|
-
|
|
20860
|
+
resolve9({});
|
|
20543
20861
|
return;
|
|
20544
20862
|
}
|
|
20545
20863
|
try {
|
|
20546
|
-
|
|
20864
|
+
resolve9(JSON.parse(raw));
|
|
20547
20865
|
} catch (err) {
|
|
20548
20866
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
20549
20867
|
}
|
|
@@ -20689,6 +21007,9 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
20689
21007
|
}
|
|
20690
21008
|
const repo = strField(body, "repo");
|
|
20691
21009
|
const repoToken = strField(body, "repoToken");
|
|
21010
|
+
const dashboardUrl = strField(body, "dashboardUrl");
|
|
21011
|
+
const storeRepoUrl = strField(body, "storeRepoUrl");
|
|
21012
|
+
const storeRef = strField(body, "storeRef");
|
|
20692
21013
|
let agentCwd;
|
|
20693
21014
|
try {
|
|
20694
21015
|
agentCwd = await ensureRepoCwd({
|
|
@@ -20765,7 +21086,14 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
20765
21086
|
sink,
|
|
20766
21087
|
// Let the agent clone + work on OTHER repos via the fetch_repo tool.
|
|
20767
21088
|
reposRoot: opts.reposRoot,
|
|
20768
|
-
repoToken
|
|
21089
|
+
repoToken,
|
|
21090
|
+
...dashboardUrl && repo && stateToken ? {
|
|
21091
|
+
cmsDashboardUrl: dashboardUrl,
|
|
21092
|
+
cmsRepoSlug: repo,
|
|
21093
|
+
cmsToken: stateToken,
|
|
21094
|
+
cmsStoreRepoUrl: storeRepoUrl,
|
|
21095
|
+
cmsStoreRef: storeRef
|
|
21096
|
+
} : {}
|
|
20769
21097
|
});
|
|
20770
21098
|
} catch (err) {
|
|
20771
21099
|
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
@@ -20885,11 +21213,11 @@ async function brainServe(opts) {
|
|
|
20885
21213
|
model,
|
|
20886
21214
|
litellmUrl
|
|
20887
21215
|
});
|
|
20888
|
-
await new Promise((
|
|
21216
|
+
await new Promise((resolve9) => {
|
|
20889
21217
|
server.listen(port, "0.0.0.0", () => {
|
|
20890
21218
|
process.stdout.write(`[brain-serve] listening on 0.0.0.0:${port} (cwd=${opts.cwd})
|
|
20891
21219
|
`);
|
|
20892
|
-
|
|
21220
|
+
resolve9();
|
|
20893
21221
|
});
|
|
20894
21222
|
});
|
|
20895
21223
|
const shutdown = (signal) => {
|
|
@@ -21022,7 +21350,10 @@ async function proxyToBrainServe(args) {
|
|
|
21022
21350
|
body: JSON.stringify({
|
|
21023
21351
|
message: args.body.message,
|
|
21024
21352
|
...args.body.repo ? { repo: args.body.repo } : {},
|
|
21025
|
-
...args.body.repoToken ? { repoToken: args.body.repoToken } : {}
|
|
21353
|
+
...args.body.repoToken ? { repoToken: args.body.repoToken } : {},
|
|
21354
|
+
...args.body.dashboardUrl ? { dashboardUrl: args.body.dashboardUrl } : {},
|
|
21355
|
+
...args.body.storeRepoUrl ? { storeRepoUrl: args.body.storeRepoUrl } : {},
|
|
21356
|
+
...args.body.storeRef ? { storeRef: args.body.storeRef } : {}
|
|
21026
21357
|
})
|
|
21027
21358
|
});
|
|
21028
21359
|
if (!upstream.ok || !upstream.body) {
|
|
@@ -21139,14 +21470,14 @@ async function startBrainProxy(opts) {
|
|
|
21139
21470
|
const { httpServer, handler } = buildBrainProxy(opts);
|
|
21140
21471
|
const port = opts.port ?? 0;
|
|
21141
21472
|
const host = opts.host ?? "127.0.0.1";
|
|
21142
|
-
await new Promise((
|
|
21473
|
+
await new Promise((resolve9) => httpServer.listen(port, host, () => resolve9()));
|
|
21143
21474
|
const addr = httpServer.address();
|
|
21144
21475
|
return {
|
|
21145
21476
|
httpServer,
|
|
21146
21477
|
port: addr.port,
|
|
21147
21478
|
url: `http://${host}:${addr.port}`,
|
|
21148
|
-
stop: () => new Promise((
|
|
21149
|
-
httpServer.close(() =>
|
|
21479
|
+
stop: () => new Promise((resolve9) => {
|
|
21480
|
+
httpServer.close(() => resolve9());
|
|
21150
21481
|
}),
|
|
21151
21482
|
handler
|
|
21152
21483
|
};
|
|
@@ -21237,15 +21568,15 @@ function buildMcpHttpServer(opts) {
|
|
|
21237
21568
|
transports.set(route.path, transport);
|
|
21238
21569
|
routes.set(route.path, route.name);
|
|
21239
21570
|
const mcpServer = new McpServer({ name: route.name, version: route.version });
|
|
21240
|
-
for (const
|
|
21571
|
+
for (const tool6 of route.tools) {
|
|
21241
21572
|
mcpServer.registerTool(
|
|
21242
|
-
|
|
21573
|
+
tool6.name,
|
|
21243
21574
|
{
|
|
21244
|
-
description:
|
|
21245
|
-
inputSchema:
|
|
21575
|
+
description: tool6.description,
|
|
21576
|
+
inputSchema: tool6.inputSchema
|
|
21246
21577
|
},
|
|
21247
21578
|
async (args) => {
|
|
21248
|
-
const result = await
|
|
21579
|
+
const result = await tool6.handler(args);
|
|
21249
21580
|
return {
|
|
21250
21581
|
content: result.content,
|
|
21251
21582
|
...result.isError ? { isError: true } : {}
|
|
@@ -21296,23 +21627,23 @@ function buildMcpHttpServer(opts) {
|
|
|
21296
21627
|
httpServer,
|
|
21297
21628
|
routes,
|
|
21298
21629
|
port,
|
|
21299
|
-
stop: () => new Promise((
|
|
21630
|
+
stop: () => new Promise((resolve9) => {
|
|
21300
21631
|
let pending = transports.size;
|
|
21301
21632
|
if (pending === 0) {
|
|
21302
|
-
httpServer.close(() =>
|
|
21633
|
+
httpServer.close(() => resolve9());
|
|
21303
21634
|
return;
|
|
21304
21635
|
}
|
|
21305
21636
|
for (const transport of transports.values()) {
|
|
21306
21637
|
void transport.close().finally(() => {
|
|
21307
21638
|
pending--;
|
|
21308
|
-
if (pending === 0) httpServer.close(() =>
|
|
21639
|
+
if (pending === 0) httpServer.close(() => resolve9());
|
|
21309
21640
|
});
|
|
21310
21641
|
}
|
|
21311
21642
|
})
|
|
21312
21643
|
};
|
|
21313
21644
|
}
|
|
21314
21645
|
function listenMcpHttpServer(server, host = "127.0.0.1") {
|
|
21315
|
-
return new Promise((
|
|
21646
|
+
return new Promise((resolve9, reject) => {
|
|
21316
21647
|
server.httpServer.once("error", reject);
|
|
21317
21648
|
server.httpServer.listen(server.port, host, () => {
|
|
21318
21649
|
server.httpServer.off("error", reject);
|
|
@@ -21320,7 +21651,7 @@ function listenMcpHttpServer(server, host = "127.0.0.1") {
|
|
|
21320
21651
|
if (addr && typeof addr === "object") {
|
|
21321
21652
|
server.port = addr.port;
|
|
21322
21653
|
}
|
|
21323
|
-
|
|
21654
|
+
resolve9();
|
|
21324
21655
|
});
|
|
21325
21656
|
});
|
|
21326
21657
|
}
|
|
@@ -21409,7 +21740,7 @@ import * as fs50 from "fs";
|
|
|
21409
21740
|
import * as path49 from "path";
|
|
21410
21741
|
|
|
21411
21742
|
// src/chat/inbox.ts
|
|
21412
|
-
import { execFileSync as
|
|
21743
|
+
import { execFileSync as execFileSync27 } from "child_process";
|
|
21413
21744
|
var DEFAULT_POLL_MS = 3e3;
|
|
21414
21745
|
async function waitForNextUserMessage(opts) {
|
|
21415
21746
|
const pollMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS;
|
|
@@ -21433,13 +21764,13 @@ async function waitForNextUserMessage(opts) {
|
|
|
21433
21764
|
try {
|
|
21434
21765
|
const branch = currentBranch(opts.cwd);
|
|
21435
21766
|
if (branch) {
|
|
21436
|
-
|
|
21437
|
-
|
|
21767
|
+
execFileSync27("git", ["fetch", "--quiet", "origin", branch], { cwd: opts.cwd, stdio: "pipe" });
|
|
21768
|
+
execFileSync27("git", ["merge", "--ff-only", "--quiet", `origin/${branch}`], {
|
|
21438
21769
|
cwd: opts.cwd,
|
|
21439
21770
|
stdio: "pipe"
|
|
21440
21771
|
});
|
|
21441
21772
|
} else {
|
|
21442
|
-
|
|
21773
|
+
execFileSync27("git", ["fetch", "--quiet", "--all"], { cwd: opts.cwd, stdio: "pipe" });
|
|
21443
21774
|
}
|
|
21444
21775
|
} catch (err) {
|
|
21445
21776
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -21461,11 +21792,11 @@ async function waitForNextUserMessage(opts) {
|
|
|
21461
21792
|
}
|
|
21462
21793
|
}
|
|
21463
21794
|
function sleep3(ms) {
|
|
21464
|
-
return new Promise((
|
|
21795
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
21465
21796
|
}
|
|
21466
21797
|
function currentBranch(cwd) {
|
|
21467
21798
|
try {
|
|
21468
|
-
const out =
|
|
21799
|
+
const out = execFileSync27("git", ["symbolic-ref", "--short", "HEAD"], {
|
|
21469
21800
|
cwd,
|
|
21470
21801
|
stdio: ["ignore", "pipe", "ignore"]
|
|
21471
21802
|
});
|
|
@@ -22551,7 +22882,7 @@ var PERF_GUEST = {
|
|
|
22551
22882
|
medium: { cpu_kind: "performance", cpus: 1, memory_mb: 2048 },
|
|
22552
22883
|
high: { cpu_kind: "performance", cpus: 2, memory_mb: 4096 }
|
|
22553
22884
|
};
|
|
22554
|
-
function
|
|
22885
|
+
function envInt2(name, dflt) {
|
|
22555
22886
|
const v = Number(process.env[name]);
|
|
22556
22887
|
return Number.isFinite(v) && v > 0 ? v : dflt;
|
|
22557
22888
|
}
|
|
@@ -22564,14 +22895,14 @@ function sendJson2(res, status, body) {
|
|
|
22564
22895
|
res.end(JSON.stringify(body));
|
|
22565
22896
|
}
|
|
22566
22897
|
function readJsonBody2(req) {
|
|
22567
|
-
return new Promise((
|
|
22898
|
+
return new Promise((resolve9, reject) => {
|
|
22568
22899
|
const chunks = [];
|
|
22569
22900
|
req.on("data", (c) => chunks.push(c));
|
|
22570
22901
|
req.on("end", () => {
|
|
22571
22902
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
22572
|
-
if (!raw.trim()) return
|
|
22903
|
+
if (!raw.trim()) return resolve9({});
|
|
22573
22904
|
try {
|
|
22574
|
-
|
|
22905
|
+
resolve9(JSON.parse(raw));
|
|
22575
22906
|
} catch (err) {
|
|
22576
22907
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
22577
22908
|
}
|
|
@@ -22662,8 +22993,8 @@ function synthesizeLegacyClaimRequest(input) {
|
|
|
22662
22993
|
async function poolServe() {
|
|
22663
22994
|
const masterRaw = process.env.KODY_MASTER_KEY?.trim();
|
|
22664
22995
|
if (!masterRaw) throw new Error("KODY_MASTER_KEY required for pool-serve");
|
|
22665
|
-
const
|
|
22666
|
-
if (!
|
|
22996
|
+
const githubToken2 = process.env.GITHUB_TOKEN?.trim();
|
|
22997
|
+
if (!githubToken2) throw new Error("GITHUB_TOKEN required for pool-serve (reads per-repo vaults)");
|
|
22667
22998
|
const master = masterKeyBytes(masterRaw);
|
|
22668
22999
|
const poolApiKey = derivePoolApiKey(master);
|
|
22669
23000
|
const runnerApiKey = deriveRunnerApiKey(master);
|
|
@@ -22671,12 +23002,12 @@ async function poolServe() {
|
|
|
22671
23002
|
const region = process.env.POOL_REGION ?? "fra";
|
|
22672
23003
|
const perf = process.env.POOL_PERF ?? "medium";
|
|
22673
23004
|
const guest = PERF_GUEST[perf] ?? PERF_GUEST.medium;
|
|
22674
|
-
const min =
|
|
22675
|
-
const runnerPort =
|
|
22676
|
-
const apiPort =
|
|
22677
|
-
const healthTimeoutMs =
|
|
23005
|
+
const min = envInt2("POOL_MIN", 2);
|
|
23006
|
+
const runnerPort = envInt2("RUNNER_PORT", 8080);
|
|
23007
|
+
const apiPort = envInt2("POOL_API_PORT", 4100);
|
|
23008
|
+
const healthTimeoutMs = envInt2("POOL_HEALTH_TIMEOUT_MS", 12e4);
|
|
22678
23009
|
const registry = new PoolRegistry({
|
|
22679
|
-
githubToken,
|
|
23010
|
+
githubToken: githubToken2,
|
|
22680
23011
|
masterKey: master,
|
|
22681
23012
|
base: {
|
|
22682
23013
|
min,
|
|
@@ -22690,12 +23021,12 @@ async function poolServe() {
|
|
|
22690
23021
|
},
|
|
22691
23022
|
log
|
|
22692
23023
|
});
|
|
22693
|
-
const refillMs =
|
|
23024
|
+
const refillMs = envInt2("POOL_REFILL_INTERVAL_MS", 6e4);
|
|
22694
23025
|
const tick = setInterval(() => {
|
|
22695
23026
|
registry.resyncAll().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
22696
23027
|
}, refillMs);
|
|
22697
23028
|
const dutyTickEnabled = (process.env.POOL_CAPABILITY_TICK ?? "1") !== "0";
|
|
22698
|
-
const dutyTickMs =
|
|
23029
|
+
const dutyTickMs = envInt2("POOL_CAPABILITY_TICK_MS", 15 * 6e4);
|
|
22699
23030
|
const dutyTick = dutyTickEnabled ? setInterval(() => {
|
|
22700
23031
|
runCapabilityFallbackTick({
|
|
22701
23032
|
isDegraded: () => gitHubActionsDegraded(),
|
|
@@ -22753,10 +23084,10 @@ async function poolServe() {
|
|
|
22753
23084
|
}
|
|
22754
23085
|
});
|
|
22755
23086
|
const apiHost = process.env.POOL_API_HOST ?? "::";
|
|
22756
|
-
await new Promise((
|
|
23087
|
+
await new Promise((resolve9) => {
|
|
22757
23088
|
server.listen(apiPort, apiHost, () => {
|
|
22758
23089
|
log(`listening on ${apiHost}:${apiPort} (min=${min}, app=${app}, region=${region})`);
|
|
22759
|
-
|
|
23090
|
+
resolve9();
|
|
22760
23091
|
});
|
|
22761
23092
|
});
|
|
22762
23093
|
const shutdown = (signal) => {
|
|
@@ -22795,17 +23126,17 @@ function authOk2(req, expected) {
|
|
|
22795
23126
|
return false;
|
|
22796
23127
|
}
|
|
22797
23128
|
function readJsonBody3(req) {
|
|
22798
|
-
return new Promise((
|
|
23129
|
+
return new Promise((resolve9, reject) => {
|
|
22799
23130
|
const chunks = [];
|
|
22800
23131
|
req.on("data", (c) => chunks.push(c));
|
|
22801
23132
|
req.on("end", () => {
|
|
22802
23133
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
22803
23134
|
if (!raw.trim()) {
|
|
22804
|
-
|
|
23135
|
+
resolve9({});
|
|
22805
23136
|
return;
|
|
22806
23137
|
}
|
|
22807
23138
|
try {
|
|
22808
|
-
|
|
23139
|
+
resolve9(JSON.parse(raw));
|
|
22809
23140
|
} catch (err) {
|
|
22810
23141
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
22811
23142
|
}
|
|
@@ -22824,8 +23155,8 @@ function parseJob(body) {
|
|
|
22824
23155
|
if (!jobId) return { error: "jobId required" };
|
|
22825
23156
|
const repo = typeof b.repo === "string" ? b.repo.trim() : "";
|
|
22826
23157
|
if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be 'owner/name'" };
|
|
22827
|
-
const
|
|
22828
|
-
if (!
|
|
23158
|
+
const githubToken2 = typeof b.githubToken === "string" ? b.githubToken.trim() : "";
|
|
23159
|
+
if (!githubToken2) return { error: "githubToken required" };
|
|
22829
23160
|
const action = typeof b.action === "string" && b.action.trim() ? b.action.trim() : void 0;
|
|
22830
23161
|
const message = typeof b.message === "string" && b.message.trim() ? b.message.trim() : void 0;
|
|
22831
23162
|
const mode = b.mode === "interactive" ? "interactive" : b.mode === "scheduled" ? "scheduled" : "issue";
|
|
@@ -22837,7 +23168,7 @@ function parseJob(body) {
|
|
|
22837
23168
|
message
|
|
22838
23169
|
});
|
|
22839
23170
|
if ("error" in runRequest) return { error: runRequest.error };
|
|
22840
|
-
const job = { jobId, repo, githubToken, runRequest: runRequest.request };
|
|
23171
|
+
const job = { jobId, repo, githubToken: githubToken2, runRequest: runRequest.request };
|
|
22841
23172
|
if (job.runRequest.target.type === "issue") {
|
|
22842
23173
|
job.issueNumber = job.runRequest.target.id;
|
|
22843
23174
|
} else if (job.runRequest.target.type === "chat") {
|
|
@@ -22939,13 +23270,13 @@ async function defaultRunJob(job) {
|
|
|
22939
23270
|
...interactive && job.idleExitMs ? { KODY_IDLE_EXIT_MS: String(job.idleExitMs) } : {},
|
|
22940
23271
|
...interactive && job.hardCapMs ? { KODY_HARD_CAP_MS: String(job.hardCapMs) } : {}
|
|
22941
23272
|
};
|
|
22942
|
-
const run = (cmd, args, cwd) => new Promise((
|
|
23273
|
+
const run = (cmd, args, cwd) => new Promise((resolve9) => {
|
|
22943
23274
|
const child = spawn8(cmd, args, { stdio: "inherit", env: childEnv, cwd });
|
|
22944
|
-
child.on("exit", (code) =>
|
|
23275
|
+
child.on("exit", (code) => resolve9(code ?? 0));
|
|
22945
23276
|
child.on("error", (err) => {
|
|
22946
23277
|
process.stderr.write(`[runner-serve] ${cmd} failed: ${err.message}
|
|
22947
23278
|
`);
|
|
22948
|
-
|
|
23279
|
+
resolve9(1);
|
|
22949
23280
|
});
|
|
22950
23281
|
});
|
|
22951
23282
|
process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
|
|
@@ -23021,11 +23352,11 @@ async function runnerServe() {
|
|
|
23021
23352
|
const port = Number(process.env.PORT ?? DEFAULT_PORT2);
|
|
23022
23353
|
const server = buildServer2({ apiKey });
|
|
23023
23354
|
const host = process.env.RUNNER_HOST ?? "::";
|
|
23024
|
-
await new Promise((
|
|
23355
|
+
await new Promise((resolve9) => {
|
|
23025
23356
|
server.listen(port, host, () => {
|
|
23026
23357
|
process.stdout.write(`[runner-serve] listening on ${host}:${port} (idle, awaiting job)
|
|
23027
23358
|
`);
|
|
23028
|
-
|
|
23359
|
+
resolve9();
|
|
23029
23360
|
});
|
|
23030
23361
|
});
|
|
23031
23362
|
const shutdown = (signal) => {
|
|
@@ -23094,14 +23425,14 @@ async function serve(opts) {
|
|
|
23094
23425
|
`);
|
|
23095
23426
|
const args = ["--dangerously-skip-permissions", "--model", model.model];
|
|
23096
23427
|
const child = spawn9("claude", args, { stdio: "inherit", env: editorEnv, cwd: opts.cwd });
|
|
23097
|
-
const exitCode = await new Promise((
|
|
23098
|
-
child.on("exit", (code) =>
|
|
23428
|
+
const exitCode = await new Promise((resolve9) => {
|
|
23429
|
+
child.on("exit", (code) => resolve9(code ?? 0));
|
|
23099
23430
|
child.on("error", (err) => {
|
|
23100
23431
|
process.stderr.write(`[kody serve] failed to launch Claude Code: ${err.message}
|
|
23101
23432
|
`);
|
|
23102
23433
|
process.stderr.write(` Install: https://docs.anthropic.com/claude/docs/claude-code
|
|
23103
23434
|
`);
|
|
23104
|
-
|
|
23435
|
+
resolve9(1);
|
|
23105
23436
|
});
|
|
23106
23437
|
});
|
|
23107
23438
|
killProxy();
|