@kody-ade/kody-engine 0.4.276 → 0.4.278
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 +616 -315
- package/package.json +1 -1
package/dist/bin/kody.js
CHANGED
|
@@ -15,7 +15,7 @@ var init_package = __esm({
|
|
|
15
15
|
"package.json"() {
|
|
16
16
|
package_default = {
|
|
17
17
|
name: "@kody-ade/kody-engine",
|
|
18
|
-
version: "0.4.
|
|
18
|
+
version: "0.4.278",
|
|
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",
|
|
@@ -156,14 +156,97 @@ function ghToken(preferRepoToken = false) {
|
|
|
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(
|
|
@@ -352,12 +438,25 @@ var init_issue = __esm({
|
|
|
352
438
|
}
|
|
353
439
|
});
|
|
354
440
|
|
|
441
|
+
// src/stateBranch.ts
|
|
442
|
+
var STATE_BRANCH;
|
|
443
|
+
var init_stateBranch = __esm({
|
|
444
|
+
"src/stateBranch.ts"() {
|
|
445
|
+
"use strict";
|
|
446
|
+
STATE_BRANCH = "kody-state";
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
355
450
|
// src/stateRepo.ts
|
|
356
451
|
import path2 from "path";
|
|
357
452
|
function is404(err) {
|
|
358
453
|
const msg = err instanceof Error ? err.message : String(err);
|
|
359
454
|
return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
|
|
360
455
|
}
|
|
456
|
+
function isAlreadyExists(err) {
|
|
457
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
458
|
+
return /HTTP 422/i.test(msg) || /Reference already exists/i.test(msg);
|
|
459
|
+
}
|
|
361
460
|
function parseStateRepoSlug(slug2, field = "stateRepo") {
|
|
362
461
|
const value = slug2.trim();
|
|
363
462
|
let repoPath = value;
|
|
@@ -428,11 +527,41 @@ function apiPath(config, targetPath) {
|
|
|
428
527
|
const parsed = parseStateRepo(config);
|
|
429
528
|
return `/repos/${parsed.owner}/${parsed.repo}/contents/${targetPath}`;
|
|
430
529
|
}
|
|
530
|
+
function branchApiPath(config, targetPath) {
|
|
531
|
+
return `${apiPath(config, targetPath)}?ref=${encodeURIComponent(STATE_BRANCH)}`;
|
|
532
|
+
}
|
|
533
|
+
function ensureStateBranch(config, cwd) {
|
|
534
|
+
const parsed = parseStateRepo(config);
|
|
535
|
+
const cacheKey2 = `${parsed.owner}/${parsed.repo}:${STATE_BRANCH}`;
|
|
536
|
+
if (ensuredStateBranches.has(cacheKey2)) return;
|
|
537
|
+
try {
|
|
538
|
+
gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
|
|
539
|
+
ensuredStateBranches.add(cacheKey2);
|
|
540
|
+
return;
|
|
541
|
+
} catch (err) {
|
|
542
|
+
if (!is404(err)) throw err;
|
|
543
|
+
}
|
|
544
|
+
const repoRaw = gh(["api", `/repos/${parsed.owner}/${parsed.repo}`], { cwd });
|
|
545
|
+
const defaultBranch = String(JSON.parse(repoRaw).default_branch ?? "").trim();
|
|
546
|
+
if (!defaultBranch) throw new Error(`stateRepo: ${parsed.owner}/${parsed.repo} default branch missing`);
|
|
547
|
+
const refRaw = gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${defaultBranch}`], { cwd });
|
|
548
|
+
const sha = String(JSON.parse(refRaw).object?.sha ?? "").trim();
|
|
549
|
+
if (!sha) throw new Error(`stateRepo: ${parsed.owner}/${parsed.repo} ${defaultBranch} ref sha missing`);
|
|
550
|
+
try {
|
|
551
|
+
gh(["api", "--method", "POST", `/repos/${parsed.owner}/${parsed.repo}/git/refs`, "--input", "-"], {
|
|
552
|
+
cwd,
|
|
553
|
+
input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
|
|
554
|
+
});
|
|
555
|
+
} catch (err) {
|
|
556
|
+
if (!isAlreadyExists(err)) throw err;
|
|
557
|
+
}
|
|
558
|
+
ensuredStateBranches.add(cacheKey2);
|
|
559
|
+
}
|
|
431
560
|
function readStateText(config, cwd, filePath) {
|
|
432
561
|
const targetPath = stateRepoPath(config, filePath);
|
|
433
562
|
let raw = "";
|
|
434
563
|
try {
|
|
435
|
-
raw = gh(["api",
|
|
564
|
+
raw = gh(["api", branchApiPath(config, targetPath)], { cwd });
|
|
436
565
|
} catch (err) {
|
|
437
566
|
if (is404(err)) return null;
|
|
438
567
|
throw err;
|
|
@@ -457,9 +586,11 @@ function readStateText(config, cwd, filePath) {
|
|
|
457
586
|
}
|
|
458
587
|
function writeStateText(config, cwd, filePath, content, message, sha) {
|
|
459
588
|
const targetPath = stateRepoPath(config, filePath);
|
|
589
|
+
ensureStateBranch(config, cwd);
|
|
460
590
|
const payload = {
|
|
461
591
|
message,
|
|
462
|
-
content: Buffer.from(content, "utf-8").toString("base64")
|
|
592
|
+
content: Buffer.from(content, "utf-8").toString("base64"),
|
|
593
|
+
branch: STATE_BRANCH
|
|
463
594
|
};
|
|
464
595
|
if (sha) payload.sha = sha;
|
|
465
596
|
gh(["api", "--method", "PUT", apiPath(config, targetPath), "--input", "-"], {
|
|
@@ -490,7 +621,7 @@ function listStateDirectory(config, cwd, dirPath) {
|
|
|
490
621
|
const targetPath = stateRepoPath(config, dirPath);
|
|
491
622
|
let raw = "";
|
|
492
623
|
try {
|
|
493
|
-
raw = gh(["api",
|
|
624
|
+
raw = gh(["api", branchApiPath(config, targetPath)], { cwd });
|
|
494
625
|
} catch (err) {
|
|
495
626
|
if (is404(err)) return [];
|
|
496
627
|
throw err;
|
|
@@ -498,10 +629,13 @@ function listStateDirectory(config, cwd, dirPath) {
|
|
|
498
629
|
const parsed = JSON.parse(raw);
|
|
499
630
|
return Array.isArray(parsed) ? parsed : [];
|
|
500
631
|
}
|
|
632
|
+
var ensuredStateBranches;
|
|
501
633
|
var init_stateRepo = __esm({
|
|
502
634
|
"src/stateRepo.ts"() {
|
|
503
635
|
"use strict";
|
|
504
636
|
init_issue();
|
|
637
|
+
init_stateBranch();
|
|
638
|
+
ensuredStateBranches = /* @__PURE__ */ new Set();
|
|
505
639
|
}
|
|
506
640
|
});
|
|
507
641
|
|
|
@@ -1356,18 +1490,312 @@ function buildSubmitMcpServer() {
|
|
|
1356
1490
|
});
|
|
1357
1491
|
return { server, getSubmitted: handle.getSubmitted };
|
|
1358
1492
|
}
|
|
1359
|
-
var DESCRIPTION2, INPUT_SCHEMA2;
|
|
1360
|
-
var init_submitMcp = __esm({
|
|
1361
|
-
"src/submitMcp.ts"() {
|
|
1493
|
+
var DESCRIPTION2, INPUT_SCHEMA2;
|
|
1494
|
+
var init_submitMcp = __esm({
|
|
1495
|
+
"src/submitMcp.ts"() {
|
|
1496
|
+
"use strict";
|
|
1497
|
+
DESCRIPTION2 = "Persist this tick's next state. Call this EXACTLY ONCE, at the very end, when you've finished your work \u2014 it is the ONLY way your decision is saved. Pass your next `cursor` (string), your next `data` (object \u2014 carry prior data forward and mutate what you acted on), and `done` (boolean). After calling it you are finished; do not take further actions.";
|
|
1498
|
+
INPUT_SCHEMA2 = {
|
|
1499
|
+
cursor: z.string().describe('The next cursor value (e.g. "idle"). Must be a non-empty string.'),
|
|
1500
|
+
data: z.record(z.string(), z.unknown()).describe("The next `data` object. Carry forward prior data and mutate only what you acted on this tick."),
|
|
1501
|
+
done: z.boolean().describe(
|
|
1502
|
+
"true only if this capability is permanently finished; evergreen capabilities stay false."
|
|
1503
|
+
)
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
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"() {
|
|
1362
1791
|
"use strict";
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
};
|
|
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
|
+
];
|
|
1371
1799
|
}
|
|
1372
1800
|
});
|
|
1373
1801
|
|
|
@@ -1941,8 +2369,8 @@ __export(capabilityMcp_exports, {
|
|
|
1941
2369
|
readCheckRuns: () => readCheckRuns,
|
|
1942
2370
|
readThread: () => readThread
|
|
1943
2371
|
});
|
|
1944
|
-
import { createSdkMcpServer as
|
|
1945
|
-
import { z as
|
|
2372
|
+
import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
|
|
2373
|
+
import { z as z3 } from "zod";
|
|
1946
2374
|
function summarizeCiStatus(rollup) {
|
|
1947
2375
|
if (!Array.isArray(rollup) || rollup.length === 0) return "UNKNOWN";
|
|
1948
2376
|
let hasRunning = false;
|
|
@@ -2181,132 +2609,12 @@ function isDispatchGated(capability, mode) {
|
|
|
2181
2609
|
function trustRefusal(capabilitySlug) {
|
|
2182
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.`;
|
|
2183
2611
|
}
|
|
2184
|
-
function dashboardBaseUrl() {
|
|
2185
|
-
const raw = process.env.KODY_CMS_DASHBOARD_URL?.trim() || process.env.KODY_DASHBOARD_URL?.trim() || process.env.DASHBOARD_URL?.trim() || "";
|
|
2186
|
-
if (!raw) return null;
|
|
2187
|
-
try {
|
|
2188
|
-
const parsed = new URL(raw);
|
|
2189
|
-
return parsed.origin;
|
|
2190
|
-
} catch {
|
|
2191
|
-
return null;
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
function dashboardCmsToken() {
|
|
2195
|
-
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;
|
|
2196
|
-
}
|
|
2197
|
-
function cmsHeaders(opts) {
|
|
2198
|
-
const token = dashboardCmsToken();
|
|
2199
|
-
const [owner, repo] = opts.repoSlug.split("/");
|
|
2200
|
-
if (!token) {
|
|
2201
|
-
return {
|
|
2202
|
-
ok: false,
|
|
2203
|
-
error: "missing_cms_token",
|
|
2204
|
-
message: "Set KODY_CMS_TOKEN, KODY_DASHBOARD_TOKEN, KODY_TOKEN, GH_TOKEN, GITHUB_TOKEN, or GH_PAT."
|
|
2205
|
-
};
|
|
2206
|
-
}
|
|
2207
|
-
if (!owner || !repo) {
|
|
2208
|
-
return { ok: false, error: "invalid_repo", message: `Invalid repo slug: ${opts.repoSlug}` };
|
|
2209
|
-
}
|
|
2210
|
-
return {
|
|
2211
|
-
ok: true,
|
|
2212
|
-
headers: {
|
|
2213
|
-
"Content-Type": "application/json",
|
|
2214
|
-
"x-kody-token": token,
|
|
2215
|
-
"x-kody-owner": owner,
|
|
2216
|
-
"x-kody-repo": repo
|
|
2217
|
-
}
|
|
2218
|
-
};
|
|
2219
|
-
}
|
|
2220
|
-
async function callDashboardCms(opts, path50, init = {}) {
|
|
2221
|
-
const baseUrl = dashboardBaseUrl();
|
|
2222
|
-
if (!baseUrl) {
|
|
2223
|
-
return {
|
|
2224
|
-
ok: false,
|
|
2225
|
-
error: "missing_dashboard_url",
|
|
2226
|
-
message: "Set KODY_CMS_DASHBOARD_URL or KODY_DASHBOARD_URL to the Dashboard origin."
|
|
2227
|
-
};
|
|
2228
|
-
}
|
|
2229
|
-
const headerResult = cmsHeaders(opts);
|
|
2230
|
-
if (!headerResult.ok) return headerResult;
|
|
2231
|
-
try {
|
|
2232
|
-
const res = await fetch(`${baseUrl}${path50}`, {
|
|
2233
|
-
...init,
|
|
2234
|
-
headers: {
|
|
2235
|
-
...headerResult.headers,
|
|
2236
|
-
...init.headers
|
|
2237
|
-
}
|
|
2238
|
-
});
|
|
2239
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
2240
|
-
const data = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => "");
|
|
2241
|
-
if (!res.ok) {
|
|
2242
|
-
return {
|
|
2243
|
-
ok: false,
|
|
2244
|
-
status: res.status,
|
|
2245
|
-
error: stringFieldFromRecord(data, "error") ?? "cms_request_failed",
|
|
2246
|
-
message: stringFieldFromRecord(data, "message") ?? `Dashboard CMS request failed with ${res.status}.`,
|
|
2247
|
-
data
|
|
2248
|
-
};
|
|
2249
|
-
}
|
|
2250
|
-
return { ok: true, status: res.status, data };
|
|
2251
|
-
} catch (err) {
|
|
2252
|
-
return {
|
|
2253
|
-
ok: false,
|
|
2254
|
-
error: "cms_request_error",
|
|
2255
|
-
message: err instanceof Error ? err.message : String(err)
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
function cmsToolResponse(result) {
|
|
2260
|
-
return {
|
|
2261
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2262
|
-
...result.ok ? {} : { isError: true }
|
|
2263
|
-
};
|
|
2264
|
-
}
|
|
2265
2612
|
function assertCmsWriteAllowed(opts) {
|
|
2266
2613
|
if (isDispatchGated(opts.capabilitySlug, readCapabilityTrustMode(opts.state, opts.repoSlug, opts.capabilitySlug))) {
|
|
2267
2614
|
return trustRefusal(opts.capabilitySlug);
|
|
2268
2615
|
}
|
|
2269
2616
|
return null;
|
|
2270
2617
|
}
|
|
2271
|
-
function cmsQuery(args) {
|
|
2272
|
-
const params = new URLSearchParams();
|
|
2273
|
-
const q = stringArg(args.q);
|
|
2274
|
-
if (q) params.set("q", q);
|
|
2275
|
-
const limit = numberArg(args.limit);
|
|
2276
|
-
if (limit !== void 0) params.set("limit", String(limit));
|
|
2277
|
-
const offset = numberArg(args.offset);
|
|
2278
|
-
if (offset !== void 0) params.set("offset", String(offset));
|
|
2279
|
-
if (args.filters && typeof args.filters === "object" && !Array.isArray(args.filters)) {
|
|
2280
|
-
params.set("filters", JSON.stringify(args.filters));
|
|
2281
|
-
}
|
|
2282
|
-
if (Array.isArray(args.sort)) {
|
|
2283
|
-
const sort = args.sort.flatMap((entry) => {
|
|
2284
|
-
if (!entry || typeof entry !== "object") return [];
|
|
2285
|
-
const field = stringArg(entry.field);
|
|
2286
|
-
if (!field) return [];
|
|
2287
|
-
const direction = entry.direction === "asc" ? "asc" : "desc";
|
|
2288
|
-
return [`${field}:${direction}`];
|
|
2289
|
-
}).join(",");
|
|
2290
|
-
if (sort) params.set("sort", sort);
|
|
2291
|
-
}
|
|
2292
|
-
const value = params.toString();
|
|
2293
|
-
return value ? `?${value}` : "";
|
|
2294
|
-
}
|
|
2295
|
-
function stringArg(value) {
|
|
2296
|
-
return typeof value === "string" ? value.trim() : "";
|
|
2297
|
-
}
|
|
2298
|
-
function numberArg(value) {
|
|
2299
|
-
if (value === void 0 || value === null || value === "") return void 0;
|
|
2300
|
-
const n = Number(value);
|
|
2301
|
-
return Number.isFinite(n) ? n : void 0;
|
|
2302
|
-
}
|
|
2303
|
-
function documentArg(value) {
|
|
2304
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
2305
|
-
return value;
|
|
2306
|
-
}
|
|
2307
|
-
function stringFieldFromRecord(value, field) {
|
|
2308
|
-
return value && typeof value === "object" && !Array.isArray(value) && typeof value[field] === "string" ? String(value[field]) : void 0;
|
|
2309
|
-
}
|
|
2310
2618
|
function capabilityToolDefinitions(opts) {
|
|
2311
2619
|
const workflowFile = opts.workflowFile ?? "kody.yml";
|
|
2312
2620
|
const listTool = {
|
|
@@ -2329,7 +2637,7 @@ function capabilityToolDefinitions(opts) {
|
|
|
2329
2637
|
name: `${verb.replace("-", "_")}_pr`,
|
|
2330
2638
|
description: describe,
|
|
2331
2639
|
inputSchema: {
|
|
2332
|
-
pr:
|
|
2640
|
+
pr: z3.number().int().positive().describe("PR number to repair.")
|
|
2333
2641
|
},
|
|
2334
2642
|
handler: async (args) => {
|
|
2335
2643
|
const pr = Number(args.pr);
|
|
@@ -2357,8 +2665,8 @@ function capabilityToolDefinitions(opts) {
|
|
|
2357
2665
|
name: "recommend_to_operator",
|
|
2358
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.",
|
|
2359
2667
|
inputSchema: {
|
|
2360
|
-
pr:
|
|
2361
|
-
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.")
|
|
2362
2670
|
},
|
|
2363
2671
|
handler: async (args) => {
|
|
2364
2672
|
const pr = Number(args.pr);
|
|
@@ -2372,7 +2680,7 @@ function capabilityToolDefinitions(opts) {
|
|
|
2372
2680
|
name: "read_ledger",
|
|
2373
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.",
|
|
2374
2682
|
inputSchema: {
|
|
2375
|
-
label:
|
|
2683
|
+
label: z3.string().min(1).describe("GitHub issue label that identifies the manifest issue.")
|
|
2376
2684
|
},
|
|
2377
2685
|
handler: async (args) => {
|
|
2378
2686
|
const label = String(args.label ?? "");
|
|
@@ -2384,8 +2692,8 @@ function capabilityToolDefinitions(opts) {
|
|
|
2384
2692
|
name: "read_check_runs",
|
|
2385
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.",
|
|
2386
2694
|
inputSchema: {
|
|
2387
|
-
ref:
|
|
2388
|
-
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).")
|
|
2389
2697
|
},
|
|
2390
2698
|
handler: async (args) => {
|
|
2391
2699
|
const ref = String(args.ref ?? "");
|
|
@@ -2398,8 +2706,8 @@ function capabilityToolDefinitions(opts) {
|
|
|
2398
2706
|
name: "read_thread",
|
|
2399
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.",
|
|
2400
2708
|
inputSchema: {
|
|
2401
|
-
number:
|
|
2402
|
-
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).")
|
|
2403
2711
|
},
|
|
2404
2712
|
handler: async (args) => {
|
|
2405
2713
|
const number = Number(args.number);
|
|
@@ -2412,11 +2720,11 @@ function capabilityToolDefinitions(opts) {
|
|
|
2412
2720
|
name: "ensure_issue",
|
|
2413
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.",
|
|
2414
2722
|
inputSchema: {
|
|
2415
|
-
key:
|
|
2723
|
+
key: z3.string().min(1).describe(
|
|
2416
2724
|
"Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."
|
|
2417
2725
|
),
|
|
2418
|
-
title:
|
|
2419
|
-
body:
|
|
2726
|
+
title: z3.string().min(1).describe("Issue title (used only on first creation)."),
|
|
2727
|
+
body: z3.string().min(1).describe(
|
|
2420
2728
|
"Issue body markdown (used only on first creation). Include the operator mention verbatim if the capability body has one."
|
|
2421
2729
|
)
|
|
2422
2730
|
},
|
|
@@ -2432,9 +2740,9 @@ function capabilityToolDefinitions(opts) {
|
|
|
2432
2740
|
name: "ensure_comment",
|
|
2433
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.",
|
|
2434
2742
|
inputSchema: {
|
|
2435
|
-
issue:
|
|
2436
|
-
key:
|
|
2437
|
-
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.")
|
|
2438
2746
|
},
|
|
2439
2747
|
handler: async (args) => {
|
|
2440
2748
|
const issue = Number(args.issue);
|
|
@@ -2448,9 +2756,9 @@ function capabilityToolDefinitions(opts) {
|
|
|
2448
2756
|
name: "dispatch_workflow",
|
|
2449
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}.",
|
|
2450
2758
|
inputSchema: {
|
|
2451
|
-
capability:
|
|
2452
|
-
executable:
|
|
2453
|
-
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.")
|
|
2454
2762
|
},
|
|
2455
2763
|
handler: async (args) => {
|
|
2456
2764
|
const capability = String(args.capability ?? args.executable ?? "");
|
|
@@ -2463,86 +2771,10 @@ function capabilityToolDefinitions(opts) {
|
|
|
2463
2771
|
return { content: [{ type: "text", text }] };
|
|
2464
2772
|
}
|
|
2465
2773
|
};
|
|
2466
|
-
const
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
handler: async () => cmsToolResponse(await callDashboardCms(opts, "/api/kody/cms"))
|
|
2471
|
-
};
|
|
2472
|
-
const cmsListDocumentsTool = {
|
|
2473
|
-
name: "cms_list_documents",
|
|
2474
|
-
description: "List or search Dashboard CMS documents from one configured collection. Read-only.",
|
|
2475
|
-
inputSchema: {
|
|
2476
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2477
|
-
q: z2.string().optional().describe("Optional search query."),
|
|
2478
|
-
filters: z2.record(z2.string(), z2.unknown()).optional().describe("Optional filter object keyed by field."),
|
|
2479
|
-
sort: z2.array(
|
|
2480
|
-
z2.object({
|
|
2481
|
-
field: z2.string().min(1),
|
|
2482
|
-
direction: z2.enum(["asc", "desc"]).default("desc")
|
|
2483
|
-
})
|
|
2484
|
-
).optional(),
|
|
2485
|
-
limit: z2.number().int().min(1).max(100).optional(),
|
|
2486
|
-
offset: z2.number().int().min(0).optional()
|
|
2487
|
-
},
|
|
2488
|
-
handler: async (args) => {
|
|
2489
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2490
|
-
return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}${cmsQuery(args)}`));
|
|
2491
|
-
}
|
|
2492
|
-
};
|
|
2493
|
-
const cmsGetDocumentTool = {
|
|
2494
|
-
name: "cms_get_document",
|
|
2495
|
-
description: "Get one Dashboard CMS document by collection and id. Read-only.",
|
|
2496
|
-
inputSchema: {
|
|
2497
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2498
|
-
id: z2.string().min(1).describe("Document id.")
|
|
2499
|
-
},
|
|
2500
|
-
handler: async (args) => {
|
|
2501
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2502
|
-
const id = encodeURIComponent(stringArg(args.id));
|
|
2503
|
-
return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`));
|
|
2504
|
-
}
|
|
2505
|
-
};
|
|
2506
|
-
const cmsCreateDocumentTool = {
|
|
2507
|
-
name: "cms_create_document",
|
|
2508
|
-
description: "Create one Dashboard CMS document when the capability is trusted and the CMS collection allows create.",
|
|
2509
|
-
inputSchema: {
|
|
2510
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2511
|
-
data: z2.record(z2.string(), z2.unknown()).describe("Document fields to create.")
|
|
2512
|
-
},
|
|
2513
|
-
handler: async (args) => {
|
|
2514
|
-
const refusal = assertCmsWriteAllowed(opts);
|
|
2515
|
-
if (refusal) return { content: [{ type: "text", text: refusal }] };
|
|
2516
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2517
|
-
return cmsToolResponse(
|
|
2518
|
-
await callDashboardCms(opts, `/api/kody/cms/${collection}`, {
|
|
2519
|
-
method: "POST",
|
|
2520
|
-
body: JSON.stringify(documentArg(args.data))
|
|
2521
|
-
})
|
|
2522
|
-
);
|
|
2523
|
-
}
|
|
2524
|
-
};
|
|
2525
|
-
const cmsUpdateDocumentTool = {
|
|
2526
|
-
name: "cms_update_document",
|
|
2527
|
-
description: "Update one Dashboard CMS document when the capability is trusted and the CMS collection allows update.",
|
|
2528
|
-
inputSchema: {
|
|
2529
|
-
collection: z2.string().min(1).describe("CMS collection name."),
|
|
2530
|
-
id: z2.string().min(1).describe("Document id."),
|
|
2531
|
-
data: z2.record(z2.string(), z2.unknown()).describe("Partial document fields to update.")
|
|
2532
|
-
},
|
|
2533
|
-
handler: async (args) => {
|
|
2534
|
-
const refusal = assertCmsWriteAllowed(opts);
|
|
2535
|
-
if (refusal) return { content: [{ type: "text", text: refusal }] };
|
|
2536
|
-
const collection = encodeURIComponent(stringArg(args.collection));
|
|
2537
|
-
const id = encodeURIComponent(stringArg(args.id));
|
|
2538
|
-
return cmsToolResponse(
|
|
2539
|
-
await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`, {
|
|
2540
|
-
method: "PATCH",
|
|
2541
|
-
body: JSON.stringify(documentArg(args.data))
|
|
2542
|
-
})
|
|
2543
|
-
);
|
|
2544
|
-
}
|
|
2545
|
-
};
|
|
2774
|
+
const cmsTools = dashboardCmsToolDefinitions({
|
|
2775
|
+
repoSlug: opts.repoSlug,
|
|
2776
|
+
assertWriteAllowed: () => assertCmsWriteAllowed(opts)
|
|
2777
|
+
});
|
|
2546
2778
|
return [
|
|
2547
2779
|
listTool,
|
|
2548
2780
|
syncTool,
|
|
@@ -2555,24 +2787,20 @@ function capabilityToolDefinitions(opts) {
|
|
|
2555
2787
|
ensureIssueTool,
|
|
2556
2788
|
ensureCommentTool,
|
|
2557
2789
|
dispatchTool,
|
|
2558
|
-
|
|
2559
|
-
cmsListDocumentsTool,
|
|
2560
|
-
cmsGetDocumentTool,
|
|
2561
|
-
cmsCreateDocumentTool,
|
|
2562
|
-
cmsUpdateDocumentTool
|
|
2790
|
+
...cmsTools
|
|
2563
2791
|
];
|
|
2564
2792
|
}
|
|
2565
2793
|
function buildCapabilityMcpServer(opts) {
|
|
2566
2794
|
const definitions = capabilityToolDefinitions(opts);
|
|
2567
2795
|
const tools = definitions.map(
|
|
2568
|
-
(def) =>
|
|
2796
|
+
(def) => tool4(
|
|
2569
2797
|
def.name,
|
|
2570
2798
|
def.description,
|
|
2571
2799
|
def.inputSchema,
|
|
2572
2800
|
async (args) => def.handler(args)
|
|
2573
2801
|
)
|
|
2574
2802
|
);
|
|
2575
|
-
const server =
|
|
2803
|
+
const server = createSdkMcpServer4({
|
|
2576
2804
|
name: "kody-capability",
|
|
2577
2805
|
version: "0.1.0",
|
|
2578
2806
|
tools
|
|
@@ -2583,6 +2811,7 @@ var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, THREAD_BODY_MAX, CHECK_
|
|
|
2583
2811
|
var init_capabilityMcp = __esm({
|
|
2584
2812
|
"src/capabilityMcp.ts"() {
|
|
2585
2813
|
"use strict";
|
|
2814
|
+
init_dashboardCmsMcp();
|
|
2586
2815
|
init_issue();
|
|
2587
2816
|
init_registry();
|
|
2588
2817
|
init_stateRepo();
|
|
@@ -2607,11 +2836,7 @@ var init_capabilityMcp = __esm({
|
|
|
2607
2836
|
"ensure_issue",
|
|
2608
2837
|
"ensure_comment",
|
|
2609
2838
|
"dispatch_workflow",
|
|
2610
|
-
|
|
2611
|
-
"cms_list_documents",
|
|
2612
|
-
"cms_get_document",
|
|
2613
|
-
"cms_create_document",
|
|
2614
|
-
"cms_update_document"
|
|
2839
|
+
...DASHBOARD_CMS_MCP_TOOL_NAMES
|
|
2615
2840
|
];
|
|
2616
2841
|
}
|
|
2617
2842
|
});
|
|
@@ -2689,8 +2914,8 @@ __export(fetchRepoMcp_exports, {
|
|
|
2689
2914
|
buildFetchRepoMcpServer: () => buildFetchRepoMcpServer,
|
|
2690
2915
|
fetchRepoToolDefinition: () => fetchRepoToolDefinition
|
|
2691
2916
|
});
|
|
2692
|
-
import { createSdkMcpServer as
|
|
2693
|
-
import { z as
|
|
2917
|
+
import { createSdkMcpServer as createSdkMcpServer5, tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
|
|
2918
|
+
import { z as z4 } from "zod";
|
|
2694
2919
|
function fetchRepoToolDefinition(opts) {
|
|
2695
2920
|
return {
|
|
2696
2921
|
name: "fetch_repo",
|
|
@@ -2725,10 +2950,10 @@ Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in you
|
|
|
2725
2950
|
}
|
|
2726
2951
|
function buildFetchRepoMcpServer(opts) {
|
|
2727
2952
|
const def = fetchRepoToolDefinition(opts);
|
|
2728
|
-
const fetchTool =
|
|
2953
|
+
const fetchTool = tool5(def.name, def.description, def.inputSchema, async (args) => {
|
|
2729
2954
|
return def.handler({ repo: String(args.repo ?? "") });
|
|
2730
2955
|
});
|
|
2731
|
-
return
|
|
2956
|
+
return createSdkMcpServer5({
|
|
2732
2957
|
name: "kody-fetch-repo",
|
|
2733
2958
|
version: "0.1.0",
|
|
2734
2959
|
tools: [fetchTool]
|
|
@@ -2741,7 +2966,7 @@ var init_fetchRepoMcp = __esm({
|
|
|
2741
2966
|
init_repoWorkspace();
|
|
2742
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.';
|
|
2743
2968
|
INPUT_SCHEMA3 = {
|
|
2744
|
-
repo:
|
|
2969
|
+
repo: z4.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
|
|
2745
2970
|
};
|
|
2746
2971
|
}
|
|
2747
2972
|
});
|
|
@@ -2825,7 +3050,7 @@ async function runAgent(opts) {
|
|
|
2825
3050
|
const turnTimeoutMs = resolveTurnTimeoutMs(opts);
|
|
2826
3051
|
let outcome = "failed";
|
|
2827
3052
|
let outcomeKind = "generic_failed";
|
|
2828
|
-
let
|
|
3053
|
+
let errorMessage2;
|
|
2829
3054
|
let tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
2830
3055
|
let messageCount = 0;
|
|
2831
3056
|
let finalText = "";
|
|
@@ -2841,7 +3066,7 @@ async function runAgent(opts) {
|
|
|
2841
3066
|
const resultTexts = [];
|
|
2842
3067
|
outcome = "failed";
|
|
2843
3068
|
outcomeKind = "generic_failed";
|
|
2844
|
-
|
|
3069
|
+
errorMessage2 = void 0;
|
|
2845
3070
|
tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
2846
3071
|
messageCount = 0;
|
|
2847
3072
|
let sawMutatingTool = false;
|
|
@@ -2899,6 +3124,24 @@ async function runAgent(opts) {
|
|
|
2899
3124
|
});
|
|
2900
3125
|
mcpEntries.push(["kody-capability", dutyHandle.server]);
|
|
2901
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
|
+
}
|
|
2902
3145
|
if (opts.enableFetchRepoTool && opts.reposRoot) {
|
|
2903
3146
|
const { buildFetchRepoMcpServer: buildFetchRepoMcpServer2 } = await Promise.resolve().then(() => (init_fetchRepoMcp(), fetchRepoMcp_exports));
|
|
2904
3147
|
const fetchServer = buildFetchRepoMcpServer2({
|
|
@@ -2979,7 +3222,7 @@ async function runAgent(opts) {
|
|
|
2979
3222
|
if (timedOut) {
|
|
2980
3223
|
outcome = "failed";
|
|
2981
3224
|
outcomeKind = "stalled";
|
|
2982
|
-
|
|
3225
|
+
errorMessage2 = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
|
|
2983
3226
|
if (typeof iterator.return === "function") {
|
|
2984
3227
|
try {
|
|
2985
3228
|
await iterator.return(void 0);
|
|
@@ -3083,17 +3326,17 @@ async function runAgent(opts) {
|
|
|
3083
3326
|
} else {
|
|
3084
3327
|
outcome = "failed";
|
|
3085
3328
|
outcomeKind = classifySubtype(m.subtype);
|
|
3086
|
-
|
|
3329
|
+
errorMessage2 = `result subtype: ${m.subtype ?? "unknown"}`;
|
|
3087
3330
|
}
|
|
3088
3331
|
}
|
|
3089
3332
|
}
|
|
3090
3333
|
} catch (e) {
|
|
3091
3334
|
if (sawTerminalSuccess) {
|
|
3092
|
-
|
|
3335
|
+
errorMessage2 = e instanceof Error ? e.message : String(e);
|
|
3093
3336
|
} else {
|
|
3094
3337
|
outcome = "failed";
|
|
3095
3338
|
outcomeKind = "model_error";
|
|
3096
|
-
|
|
3339
|
+
errorMessage2 = e instanceof Error ? e.message : String(e);
|
|
3097
3340
|
}
|
|
3098
3341
|
} finally {
|
|
3099
3342
|
try {
|
|
@@ -3111,7 +3354,7 @@ async function runAgent(opts) {
|
|
|
3111
3354
|
if (outcome === "completed" && sawLoginRequired) {
|
|
3112
3355
|
outcome = "failed";
|
|
3113
3356
|
outcomeKind = "model_error";
|
|
3114
|
-
|
|
3357
|
+
errorMessage2 = "Claude Code reported it is not logged in; refusing to mark agent run successful";
|
|
3115
3358
|
}
|
|
3116
3359
|
if (outcome === "completed" && !sawMutatingTool) {
|
|
3117
3360
|
const backendDead = opts.isBackendHealthy ? !await opts.isBackendHealthy() : false;
|
|
@@ -3120,14 +3363,14 @@ async function runAgent(opts) {
|
|
|
3120
3363
|
outcome = "failed";
|
|
3121
3364
|
outcomeKind = "model_error";
|
|
3122
3365
|
noWorkSuccess = true;
|
|
3123
|
-
|
|
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");
|
|
3124
3367
|
}
|
|
3125
3368
|
}
|
|
3126
|
-
const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(
|
|
3369
|
+
const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(errorMessage2) || noWorkSuccess);
|
|
3127
3370
|
if (!shouldRetry) break;
|
|
3128
3371
|
const delayMs = CONNECTION_RETRY_BASE_MS * 2 ** attempt;
|
|
3129
3372
|
process.stderr.write(
|
|
3130
|
-
`[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}
|
|
3131
3374
|
`
|
|
3132
3375
|
);
|
|
3133
3376
|
if (opts.ensureBackend) {
|
|
@@ -3146,7 +3389,7 @@ async function runAgent(opts) {
|
|
|
3146
3389
|
outcomeKind,
|
|
3147
3390
|
finalText,
|
|
3148
3391
|
...submittedState ? { submittedState } : {},
|
|
3149
|
-
error:
|
|
3392
|
+
error: errorMessage2,
|
|
3150
3393
|
ndjsonPath,
|
|
3151
3394
|
durationMs: Date.now() - startedAt,
|
|
3152
3395
|
tokens,
|
|
@@ -5771,7 +6014,7 @@ var init_litellm = __esm({
|
|
|
5771
6014
|
|
|
5772
6015
|
// src/pushWithRetry.ts
|
|
5773
6016
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
5774
|
-
function
|
|
6017
|
+
function sleepSync2(ms) {
|
|
5775
6018
|
if (ms <= 0) return;
|
|
5776
6019
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5777
6020
|
}
|
|
@@ -5832,7 +6075,7 @@ function pushWithRetry(opts = {}) {
|
|
|
5832
6075
|
};
|
|
5833
6076
|
}
|
|
5834
6077
|
const delay = Math.min(baseBackoff * 2 ** (attempt - 1), MAX_BACKOFF_MS);
|
|
5835
|
-
|
|
6078
|
+
sleepSync2(delay);
|
|
5836
6079
|
}
|
|
5837
6080
|
return {
|
|
5838
6081
|
ok: false,
|
|
@@ -6077,6 +6320,13 @@ function jobMetaFromData(data) {
|
|
|
6077
6320
|
why: typeof data.jobWhy === "string" ? data.jobWhy : void 0
|
|
6078
6321
|
};
|
|
6079
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
|
+
}
|
|
6080
6330
|
function synthesizeAction(ctx) {
|
|
6081
6331
|
const ok = ctx.output.exitCode === 0;
|
|
6082
6332
|
return {
|
|
@@ -6107,6 +6357,7 @@ var init_saveTaskState = __esm({
|
|
|
6107
6357
|
});
|
|
6108
6358
|
if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
|
|
6109
6359
|
if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
|
|
6360
|
+
applyStandaloneTerminalState(next, ctx, profile);
|
|
6110
6361
|
writeTaskState(target, number, next, ctx.cwd, ctx.config);
|
|
6111
6362
|
ctx.data.taskState = next;
|
|
6112
6363
|
ctx.data.taskStateRendered = renderStateComment(next);
|
|
@@ -11436,12 +11687,9 @@ var init_finalizeTerminal = __esm({
|
|
|
11436
11687
|
const issueNumber = ctx.args.issue;
|
|
11437
11688
|
const targetNumber = ctx.data.commentTargetNumber ?? issueNumber;
|
|
11438
11689
|
if (!targetNumber) return;
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
} catch {
|
|
11443
|
-
prUrl = void 0;
|
|
11444
|
-
}
|
|
11690
|
+
const cachedState = ctx.data.taskState;
|
|
11691
|
+
let state = cachedState;
|
|
11692
|
+
const prUrl = cachedState?.core.prUrl ?? ctx.output.prUrl ?? ctx.data.prResult?.url;
|
|
11445
11693
|
const delivered = ctx.output.exitCode === 0 && !!prUrl;
|
|
11446
11694
|
const spec = delivered ? DONE : FAILED;
|
|
11447
11695
|
const phase = delivered ? "shipped" : "failed";
|
|
@@ -11449,12 +11697,28 @@ var init_finalizeTerminal = __esm({
|
|
|
11449
11697
|
if (issueNumber) setKodyLabel(issueNumber, spec, ctx.cwd);
|
|
11450
11698
|
const prNumber = prUrl ? parsePrNumber(prUrl) : null;
|
|
11451
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;
|
|
11452
11720
|
try {
|
|
11453
|
-
|
|
11454
|
-
state.core.phase = phase;
|
|
11455
|
-
state.core.status = status;
|
|
11456
|
-
state.core.currentExecutable = null;
|
|
11457
|
-
writeTaskState(target, targetNumber, state, ctx.cwd, ctx.config);
|
|
11721
|
+
writeTaskState(target, targetNumber, next, ctx.cwd, ctx.config);
|
|
11458
11722
|
} catch (err) {
|
|
11459
11723
|
process.stderr.write(
|
|
11460
11724
|
`[kody finalizeTerminal] failed to write terminal state on ${target} #${targetNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -13119,17 +13383,22 @@ var init_mirrorStateToPr = __esm({
|
|
|
13119
13383
|
const issueNumber = ctx.data.commentTargetNumber;
|
|
13120
13384
|
const issueTarget = ctx.data.commentTargetType;
|
|
13121
13385
|
if (!issueNumber || issueTarget !== "issue") return;
|
|
13122
|
-
|
|
13386
|
+
let state = ctx.data.taskState;
|
|
13387
|
+
const prUrl = ctx.output.prUrl ?? ctx.data.prResult?.url ?? state?.core.prUrl;
|
|
13123
13388
|
if (!prUrl) return;
|
|
13124
13389
|
const prNumber = parsePrNumber4(prUrl);
|
|
13125
13390
|
if (!prNumber) return;
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13130
|
-
|
|
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;
|
|
13131
13401
|
}
|
|
13132
|
-
if (prUrl && !state.core.prUrl) state.core.prUrl = prUrl;
|
|
13133
13402
|
try {
|
|
13134
13403
|
writeTaskState("pr", prNumber, state, ctx.cwd, ctx.config);
|
|
13135
13404
|
} catch (err) {
|
|
@@ -17093,24 +17362,24 @@ function firstRequiredFailure(results, tools) {
|
|
|
17093
17362
|
}
|
|
17094
17363
|
return null;
|
|
17095
17364
|
}
|
|
17096
|
-
function verifyOne(
|
|
17097
|
-
const result = { name:
|
|
17098
|
-
const checkRes = runShell(
|
|
17365
|
+
function verifyOne(tool6, cwd) {
|
|
17366
|
+
const result = { name: tool6.name, present: false, verified: false };
|
|
17367
|
+
const checkRes = runShell(tool6.install.checkCommand, cwd);
|
|
17099
17368
|
let present = checkRes.ok;
|
|
17100
|
-
if (!present &&
|
|
17101
|
-
runShell(
|
|
17102
|
-
present = runShell(
|
|
17369
|
+
if (!present && tool6.install.installCommand) {
|
|
17370
|
+
runShell(tool6.install.installCommand, cwd, 12e4);
|
|
17371
|
+
present = runShell(tool6.install.checkCommand, cwd).ok;
|
|
17103
17372
|
}
|
|
17104
17373
|
result.present = present;
|
|
17105
17374
|
if (!present) {
|
|
17106
|
-
result.error = `tool "${
|
|
17375
|
+
result.error = `tool "${tool6.name}" not on PATH (check: ${tool6.install.checkCommand})`;
|
|
17107
17376
|
return result;
|
|
17108
17377
|
}
|
|
17109
|
-
const verifyRes = runShell(
|
|
17378
|
+
const verifyRes = runShell(tool6.verify, cwd);
|
|
17110
17379
|
result.verified = verifyRes.ok;
|
|
17111
17380
|
if (!verifyRes.ok) {
|
|
17112
17381
|
const tail = formatStderrTail(verifyRes.stderr, verifyRes.stdout);
|
|
17113
|
-
result.error = `tool "${
|
|
17382
|
+
result.error = `tool "${tool6.name}" failed verify: ${tool6.verify}${tail ? ` \u2014 ${tail}` : ""}`;
|
|
17114
17383
|
}
|
|
17115
17384
|
return result;
|
|
17116
17385
|
}
|
|
@@ -18822,6 +19091,12 @@ var CROSS_REPO_PROMPT = [
|
|
|
18822
19091
|
"instantly. When the user asks about a different repo \u2014 or to compare repos \u2014",
|
|
18823
19092
|
"fetch it instead of saying you are scoped to a single repo."
|
|
18824
19093
|
].join("\n");
|
|
19094
|
+
var DASHBOARD_CMS_PROMPT = [
|
|
19095
|
+
"# Dashboard CMS",
|
|
19096
|
+
"Use Dashboard CMS tools for CMS content. The Dashboard is the source of truth.",
|
|
19097
|
+
"Do not query Mongo directly for CMS content and do not infer CMS data from repository files.",
|
|
19098
|
+
"Use cms_list_documents first when you need to find content, then pass the raw id or cmsDocumentId to cms_get_document."
|
|
19099
|
+
].join("\n");
|
|
18825
19100
|
function buildExecutableCatalog() {
|
|
18826
19101
|
let discovered;
|
|
18827
19102
|
try {
|
|
@@ -18880,6 +19155,8 @@ async function runChatTurn(opts) {
|
|
|
18880
19155
|
const memoryBlock = readMemoryIndexBlock(opts.cwd);
|
|
18881
19156
|
const instructionsBlock = readInstructionsBlock(opts.cwd);
|
|
18882
19157
|
const crossRepoBlock = opts.reposRoot ? CROSS_REPO_PROMPT : null;
|
|
19158
|
+
const dashboardCmsEnabled = Boolean(opts.cmsDashboardUrl && opts.cmsRepoSlug && opts.cmsToken);
|
|
19159
|
+
const dashboardCmsBlock = dashboardCmsEnabled ? DASHBOARD_CMS_PROMPT : null;
|
|
18883
19160
|
const imageBlock = imagePaths.length > 0 ? [
|
|
18884
19161
|
"# Attached images",
|
|
18885
19162
|
"The user attached one or more images on this turn. They are saved as",
|
|
@@ -18894,6 +19171,7 @@ async function runChatTurn(opts) {
|
|
|
18894
19171
|
memoryBlock,
|
|
18895
19172
|
instructionsBlock,
|
|
18896
19173
|
crossRepoBlock,
|
|
19174
|
+
dashboardCmsBlock,
|
|
18897
19175
|
imageBlock,
|
|
18898
19176
|
catalog,
|
|
18899
19177
|
artifactAddendum
|
|
@@ -18921,6 +19199,14 @@ async function runChatTurn(opts) {
|
|
|
18921
19199
|
reposRoot: opts.reposRoot,
|
|
18922
19200
|
repoToken: opts.repoToken
|
|
18923
19201
|
} : {},
|
|
19202
|
+
...dashboardCmsEnabled ? {
|
|
19203
|
+
enableDashboardCmsTool: true,
|
|
19204
|
+
cmsDashboardUrl: opts.cmsDashboardUrl,
|
|
19205
|
+
cmsRepoSlug: opts.cmsRepoSlug,
|
|
19206
|
+
cmsToken: opts.cmsToken,
|
|
19207
|
+
cmsStoreRepoUrl: opts.cmsStoreRepoUrl,
|
|
19208
|
+
cmsStoreRef: opts.cmsStoreRef
|
|
19209
|
+
} : {},
|
|
18924
19210
|
onProgress: async (ev) => {
|
|
18925
19211
|
progressSeq += 1;
|
|
18926
19212
|
if (ev.kind === "thinking") {
|
|
@@ -19019,7 +19305,9 @@ function readMemoryIndexBlock(cwd) {
|
|
|
19019
19305
|
}
|
|
19020
19306
|
const trimmed = raw.trim();
|
|
19021
19307
|
if (!trimmed) return "";
|
|
19022
|
-
const body = trimmed.length > MAX_INDEX_BYTES ? trimmed.slice(0, MAX_INDEX_BYTES)
|
|
19308
|
+
const body = trimmed.length > MAX_INDEX_BYTES ? `${trimmed.slice(0, MAX_INDEX_BYTES)}
|
|
19309
|
+
|
|
19310
|
+
_\u2026 (memory index truncated; use recall_search to read more)_` : trimmed;
|
|
19023
19311
|
return [
|
|
19024
19312
|
"# Project memory index (state repo `memory/INDEX.md`)",
|
|
19025
19313
|
"",
|
|
@@ -20647,6 +20935,9 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
20647
20935
|
}
|
|
20648
20936
|
const repo = strField(body, "repo");
|
|
20649
20937
|
const repoToken = strField(body, "repoToken");
|
|
20938
|
+
const dashboardUrl = strField(body, "dashboardUrl");
|
|
20939
|
+
const storeRepoUrl = strField(body, "storeRepoUrl");
|
|
20940
|
+
const storeRef = strField(body, "storeRef");
|
|
20650
20941
|
let agentCwd;
|
|
20651
20942
|
try {
|
|
20652
20943
|
agentCwd = await ensureRepoCwd({
|
|
@@ -20723,7 +21014,14 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
20723
21014
|
sink,
|
|
20724
21015
|
// Let the agent clone + work on OTHER repos via the fetch_repo tool.
|
|
20725
21016
|
reposRoot: opts.reposRoot,
|
|
20726
|
-
repoToken
|
|
21017
|
+
repoToken,
|
|
21018
|
+
...dashboardUrl && repo && stateToken ? {
|
|
21019
|
+
cmsDashboardUrl: dashboardUrl,
|
|
21020
|
+
cmsRepoSlug: repo,
|
|
21021
|
+
cmsToken: stateToken,
|
|
21022
|
+
cmsStoreRepoUrl: storeRepoUrl,
|
|
21023
|
+
cmsStoreRef: storeRef
|
|
21024
|
+
} : {}
|
|
20727
21025
|
});
|
|
20728
21026
|
} catch (err) {
|
|
20729
21027
|
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
@@ -20980,7 +21278,10 @@ async function proxyToBrainServe(args) {
|
|
|
20980
21278
|
body: JSON.stringify({
|
|
20981
21279
|
message: args.body.message,
|
|
20982
21280
|
...args.body.repo ? { repo: args.body.repo } : {},
|
|
20983
|
-
...args.body.repoToken ? { repoToken: args.body.repoToken } : {}
|
|
21281
|
+
...args.body.repoToken ? { repoToken: args.body.repoToken } : {},
|
|
21282
|
+
...args.body.dashboardUrl ? { dashboardUrl: args.body.dashboardUrl } : {},
|
|
21283
|
+
...args.body.storeRepoUrl ? { storeRepoUrl: args.body.storeRepoUrl } : {},
|
|
21284
|
+
...args.body.storeRef ? { storeRef: args.body.storeRef } : {}
|
|
20984
21285
|
})
|
|
20985
21286
|
});
|
|
20986
21287
|
if (!upstream.ok || !upstream.body) {
|
|
@@ -21195,15 +21496,15 @@ function buildMcpHttpServer(opts) {
|
|
|
21195
21496
|
transports.set(route.path, transport);
|
|
21196
21497
|
routes.set(route.path, route.name);
|
|
21197
21498
|
const mcpServer = new McpServer({ name: route.name, version: route.version });
|
|
21198
|
-
for (const
|
|
21499
|
+
for (const tool6 of route.tools) {
|
|
21199
21500
|
mcpServer.registerTool(
|
|
21200
|
-
|
|
21501
|
+
tool6.name,
|
|
21201
21502
|
{
|
|
21202
|
-
description:
|
|
21203
|
-
inputSchema:
|
|
21503
|
+
description: tool6.description,
|
|
21504
|
+
inputSchema: tool6.inputSchema
|
|
21204
21505
|
},
|
|
21205
21506
|
async (args) => {
|
|
21206
|
-
const result = await
|
|
21507
|
+
const result = await tool6.handler(args);
|
|
21207
21508
|
return {
|
|
21208
21509
|
content: result.content,
|
|
21209
21510
|
...result.isError ? { isError: true } : {}
|
|
@@ -22509,7 +22810,7 @@ var PERF_GUEST = {
|
|
|
22509
22810
|
medium: { cpu_kind: "performance", cpus: 1, memory_mb: 2048 },
|
|
22510
22811
|
high: { cpu_kind: "performance", cpus: 2, memory_mb: 4096 }
|
|
22511
22812
|
};
|
|
22512
|
-
function
|
|
22813
|
+
function envInt2(name, dflt) {
|
|
22513
22814
|
const v = Number(process.env[name]);
|
|
22514
22815
|
return Number.isFinite(v) && v > 0 ? v : dflt;
|
|
22515
22816
|
}
|
|
@@ -22629,10 +22930,10 @@ async function poolServe() {
|
|
|
22629
22930
|
const region = process.env.POOL_REGION ?? "fra";
|
|
22630
22931
|
const perf = process.env.POOL_PERF ?? "medium";
|
|
22631
22932
|
const guest = PERF_GUEST[perf] ?? PERF_GUEST.medium;
|
|
22632
|
-
const min =
|
|
22633
|
-
const runnerPort =
|
|
22634
|
-
const apiPort =
|
|
22635
|
-
const healthTimeoutMs =
|
|
22933
|
+
const min = envInt2("POOL_MIN", 2);
|
|
22934
|
+
const runnerPort = envInt2("RUNNER_PORT", 8080);
|
|
22935
|
+
const apiPort = envInt2("POOL_API_PORT", 4100);
|
|
22936
|
+
const healthTimeoutMs = envInt2("POOL_HEALTH_TIMEOUT_MS", 12e4);
|
|
22636
22937
|
const registry = new PoolRegistry({
|
|
22637
22938
|
githubToken,
|
|
22638
22939
|
masterKey: master,
|
|
@@ -22648,12 +22949,12 @@ async function poolServe() {
|
|
|
22648
22949
|
},
|
|
22649
22950
|
log
|
|
22650
22951
|
});
|
|
22651
|
-
const refillMs =
|
|
22952
|
+
const refillMs = envInt2("POOL_REFILL_INTERVAL_MS", 6e4);
|
|
22652
22953
|
const tick = setInterval(() => {
|
|
22653
22954
|
registry.resyncAll().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
22654
22955
|
}, refillMs);
|
|
22655
22956
|
const dutyTickEnabled = (process.env.POOL_CAPABILITY_TICK ?? "1") !== "0";
|
|
22656
|
-
const dutyTickMs =
|
|
22957
|
+
const dutyTickMs = envInt2("POOL_CAPABILITY_TICK_MS", 15 * 6e4);
|
|
22657
22958
|
const dutyTick = dutyTickEnabled ? setInterval(() => {
|
|
22658
22959
|
runCapabilityFallbackTick({
|
|
22659
22960
|
isDegraded: () => gitHubActionsDegraded(),
|