@openape/ape-agent 2.7.0 → 2.7.1
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/bridge.mjs +454 -73
- package/package.json +4 -4
package/dist/bridge.mjs
CHANGED
|
@@ -333,7 +333,7 @@ async function prompt(message, opts = {}) {
|
|
|
333
333
|
}
|
|
334
334
|
throw new Error(`Unknown prompt type: ${opts.type}`);
|
|
335
335
|
}
|
|
336
|
-
var src, hasRequiredSrc, srcExports, picocolors, hasRequiredPicocolors, picocolorsExports, e, Q, P$1, X, DD, uD, FD, m, L$1, N, I, r, tD, eD, iD, v, CD, w$1, W$1, rD, R, y, V$1, z, ED, _, nD, oD, aD, c, S, AD, pD, h, x, fD, bD, mD, Y, wD, SD, $D,
|
|
336
|
+
var src, hasRequiredSrc, srcExports, picocolors, hasRequiredPicocolors, picocolorsExports, e, Q, P$1, X, DD, uD, FD, m, L$1, N, I, r, tD, eD, iD, v, CD, w$1, W$1, rD, R, y, V$1, z, ED, _, nD, oD, aD, c, S, AD, pD, h, x, fD, bD, mD, Y, wD, SD, $D, q2, jD, PD, V, u, le, L, W, C, o, d, k, P, A, T, F, w, B, he, ye, ve, fe, kCancel;
|
|
337
337
|
var init_prompt = __esm({
|
|
338
338
|
"../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/chunks/prompt.mjs"() {
|
|
339
339
|
"use strict";
|
|
@@ -609,10 +609,10 @@ var init_prompt = __esm({
|
|
|
609
609
|
};
|
|
610
610
|
SD = Object.defineProperty;
|
|
611
611
|
$D = (t2, u3, F3) => u3 in t2 ? SD(t2, u3, { enumerable: true, configurable: true, writable: true, value: F3 }) : t2[u3] = F3;
|
|
612
|
-
|
|
612
|
+
q2 = (t2, u3, F3) => ($D(t2, typeof u3 != "symbol" ? u3 + "" : u3, F3), F3);
|
|
613
613
|
jD = class extends x {
|
|
614
614
|
constructor(u3) {
|
|
615
|
-
super(u3, false),
|
|
615
|
+
super(u3, false), q2(this, "options"), q2(this, "cursor", 0), this.options = u3.options, this.cursor = this.options.findIndex(({ value: F3 }) => F3 === u3.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (F3) => {
|
|
616
616
|
switch (F3) {
|
|
617
617
|
case "left":
|
|
618
618
|
case "up":
|
|
@@ -1036,7 +1036,7 @@ var require_shell_quote = __commonJS({
|
|
|
1036
1036
|
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
1037
1037
|
import { homedir as homedir9 } from "os";
|
|
1038
1038
|
import { join as join9 } from "path";
|
|
1039
|
-
import
|
|
1039
|
+
import process3 from "process";
|
|
1040
1040
|
|
|
1041
1041
|
// ../../packages/cli-auth/dist/index.js
|
|
1042
1042
|
import { ofetch } from "ofetch";
|
|
@@ -1510,11 +1510,14 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as read
|
|
|
1510
1510
|
import { homedir as homedir6 } from "os";
|
|
1511
1511
|
import { join as join6 } from "path";
|
|
1512
1512
|
|
|
1513
|
-
// ../../packages/apes/dist/chunk-
|
|
1513
|
+
// ../../packages/apes/dist/chunk-VHZEVW2N.js
|
|
1514
1514
|
import { spawn } from "child_process";
|
|
1515
1515
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1516
1516
|
import { homedir as homedir3 } from "os";
|
|
1517
1517
|
import { dirname, normalize, resolve } from "path";
|
|
1518
|
+
import { homedir as homedir22 } from "os";
|
|
1519
|
+
import { resolve as resolve2 } from "path";
|
|
1520
|
+
import process2 from "process";
|
|
1518
1521
|
import { execFileSync } from "child_process";
|
|
1519
1522
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1520
1523
|
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -1526,6 +1529,57 @@ function capStdio(s2) {
|
|
|
1526
1529
|
return `${buf.subarray(0, MAX_STDIO_BYTES).toString("utf8")}
|
|
1527
1530
|
[truncated to ${MAX_STDIO_BYTES} bytes]`;
|
|
1528
1531
|
}
|
|
1532
|
+
function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1533
|
+
return new Promise((resolveResult) => {
|
|
1534
|
+
const child = spawn(BIN, ["-c", cmd], {
|
|
1535
|
+
env: { ...process.env, APE_WAIT: "1" },
|
|
1536
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1537
|
+
});
|
|
1538
|
+
let stdout2 = "";
|
|
1539
|
+
let stderr = "";
|
|
1540
|
+
let timedOut = false;
|
|
1541
|
+
let spawnError = null;
|
|
1542
|
+
child.stdout.on("data", (chunk) => {
|
|
1543
|
+
stdout2 += chunk.toString("utf8");
|
|
1544
|
+
});
|
|
1545
|
+
child.stderr.on("data", (chunk) => {
|
|
1546
|
+
stderr += chunk.toString("utf8");
|
|
1547
|
+
});
|
|
1548
|
+
child.on("error", (err) => {
|
|
1549
|
+
spawnError = err;
|
|
1550
|
+
});
|
|
1551
|
+
const timer = setTimeout(() => {
|
|
1552
|
+
timedOut = true;
|
|
1553
|
+
child.kill("SIGTERM");
|
|
1554
|
+
setTimeout(() => {
|
|
1555
|
+
try {
|
|
1556
|
+
child.kill("SIGKILL");
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
}, 5e3);
|
|
1560
|
+
}, timeoutMs);
|
|
1561
|
+
child.on("close", (code) => {
|
|
1562
|
+
clearTimeout(timer);
|
|
1563
|
+
if (spawnError) {
|
|
1564
|
+
resolveResult({
|
|
1565
|
+
stdout: "",
|
|
1566
|
+
stderr: "",
|
|
1567
|
+
exit_code: -1,
|
|
1568
|
+
error: spawnError.message,
|
|
1569
|
+
hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
|
|
1570
|
+
});
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
resolveResult({
|
|
1574
|
+
stdout: capStdio(stdout2),
|
|
1575
|
+
stderr: capStdio(stderr),
|
|
1576
|
+
exit_code: code ?? -1,
|
|
1577
|
+
...timedOut ? { timed_out: true } : {}
|
|
1578
|
+
});
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
var DEFAULTS = { DEFAULT_TIMEOUT_MS };
|
|
1529
1583
|
var bashTools = [
|
|
1530
1584
|
{
|
|
1531
1585
|
name: "bash",
|
|
@@ -1549,52 +1603,8 @@ var bashTools = [
|
|
|
1549
1603
|
if (typeof a2.cmd !== "string" || a2.cmd.trim() === "") {
|
|
1550
1604
|
throw new Error("cmd must be a non-empty string");
|
|
1551
1605
|
}
|
|
1552
|
-
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULT_TIMEOUT_MS;
|
|
1553
|
-
return await
|
|
1554
|
-
const child = spawn(BIN, ["-c", a2.cmd], {
|
|
1555
|
-
env: { ...process.env, APE_WAIT: "1" },
|
|
1556
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1557
|
-
});
|
|
1558
|
-
let stdout2 = "";
|
|
1559
|
-
let stderr = "";
|
|
1560
|
-
let timedOut = false;
|
|
1561
|
-
let spawnError = null;
|
|
1562
|
-
child.stdout.on("data", (chunk) => {
|
|
1563
|
-
stdout2 += chunk.toString("utf8");
|
|
1564
|
-
});
|
|
1565
|
-
child.stderr.on("data", (chunk) => {
|
|
1566
|
-
stderr += chunk.toString("utf8");
|
|
1567
|
-
});
|
|
1568
|
-
child.on("error", (err) => {
|
|
1569
|
-
spawnError = err;
|
|
1570
|
-
});
|
|
1571
|
-
const timer = setTimeout(() => {
|
|
1572
|
-
timedOut = true;
|
|
1573
|
-
child.kill("SIGTERM");
|
|
1574
|
-
setTimeout(() => {
|
|
1575
|
-
try {
|
|
1576
|
-
child.kill("SIGKILL");
|
|
1577
|
-
} catch {
|
|
1578
|
-
}
|
|
1579
|
-
}, 5e3);
|
|
1580
|
-
}, timeout);
|
|
1581
|
-
child.on("close", (code) => {
|
|
1582
|
-
clearTimeout(timer);
|
|
1583
|
-
if (spawnError) {
|
|
1584
|
-
resolveResult({
|
|
1585
|
-
error: spawnError.message,
|
|
1586
|
-
hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
|
|
1587
|
-
});
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
resolveResult({
|
|
1591
|
-
stdout: capStdio(stdout2),
|
|
1592
|
-
stderr: capStdio(stderr),
|
|
1593
|
-
exit_code: code ?? -1,
|
|
1594
|
-
...timedOut ? { timed_out: true } : {}
|
|
1595
|
-
});
|
|
1596
|
-
});
|
|
1597
|
-
});
|
|
1606
|
+
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULTS.DEFAULT_TIMEOUT_MS;
|
|
1607
|
+
return await runApeShell(a2.cmd, timeout);
|
|
1598
1608
|
}
|
|
1599
1609
|
}
|
|
1600
1610
|
];
|
|
@@ -1653,6 +1663,337 @@ var fileTools = [
|
|
|
1653
1663
|
writeFileSync2(p, a2.content, { encoding: "utf8" });
|
|
1654
1664
|
return { path: p, bytes: Buffer.byteLength(a2.content, "utf8") };
|
|
1655
1665
|
}
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
name: "file.edit",
|
|
1669
|
+
description: "Replace an exact substring in a file under the agent's home directory. Prefer this over file.write for edits \u2014 it touches only the changed region instead of rewriting the whole file. `old_string` must appear exactly once unless `replace_all` is true. Path traversal blocked, 1MB max.",
|
|
1670
|
+
parameters: {
|
|
1671
|
+
type: "object",
|
|
1672
|
+
properties: {
|
|
1673
|
+
path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
|
|
1674
|
+
old_string: { type: "string", description: "Exact text to replace. Include enough surrounding context to be unique unless replace_all is set." },
|
|
1675
|
+
new_string: { type: "string", description: "Replacement text. Must differ from old_string." },
|
|
1676
|
+
replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring a unique match. Default false." }
|
|
1677
|
+
},
|
|
1678
|
+
required: ["path", "old_string", "new_string"]
|
|
1679
|
+
},
|
|
1680
|
+
execute: async (args) => {
|
|
1681
|
+
const a2 = args;
|
|
1682
|
+
if (typeof a2.old_string !== "string" || a2.old_string === "") {
|
|
1683
|
+
throw new Error("old_string must be a non-empty string");
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof a2.new_string !== "string") {
|
|
1686
|
+
throw new TypeError("new_string must be a string");
|
|
1687
|
+
}
|
|
1688
|
+
if (a2.old_string === a2.new_string) {
|
|
1689
|
+
throw new Error("old_string and new_string are identical \u2014 nothing to change");
|
|
1690
|
+
}
|
|
1691
|
+
const replaceAll = a2.replace_all === true;
|
|
1692
|
+
const p = jailPath(a2.path);
|
|
1693
|
+
const before = readFileSync3(p, "utf8");
|
|
1694
|
+
const occurrences = before.split(a2.old_string).length - 1;
|
|
1695
|
+
if (occurrences === 0) {
|
|
1696
|
+
throw new Error("old_string not found in file");
|
|
1697
|
+
}
|
|
1698
|
+
if (occurrences > 1 && !replaceAll) {
|
|
1699
|
+
throw new Error(`old_string occurs ${occurrences} times \u2014 pass replace_all:true or add surrounding context to make it unique`);
|
|
1700
|
+
}
|
|
1701
|
+
const after = replaceAll ? before.split(a2.old_string).join(a2.new_string) : before.replace(a2.old_string, a2.new_string);
|
|
1702
|
+
if (Buffer.byteLength(after, "utf8") > MAX_BYTES) {
|
|
1703
|
+
throw new Error(`result exceeds ${MAX_BYTES} byte cap`);
|
|
1704
|
+
}
|
|
1705
|
+
writeFileSync2(p, after, { encoding: "utf8" });
|
|
1706
|
+
return { path: p, replacements: replaceAll ? occurrences : 1 };
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
];
|
|
1710
|
+
var BRANCH_RE = /^[\w./-]{1,200}$/;
|
|
1711
|
+
var ID_RE = /^\d{1,12}$/;
|
|
1712
|
+
function shq(s2) {
|
|
1713
|
+
return `'${String(s2).replace(/'/g, "'\\''")}'`;
|
|
1714
|
+
}
|
|
1715
|
+
function assertBranch(v2) {
|
|
1716
|
+
if (typeof v2 !== "string" || !BRANCH_RE.test(v2)) {
|
|
1717
|
+
throw new Error("branch must match ^[A-Za-z0-9._/-]{1,200}$");
|
|
1718
|
+
}
|
|
1719
|
+
return v2;
|
|
1720
|
+
}
|
|
1721
|
+
function assertId(v2) {
|
|
1722
|
+
if (typeof v2 !== "string" && typeof v2 !== "number") throw new Error("id required");
|
|
1723
|
+
const s2 = String(v2);
|
|
1724
|
+
if (!ID_RE.test(s2)) throw new Error("id must be a number");
|
|
1725
|
+
return s2;
|
|
1726
|
+
}
|
|
1727
|
+
var githubAdapter = {
|
|
1728
|
+
id: "github",
|
|
1729
|
+
matchesRemote: (url) => /github\.com/i.test(url),
|
|
1730
|
+
prCreate: (i2) => {
|
|
1731
|
+
const head = assertBranch(i2.head);
|
|
1732
|
+
const parts = ["gh", "pr", "create", "--title", shq(i2.title), "--body", shq(i2.body), "--head", shq(head)];
|
|
1733
|
+
if (i2.base !== void 0) parts.push("--base", shq(assertBranch(i2.base)));
|
|
1734
|
+
return parts.join(" ");
|
|
1735
|
+
},
|
|
1736
|
+
prMerge: (i2) => {
|
|
1737
|
+
const ref = String(i2.ref);
|
|
1738
|
+
const refTok = ID_RE.test(ref) ? ref : assertBranch(ref);
|
|
1739
|
+
const parts = ["gh", "pr", "merge", shq(refTok)];
|
|
1740
|
+
if (i2.squash === true) parts.push("--squash");
|
|
1741
|
+
if (i2.auto) parts.push("--auto");
|
|
1742
|
+
if (i2.deleteBranch) parts.push("--delete-branch");
|
|
1743
|
+
return parts.join(" ");
|
|
1744
|
+
},
|
|
1745
|
+
prStatus: (ref) => {
|
|
1746
|
+
const r3 = String(ref);
|
|
1747
|
+
const refTok = ID_RE.test(r3) ? r3 : assertBranch(r3);
|
|
1748
|
+
return `gh pr view ${shq(refTok)} --json state,mergeStateStatus,statusCheckRollup,reviewDecision`;
|
|
1749
|
+
},
|
|
1750
|
+
issueGet: (ref) => `gh issue view ${assertId(ref)} --json number,title,body,labels`
|
|
1751
|
+
};
|
|
1752
|
+
var azureAdapter = {
|
|
1753
|
+
id: "azure",
|
|
1754
|
+
matchesRemote: (url) => /dev\.azure\.com|visualstudio\.com/i.test(url),
|
|
1755
|
+
prCreate: (i2) => {
|
|
1756
|
+
const head = assertBranch(i2.head);
|
|
1757
|
+
const parts = ["az", "repos", "pr", "create", "--title", shq(i2.title), "--description", shq(i2.body), "--source-branch", shq(head)];
|
|
1758
|
+
if (i2.base !== void 0) parts.push("--target-branch", shq(assertBranch(i2.base)));
|
|
1759
|
+
return parts.join(" ");
|
|
1760
|
+
},
|
|
1761
|
+
prMerge: (i2) => {
|
|
1762
|
+
const id = assertId(i2.ref);
|
|
1763
|
+
const parts = ["az", "repos", "pr", "update", "--id", id];
|
|
1764
|
+
if (i2.auto) parts.push("--auto-complete", "true");
|
|
1765
|
+
else parts.push("--status", "completed");
|
|
1766
|
+
if (i2.squash === true) parts.push("--merge-commit-message-style", "squash");
|
|
1767
|
+
if (i2.deleteBranch) parts.push("--delete-source-branch", "true");
|
|
1768
|
+
return parts.join(" ");
|
|
1769
|
+
},
|
|
1770
|
+
prStatus: (ref) => `az repos pr show --id ${assertId(ref)}`,
|
|
1771
|
+
issueGet: (ref) => `az boards work-item show --id ${assertId(ref)}`
|
|
1772
|
+
};
|
|
1773
|
+
var registry = /* @__PURE__ */ new Map([
|
|
1774
|
+
[githubAdapter.id, githubAdapter],
|
|
1775
|
+
[azureAdapter.id, azureAdapter]
|
|
1776
|
+
]);
|
|
1777
|
+
function listForges() {
|
|
1778
|
+
return [...registry.keys()];
|
|
1779
|
+
}
|
|
1780
|
+
function getForge(id) {
|
|
1781
|
+
const a2 = registry.get(id);
|
|
1782
|
+
if (!a2) {
|
|
1783
|
+
throw new Error(`unknown forge '${id}'. Registered: ${listForges().join(", ")}. Add one with registerForge().`);
|
|
1784
|
+
}
|
|
1785
|
+
return a2;
|
|
1786
|
+
}
|
|
1787
|
+
function detectForge(remoteUrl) {
|
|
1788
|
+
if (typeof remoteUrl !== "string" || remoteUrl === "") {
|
|
1789
|
+
throw new Error("remote URL required to detect forge");
|
|
1790
|
+
}
|
|
1791
|
+
for (const a2 of registry.values()) {
|
|
1792
|
+
if (a2.matchesRemote(remoteUrl)) return a2.id;
|
|
1793
|
+
}
|
|
1794
|
+
throw new Error(`no forge adapter matches remote: ${remoteUrl}. Registered: ${listForges().join(", ")}. Register one with registerForge() (e.g. GitLab/Bitbucket/Gitea).`);
|
|
1795
|
+
}
|
|
1796
|
+
function buildPrCreate(input) {
|
|
1797
|
+
return getForge(input.forge).prCreate(input);
|
|
1798
|
+
}
|
|
1799
|
+
function buildPrMerge(input) {
|
|
1800
|
+
return getForge(input.forge).prMerge(input);
|
|
1801
|
+
}
|
|
1802
|
+
function buildPrStatus(forge, ref) {
|
|
1803
|
+
return getForge(forge).prStatus(ref);
|
|
1804
|
+
}
|
|
1805
|
+
function buildIssueGet(forge, ref) {
|
|
1806
|
+
return getForge(forge).issueGet(ref);
|
|
1807
|
+
}
|
|
1808
|
+
function resolveForge(a2) {
|
|
1809
|
+
if (typeof a2.forge === "string" && a2.forge !== "") return a2.forge;
|
|
1810
|
+
if (typeof a2.remote === "string") return detectForge(a2.remote);
|
|
1811
|
+
throw new Error("provide a forge id (e.g. github, azure, or a registered adapter) or a remote URL to detect it");
|
|
1812
|
+
}
|
|
1813
|
+
var forgeParam = { type: "string", description: "Target forge id (github, azure, or a registered adapter). Omit to auto-detect from `remote`." };
|
|
1814
|
+
var remoteParam = { type: "string", description: "git remote URL \u2014 used to auto-detect the forge when `forge` is omitted." };
|
|
1815
|
+
var forgeTools = [
|
|
1816
|
+
{
|
|
1817
|
+
name: "forge.pr.create",
|
|
1818
|
+
description: "Open a pull request on GitHub (gh) or Azure DevOps (az). Gated via the DDISA grant cycle. Provider chosen by `forge` or auto-detected from `remote`.",
|
|
1819
|
+
parameters: {
|
|
1820
|
+
type: "object",
|
|
1821
|
+
properties: {
|
|
1822
|
+
forge: forgeParam,
|
|
1823
|
+
remote: remoteParam,
|
|
1824
|
+
title: { type: "string", description: "PR title." },
|
|
1825
|
+
body: { type: "string", description: "PR description / body." },
|
|
1826
|
+
head: { type: "string", description: "Source branch." },
|
|
1827
|
+
base: { type: "string", description: "Target branch. Omit for the repo default." }
|
|
1828
|
+
},
|
|
1829
|
+
required: ["title", "body", "head"]
|
|
1830
|
+
},
|
|
1831
|
+
execute: async (args) => {
|
|
1832
|
+
const a2 = args;
|
|
1833
|
+
const cmd = buildPrCreate({ forge: resolveForge(a2), title: a2.title, body: a2.body, head: a2.head, base: a2.base });
|
|
1834
|
+
return await runApeShell(cmd);
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
name: "forge.pr.merge",
|
|
1839
|
+
description: 'Merge a PR \u2014 or with auto=true, arm "merge when checks pass" (gh --auto / az auto-complete) so the platform merges only on green CI. Gated. Never bypasses required checks (branch protection is the server-side gate).',
|
|
1840
|
+
parameters: {
|
|
1841
|
+
type: "object",
|
|
1842
|
+
properties: {
|
|
1843
|
+
forge: forgeParam,
|
|
1844
|
+
remote: remoteParam,
|
|
1845
|
+
ref: { type: "string", description: "GitHub: PR number or branch. Azure: PR id." },
|
|
1846
|
+
auto: { type: "boolean", description: "Arm merge-when-green instead of immediate merge. Recommended." },
|
|
1847
|
+
squash: { type: "boolean", description: "Squash-merge. Default true." },
|
|
1848
|
+
delete_branch: { type: "boolean", description: "Delete the source branch after merge." }
|
|
1849
|
+
},
|
|
1850
|
+
required: ["ref"]
|
|
1851
|
+
},
|
|
1852
|
+
execute: async (args) => {
|
|
1853
|
+
const a2 = args;
|
|
1854
|
+
const cmd = buildPrMerge({ forge: resolveForge(a2), ref: a2.ref, auto: a2.auto, squash: a2.squash, deleteBranch: a2.delete_branch });
|
|
1855
|
+
return await runApeShell(cmd);
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
name: "forge.pr.status",
|
|
1860
|
+
description: "Fetch a PR's state + checks + review decision. Gated (read).",
|
|
1861
|
+
parameters: {
|
|
1862
|
+
type: "object",
|
|
1863
|
+
properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "PR number/branch (GitHub) or id (Azure)." } },
|
|
1864
|
+
required: ["ref"]
|
|
1865
|
+
},
|
|
1866
|
+
execute: async (args) => {
|
|
1867
|
+
const a2 = args;
|
|
1868
|
+
return await runApeShell(buildPrStatus(resolveForge(a2), a2.ref));
|
|
1869
|
+
}
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
name: "forge.issue.get",
|
|
1873
|
+
description: "Fetch an issue (GitHub) or work-item (Azure) \u2014 title, body, labels. Gated (read). Use to turn an assigned task into a coding run.",
|
|
1874
|
+
parameters: {
|
|
1875
|
+
type: "object",
|
|
1876
|
+
properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "Issue number (GitHub) or work-item id (Azure)." } },
|
|
1877
|
+
required: ["ref"]
|
|
1878
|
+
},
|
|
1879
|
+
execute: async (args) => {
|
|
1880
|
+
const a2 = args;
|
|
1881
|
+
return await runApeShell(buildIssueGet(resolveForge(a2), a2.ref));
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
];
|
|
1885
|
+
function jailedRoot(envVar, fallbackName) {
|
|
1886
|
+
const home = homedir22();
|
|
1887
|
+
const raw = process2.env[envVar];
|
|
1888
|
+
const dir = raw ? resolve2(raw) : resolve2(home, fallbackName);
|
|
1889
|
+
if (dir !== home && !dir.startsWith(`${home}/`)) {
|
|
1890
|
+
throw new Error(`${envVar} (${dir}) must resolve inside the agent's home`);
|
|
1891
|
+
}
|
|
1892
|
+
return dir;
|
|
1893
|
+
}
|
|
1894
|
+
function workRoot() {
|
|
1895
|
+
return jailedRoot("OPENAPE_CODING_WORK_DIR", "work");
|
|
1896
|
+
}
|
|
1897
|
+
function reposRoot() {
|
|
1898
|
+
return jailedRoot("OPENAPE_CODING_REPOS_DIR", "repos");
|
|
1899
|
+
}
|
|
1900
|
+
var TASK_ID_RE = /^[\w.-]{1,64}$/;
|
|
1901
|
+
var BRANCH_RE2 = /^[\w./-]{1,128}$/;
|
|
1902
|
+
var URL_RE = /^(?:https:\/\/|git@)[\w@:/.-]{3,256}$/;
|
|
1903
|
+
function assertTaskId(v2) {
|
|
1904
|
+
if (typeof v2 !== "string" || !TASK_ID_RE.test(v2)) {
|
|
1905
|
+
throw new Error("task_id must match ^[a-zA-Z0-9._-]{1,64}$");
|
|
1906
|
+
}
|
|
1907
|
+
return v2;
|
|
1908
|
+
}
|
|
1909
|
+
function assertBranch2(v2) {
|
|
1910
|
+
if (typeof v2 !== "string" || !BRANCH_RE2.test(v2)) {
|
|
1911
|
+
throw new Error("branch must match ^[A-Za-z0-9._/-]{1,128}$");
|
|
1912
|
+
}
|
|
1913
|
+
return v2;
|
|
1914
|
+
}
|
|
1915
|
+
function resolveRepo(repo) {
|
|
1916
|
+
if (typeof repo !== "string" || repo === "") {
|
|
1917
|
+
throw new Error("repo must be a non-empty string (URL or path under $HOME)");
|
|
1918
|
+
}
|
|
1919
|
+
const home = homedir22();
|
|
1920
|
+
if (URL_RE.test(repo)) {
|
|
1921
|
+
const tail = repo.replace(/\.git$/, "").replace(/[/:]+$/, "");
|
|
1922
|
+
const parts = tail.split(/[/:]/).filter(Boolean).slice(-2);
|
|
1923
|
+
const base = parts.join("-").replace(/[^\w.-]/g, "");
|
|
1924
|
+
if (!base) throw new Error("could not derive a clone name from repo URL");
|
|
1925
|
+
return { source: repo, baseDir: resolve2(reposRoot(), base), isUrl: true };
|
|
1926
|
+
}
|
|
1927
|
+
const candidate = repo.startsWith("~/") ? resolve2(home, repo.slice(2)) : resolve2(home, repo);
|
|
1928
|
+
if (candidate !== home && !candidate.startsWith(`${home}/`)) {
|
|
1929
|
+
throw new Error(`repo path "${repo}" resolves outside the agent's home`);
|
|
1930
|
+
}
|
|
1931
|
+
return { source: candidate, baseDir: candidate, isUrl: false };
|
|
1932
|
+
}
|
|
1933
|
+
function worktreePathFor(taskId) {
|
|
1934
|
+
return resolve2(workRoot(), assertTaskId(taskId));
|
|
1935
|
+
}
|
|
1936
|
+
var q = (s2) => `'${s2}'`;
|
|
1937
|
+
function buildCreateCommand(repo, taskId, branch) {
|
|
1938
|
+
const id = assertTaskId(taskId);
|
|
1939
|
+
const br = assertBranch2(branch);
|
|
1940
|
+
const { source, baseDir, isUrl } = resolveRepo(repo);
|
|
1941
|
+
const wt = worktreePathFor(id);
|
|
1942
|
+
const clone = isUrl ? `if [ ! -d ${q(baseDir)}/.git ]; then git clone ${q(source)} ${q(baseDir)}; fi` : `test -d ${q(baseDir)}/.git`;
|
|
1943
|
+
return [
|
|
1944
|
+
`mkdir -p ${q(reposRoot())} ${q(workRoot())}`,
|
|
1945
|
+
clone,
|
|
1946
|
+
`git -C ${q(baseDir)} fetch --quiet || true`,
|
|
1947
|
+
`git -C ${q(baseDir)} worktree add -b ${q(br)} ${q(wt)}`,
|
|
1948
|
+
`echo ${q(wt)}`
|
|
1949
|
+
].join(" && ");
|
|
1950
|
+
}
|
|
1951
|
+
function buildRemoveCommand(repo, taskId) {
|
|
1952
|
+
const id = assertTaskId(taskId);
|
|
1953
|
+
const { baseDir } = resolveRepo(repo);
|
|
1954
|
+
const wt = worktreePathFor(id);
|
|
1955
|
+
return `git -C ${q(baseDir)} worktree remove --force ${q(wt)} && git -C ${q(baseDir)} worktree prune`;
|
|
1956
|
+
}
|
|
1957
|
+
function buildListCommand() {
|
|
1958
|
+
return `ls -1 ${q(workRoot())} 2>/dev/null || true`;
|
|
1959
|
+
}
|
|
1960
|
+
var gitWorktreeTools = [
|
|
1961
|
+
{
|
|
1962
|
+
name: "git.worktree",
|
|
1963
|
+
description: "Manage isolated git worktrees for coding tasks. action=create clones the repo (cached under ~/repos) and adds a fresh worktree under ~/work/<task_id> on a new branch. action=remove tears it down. action=list shows current task worktrees. Git operations go through the DDISA grant cycle (git-shape).",
|
|
1964
|
+
parameters: {
|
|
1965
|
+
type: "object",
|
|
1966
|
+
properties: {
|
|
1967
|
+
action: { type: "string", enum: ["create", "remove", "list"], description: "create | remove | list" },
|
|
1968
|
+
repo: { type: "string", description: "For create/remove: git remote URL (https/git@) or a path under $HOME to an existing clone." },
|
|
1969
|
+
task_id: { type: "string", description: "For create/remove: identifier for the worktree, ^[a-zA-Z0-9._-]{1,64}$. The worktree lands at ~/work/<task_id>." },
|
|
1970
|
+
branch: { type: "string", description: "For create: new branch name, ^[A-Za-z0-9._/-]{1,128}$." }
|
|
1971
|
+
},
|
|
1972
|
+
required: ["action"]
|
|
1973
|
+
},
|
|
1974
|
+
execute: async (args) => {
|
|
1975
|
+
const a2 = args;
|
|
1976
|
+
let cmd;
|
|
1977
|
+
if (a2.action === "create") {
|
|
1978
|
+
if (typeof a2.branch !== "string") throw new Error("branch is required for action=create");
|
|
1979
|
+
cmd = buildCreateCommand(a2.repo, assertTaskId(a2.task_id), a2.branch);
|
|
1980
|
+
} else if (a2.action === "remove") {
|
|
1981
|
+
cmd = buildRemoveCommand(a2.repo, assertTaskId(a2.task_id));
|
|
1982
|
+
} else if (a2.action === "list") {
|
|
1983
|
+
cmd = buildListCommand();
|
|
1984
|
+
} else {
|
|
1985
|
+
throw new Error("action must be one of: create, remove, list");
|
|
1986
|
+
}
|
|
1987
|
+
const res = await runApeShell(cmd);
|
|
1988
|
+
return {
|
|
1989
|
+
action: a2.action,
|
|
1990
|
+
...a2.action !== "list" ? { worktree: worktreePathFor(assertTaskId(a2.task_id)) } : {},
|
|
1991
|
+
stdout: res.stdout,
|
|
1992
|
+
stderr: res.stderr,
|
|
1993
|
+
exit_code: res.exit_code,
|
|
1994
|
+
...res.error ? { error: res.error, hint: res.hint } : {}
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1656
1997
|
}
|
|
1657
1998
|
];
|
|
1658
1999
|
var MAX_BYTES2 = 1024 * 1024;
|
|
@@ -1883,13 +2224,53 @@ var timeTools = [
|
|
|
1883
2224
|
}
|
|
1884
2225
|
}
|
|
1885
2226
|
];
|
|
2227
|
+
var CWD_RE = /^[\w./-]{1,256}$/;
|
|
2228
|
+
async function runVerify(cwd, command, timeoutMs) {
|
|
2229
|
+
if (typeof cwd !== "string" || !CWD_RE.test(cwd)) {
|
|
2230
|
+
throw new Error("cwd must match ^[A-Za-z0-9._/-]{1,256}$");
|
|
2231
|
+
}
|
|
2232
|
+
if (typeof command !== "string" || command.trim() === "") {
|
|
2233
|
+
throw new Error("verify command must be a non-empty string");
|
|
2234
|
+
}
|
|
2235
|
+
const res = await runApeShell(`cd '${cwd}' && ${command}`, timeoutMs);
|
|
2236
|
+
return {
|
|
2237
|
+
passed: res.exit_code === 0,
|
|
2238
|
+
exit_code: res.exit_code,
|
|
2239
|
+
stdout: res.stdout,
|
|
2240
|
+
stderr: res.stderr,
|
|
2241
|
+
...res.timed_out ? { timed_out: true } : {}
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
var verifyTools = [
|
|
2245
|
+
{
|
|
2246
|
+
name: "verify",
|
|
2247
|
+
description: "Run the verification command (tests/build/lint) in a worktree and report pass/fail. The coding loop must NOT open or merge a PR when this fails. Runs through the DDISA grant cycle (same as bash). Returns { passed, exit_code, stdout, stderr }.",
|
|
2248
|
+
parameters: {
|
|
2249
|
+
type: "object",
|
|
2250
|
+
properties: {
|
|
2251
|
+
cwd: { type: "string", description: "Worktree path to run in (e.g. ~/work/issue-42)." },
|
|
2252
|
+
command: { type: "string", description: "Verification command, e.g. `pnpm test` or `npm run build && npm test`." },
|
|
2253
|
+
timeout_ms: { type: "number", description: "Wall-clock cap incl. approval wait. Default 300000." }
|
|
2254
|
+
},
|
|
2255
|
+
required: ["cwd", "command"]
|
|
2256
|
+
},
|
|
2257
|
+
execute: async (args) => {
|
|
2258
|
+
const a2 = args;
|
|
2259
|
+
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : void 0;
|
|
2260
|
+
return await runVerify(a2.cwd, a2.command, timeout);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
];
|
|
1886
2264
|
var ALL_TOOLS = [
|
|
1887
2265
|
...timeTools,
|
|
1888
2266
|
...httpTools,
|
|
1889
2267
|
...fileTools,
|
|
1890
2268
|
...tasksTools,
|
|
1891
2269
|
...mailTools,
|
|
1892
|
-
...bashTools
|
|
2270
|
+
...bashTools,
|
|
2271
|
+
...gitWorktreeTools,
|
|
2272
|
+
...verifyTools,
|
|
2273
|
+
...forgeTools
|
|
1893
2274
|
];
|
|
1894
2275
|
var TOOLS = Object.fromEntries(
|
|
1895
2276
|
ALL_TOOLS.map((t2) => [t2.name, t2])
|
|
@@ -3890,7 +4271,7 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
|
|
|
3890
4271
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
3891
4272
|
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "fs";
|
|
3892
4273
|
import { homedir as homedir8 } from "os";
|
|
3893
|
-
import { dirname as dirname2, join as join8, resolve as
|
|
4274
|
+
import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
|
|
3894
4275
|
import { fileURLToPath } from "url";
|
|
3895
4276
|
import { parse as parseYaml } from "yaml";
|
|
3896
4277
|
var SKILLS_SUBDIR = [".openape", "agent", "skills"];
|
|
@@ -3999,7 +4380,7 @@ function scanSkillsDir(dir) {
|
|
|
3999
4380
|
}
|
|
4000
4381
|
function defaultSkillsDir() {
|
|
4001
4382
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
4002
|
-
return
|
|
4383
|
+
return resolve3(here, "..", "default-skills");
|
|
4003
4384
|
}
|
|
4004
4385
|
function composeSkills(home, enabledTools) {
|
|
4005
4386
|
const enabled = new Set(enabledTools);
|
|
@@ -4063,7 +4444,7 @@ function readDefaultPersona() {
|
|
|
4063
4444
|
if (_defaultPersonaCache !== void 0) return _defaultPersonaCache;
|
|
4064
4445
|
try {
|
|
4065
4446
|
const here = dirname2(fileURLToPath(import.meta.url));
|
|
4066
|
-
const path =
|
|
4447
|
+
const path = resolve3(here, "..", "default-persona.md");
|
|
4067
4448
|
if (!existsSync6(path)) {
|
|
4068
4449
|
_defaultPersonaCache = null;
|
|
4069
4450
|
return null;
|
|
@@ -4345,8 +4726,8 @@ function loadBridgeEnvFile() {
|
|
|
4345
4726
|
const key = trimmed.slice(0, eq).trim();
|
|
4346
4727
|
const value = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
4347
4728
|
if (!key) continue;
|
|
4348
|
-
if (
|
|
4349
|
-
|
|
4729
|
+
if (process3.env[key] === void 0) {
|
|
4730
|
+
process3.env[key] = value;
|
|
4350
4731
|
}
|
|
4351
4732
|
}
|
|
4352
4733
|
} catch {
|
|
@@ -4354,24 +4735,24 @@ function loadBridgeEnvFile() {
|
|
|
4354
4735
|
}
|
|
4355
4736
|
function readConfig() {
|
|
4356
4737
|
loadBridgeEnvFile();
|
|
4357
|
-
const toolsRaw =
|
|
4738
|
+
const toolsRaw = process3.env.APE_CHAT_BRIDGE_TOOLS ?? "";
|
|
4358
4739
|
const tools = toolsRaw.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
4359
|
-
const maxStepsRaw =
|
|
4740
|
+
const maxStepsRaw = process3.env.APE_CHAT_BRIDGE_MAX_STEPS;
|
|
4360
4741
|
const maxSteps = maxStepsRaw ? Number.parseInt(maxStepsRaw, 10) : DEFAULT_MAX_STEPS;
|
|
4361
|
-
const model =
|
|
4742
|
+
const model = process3.env.APE_CHAT_BRIDGE_MODEL;
|
|
4362
4743
|
if (!model) {
|
|
4363
4744
|
throw new Error(
|
|
4364
4745
|
"APE_CHAT_BRIDGE_MODEL is not set. Set it in the bridge .env (usually `~/Library/Application Support/openape/bridge/.env` on macOS) or globally in `~/litellm/.env` so resolveBridgeConfig picks it up at spawn time. Common values: `gpt-5.4` (ChatGPT-only LiteLLM proxy), `claude-haiku-4-5` (Anthropic-only)."
|
|
4365
4746
|
);
|
|
4366
4747
|
}
|
|
4367
4748
|
return {
|
|
4368
|
-
endpoint: (
|
|
4369
|
-
apesBin:
|
|
4749
|
+
endpoint: (process3.env.APE_CHAT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
|
|
4750
|
+
apesBin: process3.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
|
|
4370
4751
|
model,
|
|
4371
|
-
systemPrompt:
|
|
4752
|
+
systemPrompt: process3.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
|
|
4372
4753
|
tools,
|
|
4373
4754
|
maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
|
|
4374
|
-
roomFilter:
|
|
4755
|
+
roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM
|
|
4375
4756
|
};
|
|
4376
4757
|
}
|
|
4377
4758
|
async function getIdentity() {
|
|
@@ -4383,11 +4764,11 @@ async function getIdentity() {
|
|
|
4383
4764
|
return { email: claims.sub };
|
|
4384
4765
|
}
|
|
4385
4766
|
function log(line) {
|
|
4386
|
-
|
|
4767
|
+
process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
4387
4768
|
`);
|
|
4388
4769
|
}
|
|
4389
4770
|
function sleep(ms) {
|
|
4390
|
-
return new Promise((
|
|
4771
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
4391
4772
|
}
|
|
4392
4773
|
function truncate(s2, n2) {
|
|
4393
4774
|
return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
|
|
@@ -4413,7 +4794,7 @@ var Bridge = class {
|
|
|
4413
4794
|
chat: this.chat,
|
|
4414
4795
|
ownerEmail: this.ownerEmail,
|
|
4415
4796
|
log,
|
|
4416
|
-
troopUrl: (
|
|
4797
|
+
troopUrl: (process3.env.OPENAPE_TROOP_URL ?? "https://troop.openape.ai").replace(/\/$/, ""),
|
|
4417
4798
|
bearer: this.bearer
|
|
4418
4799
|
});
|
|
4419
4800
|
this.cron.start();
|
|
@@ -4438,8 +4819,8 @@ var Bridge = class {
|
|
|
4438
4819
|
* whole process lifetime.
|
|
4439
4820
|
*/
|
|
4440
4821
|
runtimeConfig() {
|
|
4441
|
-
const apiBase = (
|
|
4442
|
-
const apiKey =
|
|
4822
|
+
const apiBase = (process3.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1").replace(/\/$/, "");
|
|
4823
|
+
const apiKey = process3.env.LITELLM_API_KEY ?? process3.env.LITELLM_MASTER_KEY ?? "";
|
|
4443
4824
|
if (!apiKey) {
|
|
4444
4825
|
throw new Error("LITELLM_API_KEY (or LITELLM_MASTER_KEY) must be set in the bridge env.");
|
|
4445
4826
|
}
|
|
@@ -4549,7 +4930,7 @@ var Bridge = class {
|
|
|
4549
4930
|
const bearer = await this.bearer();
|
|
4550
4931
|
const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/api/ws?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
|
|
4551
4932
|
const ws = new WebSocket(wsUrl);
|
|
4552
|
-
return new Promise((
|
|
4933
|
+
return new Promise((resolve4, reject) => {
|
|
4553
4934
|
let pingTimer;
|
|
4554
4935
|
let allowlistTimer;
|
|
4555
4936
|
ws.on("open", () => {
|
|
@@ -4582,7 +4963,7 @@ var Bridge = class {
|
|
|
4582
4963
|
ws.on("close", () => {
|
|
4583
4964
|
if (pingTimer) clearInterval(pingTimer);
|
|
4584
4965
|
if (allowlistTimer) clearInterval(allowlistTimer);
|
|
4585
|
-
|
|
4966
|
+
resolve4();
|
|
4586
4967
|
});
|
|
4587
4968
|
ws.on("error", (err) => {
|
|
4588
4969
|
if (pingTimer) clearInterval(pingTimer);
|
|
@@ -4621,7 +5002,7 @@ async function main() {
|
|
|
4621
5002
|
}
|
|
4622
5003
|
main().catch((err) => {
|
|
4623
5004
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4624
|
-
|
|
5005
|
+
process3.stderr.write(`fatal: ${msg}
|
|
4625
5006
|
`);
|
|
4626
|
-
|
|
5007
|
+
process3.exit(1);
|
|
4627
5008
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/ape-agent",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "OpenApe agent runtime: per-agent process that connects to chat.openape.ai, runs the LLM loop with tools + cron tasks, and streams replies back to owners.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"ofetch": "^1.4.1",
|
|
24
24
|
"ws": "^8.18.0",
|
|
25
25
|
"yaml": "^2.8.0",
|
|
26
|
-
"@openape/apes": "1.
|
|
27
|
-
"@openape/
|
|
28
|
-
"@openape/
|
|
26
|
+
"@openape/apes": "1.26.0",
|
|
27
|
+
"@openape/cli-auth": "0.4.1",
|
|
28
|
+
"@openape/prompt-injection-detector": "0.1.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@antfu/eslint-config": "^7.6.1",
|