@beevibe/daemon 0.1.3 → 0.1.4
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/main.js +690 -18
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -876,9 +876,6 @@ class ClaudeCodeRuntime {
|
|
|
876
876
|
args.push("--max-turns", String(maxTurns));
|
|
877
877
|
if (context.resume_session_id)
|
|
878
878
|
args.push("--resume", context.resume_session_id);
|
|
879
|
-
if (context.disallowed_tools?.length) {
|
|
880
|
-
args.push("--disallowedTools", context.disallowed_tools.join(","));
|
|
881
|
-
}
|
|
882
879
|
if (context.system_prompt_append.length > 0) {
|
|
883
880
|
args.push("--append-system-prompt", context.system_prompt_append);
|
|
884
881
|
}
|
|
@@ -1672,6 +1669,9 @@ function createDefaultRuntimeRegistry() {
|
|
|
1672
1669
|
opencode: new OpenCodeRuntime({})
|
|
1673
1670
|
};
|
|
1674
1671
|
}
|
|
1672
|
+
function runtimeMissingError(cli) {
|
|
1673
|
+
return `No runtime registered for dispatch payload type '${cli}'`;
|
|
1674
|
+
}
|
|
1675
1675
|
|
|
1676
1676
|
// src/api-client.ts
|
|
1677
1677
|
import WebSocket from "ws";
|
|
@@ -1732,8 +1732,681 @@ class ApiClient {
|
|
|
1732
1732
|
}
|
|
1733
1733
|
}
|
|
1734
1734
|
|
|
1735
|
+
// ../sandbox/dist/orchestrator.js
|
|
1736
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1737
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir, writeFile as writeFile2 } from "node:fs/promises";
|
|
1738
|
+
import { dirname as dirname2, join as join7, resolve } from "node:path";
|
|
1739
|
+
import { fileURLToPath } from "node:url";
|
|
1740
|
+
|
|
1741
|
+
// ../sandbox/dist/docker.js
|
|
1742
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1743
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1744
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
1745
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
1746
|
+
import { join as join6 } from "node:path";
|
|
1747
|
+
var DEFAULT_IMAGE = "python:3.12-slim";
|
|
1748
|
+
var DEFAULT_LIMITS = {
|
|
1749
|
+
cmd_timeout_seconds: 300,
|
|
1750
|
+
cpus: 2,
|
|
1751
|
+
memory: "2g",
|
|
1752
|
+
storage: "4g",
|
|
1753
|
+
network: true
|
|
1754
|
+
};
|
|
1755
|
+
async function createSandbox(opts = {}) {
|
|
1756
|
+
const image = opts.image ?? DEFAULT_IMAGE;
|
|
1757
|
+
const limits = { ...DEFAULT_LIMITS, ...opts.limits ?? {} };
|
|
1758
|
+
const id = generateId(opts.label ?? "bv-sbx");
|
|
1759
|
+
const artifact_dir = await mkdtemp(join6(tmpdir4(), `${id}-artifacts-`));
|
|
1760
|
+
const args = [
|
|
1761
|
+
"run",
|
|
1762
|
+
"--detach",
|
|
1763
|
+
"--name",
|
|
1764
|
+
id,
|
|
1765
|
+
`--cpus=${limits.cpus}`,
|
|
1766
|
+
`--memory=${limits.memory}`,
|
|
1767
|
+
`--storage-opt=size=${limits.storage}`,
|
|
1768
|
+
"--user",
|
|
1769
|
+
"1000:1000",
|
|
1770
|
+
"--tmpfs",
|
|
1771
|
+
"/tmp:rw,size=512m",
|
|
1772
|
+
"-v",
|
|
1773
|
+
`${artifact_dir}:/sandbox/artifacts:rw`,
|
|
1774
|
+
"--workdir",
|
|
1775
|
+
"/sandbox",
|
|
1776
|
+
"--entrypoint",
|
|
1777
|
+
"tail"
|
|
1778
|
+
];
|
|
1779
|
+
if (!limits.network) {
|
|
1780
|
+
args.push("--network=none");
|
|
1781
|
+
}
|
|
1782
|
+
args.push(image, "-f", "/dev/null");
|
|
1783
|
+
const result = await runDocker(args, { timeoutMs: 30000 });
|
|
1784
|
+
if (result.exit_code !== 0) {
|
|
1785
|
+
await rm(artifact_dir, { recursive: true, force: true }).catch(() => {});
|
|
1786
|
+
throw new SandboxError(`Failed to create sandbox: ${result.stderr.trim() || "docker run exited " + result.exit_code}`);
|
|
1787
|
+
}
|
|
1788
|
+
return {
|
|
1789
|
+
id,
|
|
1790
|
+
image,
|
|
1791
|
+
artifact_dir,
|
|
1792
|
+
created_at: new Date
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
async function exec(sandbox, cmd, opts = {}) {
|
|
1796
|
+
const cwd = opts.cwd ?? "/sandbox";
|
|
1797
|
+
const timeoutMs = (opts.timeout_seconds ?? DEFAULT_LIMITS.cmd_timeout_seconds) * 1000;
|
|
1798
|
+
const args = ["exec", "--workdir", cwd];
|
|
1799
|
+
for (const [k, v] of Object.entries(opts.env ?? {})) {
|
|
1800
|
+
args.push("--env", `${k}=${v}`);
|
|
1801
|
+
}
|
|
1802
|
+
args.push(sandbox.id, "sh", "-c", cmd);
|
|
1803
|
+
const startedAt = Date.now();
|
|
1804
|
+
const r = await runDocker(args, { timeoutMs });
|
|
1805
|
+
return {
|
|
1806
|
+
stdout: r.stdout,
|
|
1807
|
+
stderr: r.stderr,
|
|
1808
|
+
exit_code: r.exit_code,
|
|
1809
|
+
timed_out: r.timed_out,
|
|
1810
|
+
duration_seconds: (Date.now() - startedAt) / 1000
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
async function destroySandbox(sandbox) {
|
|
1814
|
+
await runDocker(["rm", "-f", sandbox.id], { timeoutMs: 30000 }).catch(() => {});
|
|
1815
|
+
}
|
|
1816
|
+
async function prepareBaseEnvironment(sandbox) {
|
|
1817
|
+
const r = await runDocker([
|
|
1818
|
+
"exec",
|
|
1819
|
+
"--user",
|
|
1820
|
+
"0",
|
|
1821
|
+
sandbox.id,
|
|
1822
|
+
"sh",
|
|
1823
|
+
"-c",
|
|
1824
|
+
"apt-get update -qq && apt-get install -y -qq --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* && mkdir -p /sandbox && chown -R 1000:1000 /sandbox"
|
|
1825
|
+
], { timeoutMs: 180000 });
|
|
1826
|
+
if (r.exit_code !== 0) {
|
|
1827
|
+
throw new SandboxError(`base prep failed (exit ${r.exit_code}): ${r.stderr.trim().slice(0, 500)}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
class SandboxError extends Error {
|
|
1832
|
+
constructor(message) {
|
|
1833
|
+
super(message);
|
|
1834
|
+
this.name = "SandboxError";
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
function runDocker(args, opts) {
|
|
1838
|
+
return new Promise((resolve) => {
|
|
1839
|
+
const proc = spawn2("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1840
|
+
let stdout = "";
|
|
1841
|
+
let stderr = "";
|
|
1842
|
+
let timedOut = false;
|
|
1843
|
+
const timer = setTimeout(() => {
|
|
1844
|
+
timedOut = true;
|
|
1845
|
+
proc.kill("SIGKILL");
|
|
1846
|
+
}, opts.timeoutMs);
|
|
1847
|
+
proc.stdout?.on("data", (c) => {
|
|
1848
|
+
stdout += c.toString("utf8");
|
|
1849
|
+
});
|
|
1850
|
+
proc.stderr?.on("data", (c) => {
|
|
1851
|
+
stderr += c.toString("utf8");
|
|
1852
|
+
});
|
|
1853
|
+
proc.on("error", (err) => {
|
|
1854
|
+
clearTimeout(timer);
|
|
1855
|
+
resolve({
|
|
1856
|
+
stdout,
|
|
1857
|
+
stderr: stderr + `
|
|
1858
|
+
<spawn-error>${err.message}</spawn-error>`,
|
|
1859
|
+
exit_code: -1,
|
|
1860
|
+
timed_out: timedOut
|
|
1861
|
+
});
|
|
1862
|
+
});
|
|
1863
|
+
proc.on("close", (code) => {
|
|
1864
|
+
clearTimeout(timer);
|
|
1865
|
+
resolve({
|
|
1866
|
+
stdout,
|
|
1867
|
+
stderr,
|
|
1868
|
+
exit_code: code ?? -1,
|
|
1869
|
+
timed_out: timedOut
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
function generateId(label) {
|
|
1875
|
+
const suffix = randomBytes2(4).toString("hex");
|
|
1876
|
+
const safe = label.toLowerCase().replace(/[^a-z0-9_-]/g, "-").slice(0, 32);
|
|
1877
|
+
return `${safe}-${suffix}`;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// ../sandbox/dist/orchestrator.js
|
|
1881
|
+
var DEFAULT_PROMPT_HEADER = `You are a Beevibe sandbox child agent running inside a fresh Docker container.
|
|
1882
|
+
|
|
1883
|
+
You have ONLY these five MCP tools to touch the container. They are in your
|
|
1884
|
+
tool list as deferred MCP tools — you MUST load each one via ToolSearch
|
|
1885
|
+
before using it. ToolSearch's "select:" form only accepts ONE tool name per
|
|
1886
|
+
call, so make five separate calls at the very start of your work:
|
|
1887
|
+
|
|
1888
|
+
ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_exec", "max_results": 1 })
|
|
1889
|
+
ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_read_file", "max_results": 1 })
|
|
1890
|
+
ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_write_file", "max_results": 1 })
|
|
1891
|
+
ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_list", "max_results": 1 })
|
|
1892
|
+
ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_export_artifact", "max_results": 1 })
|
|
1893
|
+
|
|
1894
|
+
After those five ToolSearch calls, you may invoke the MCP tools directly:
|
|
1895
|
+
|
|
1896
|
+
mcp__beevibe-sandbox__sandbox_exec(cmd, cwd?, timeout_seconds?)
|
|
1897
|
+
mcp__beevibe-sandbox__sandbox_read_file(path, max_bytes?)
|
|
1898
|
+
mcp__beevibe-sandbox__sandbox_write_file(path, content)
|
|
1899
|
+
mcp__beevibe-sandbox__sandbox_list(path)
|
|
1900
|
+
mcp__beevibe-sandbox__sandbox_export_artifact(sandbox_path, title?)
|
|
1901
|
+
|
|
1902
|
+
You have NO Bash, NO Read, NO Edit, NO Write tool — those will return errors.
|
|
1903
|
+
|
|
1904
|
+
The container has Python 3.12, git, and curl pre-installed. Operate inside
|
|
1905
|
+
/sandbox. Use a project-local venv at /sandbox/venv.
|
|
1906
|
+
|
|
1907
|
+
Your job: use the external repo the user gives you to produce a real artifact
|
|
1908
|
+
for the goal. Plan, install, run, verify, export. You succeed by exporting at
|
|
1909
|
+
least one artifact under /sandbox/artifacts/ via sandbox_export_artifact. Stop
|
|
1910
|
+
as soon as you've exported something useful — don't keep exploring. If you
|
|
1911
|
+
can't, write REASON.txt explaining why and export that.`;
|
|
1912
|
+
function nowIso() {
|
|
1913
|
+
return new Date().toISOString();
|
|
1914
|
+
}
|
|
1915
|
+
var MAX_TRANSCRIPT_EVENTS = 500;
|
|
1916
|
+
var MAX_EVENT_TEXT_BYTES = 4000;
|
|
1917
|
+
function classifyStartupError(err) {
|
|
1918
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
1919
|
+
if (/Cannot connect to the Docker daemon/i.test(raw)) {
|
|
1920
|
+
return "Docker isn't running. Start Docker Desktop and try the run again.";
|
|
1921
|
+
}
|
|
1922
|
+
if (/ENOENT.*docker/i.test(raw)) {
|
|
1923
|
+
return "The `docker` CLI isn't on PATH. Install Docker Desktop (or set DOCKER_HOST).";
|
|
1924
|
+
}
|
|
1925
|
+
if (/no space left on device/i.test(raw)) {
|
|
1926
|
+
return "Docker is out of disk. Prune containers/images or expand Docker Desktop's allotment.";
|
|
1927
|
+
}
|
|
1928
|
+
return raw;
|
|
1929
|
+
}
|
|
1930
|
+
async function runRepoAgent(opts) {
|
|
1931
|
+
const state = {
|
|
1932
|
+
run_id: opts.run_id,
|
|
1933
|
+
status: "starting",
|
|
1934
|
+
repo_url: opts.repo_url,
|
|
1935
|
+
goal: opts.goal,
|
|
1936
|
+
started_at: nowIso(),
|
|
1937
|
+
transcript: [],
|
|
1938
|
+
artifacts: []
|
|
1939
|
+
};
|
|
1940
|
+
const emit = () => opts.on_state?.({ ...state, transcript: state.transcript.slice(), artifacts: state.artifacts.slice() });
|
|
1941
|
+
const log = (kind, text) => {
|
|
1942
|
+
const trimmed = text.length > MAX_EVENT_TEXT_BYTES ? text.slice(0, MAX_EVENT_TEXT_BYTES) + `
|
|
1943
|
+
…[truncated ${text.length - MAX_EVENT_TEXT_BYTES} bytes]…` : text;
|
|
1944
|
+
state.transcript.push({ at: nowIso(), kind, text: trimmed });
|
|
1945
|
+
if (state.transcript.length > MAX_TRANSCRIPT_EVENTS) {
|
|
1946
|
+
const overflow = state.transcript.length - MAX_TRANSCRIPT_EVENTS;
|
|
1947
|
+
state.transcript.splice(1, overflow);
|
|
1948
|
+
const alreadyMarked = state.transcript[1]?.text.startsWith("[log truncated:");
|
|
1949
|
+
if (!alreadyMarked) {
|
|
1950
|
+
state.transcript.splice(1, 0, {
|
|
1951
|
+
at: nowIso(),
|
|
1952
|
+
kind: "log",
|
|
1953
|
+
text: `[log truncated: dropped ${overflow} earlier event${overflow === 1 ? "" : "s"}]`
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
emit();
|
|
1958
|
+
};
|
|
1959
|
+
let sandbox = null;
|
|
1960
|
+
try {
|
|
1961
|
+
log("log", "Creating sandbox container…");
|
|
1962
|
+
state.status = "preparing";
|
|
1963
|
+
emit();
|
|
1964
|
+
try {
|
|
1965
|
+
sandbox = await createSandbox({ label: `bv-run-${opts.run_id}` });
|
|
1966
|
+
} catch (err) {
|
|
1967
|
+
throw new Error(classifyStartupError(err));
|
|
1968
|
+
}
|
|
1969
|
+
state.sandbox_id = sandbox.id;
|
|
1970
|
+
log("log", `Sandbox ${sandbox.id} created (image ${sandbox.image}).`);
|
|
1971
|
+
log("log", "Installing git + curl in the sandbox base image…");
|
|
1972
|
+
await prepareBaseEnvironment(sandbox);
|
|
1973
|
+
log("log", "Base environment ready.");
|
|
1974
|
+
if (opts.input_url) {
|
|
1975
|
+
const filename = opts.input_filename ?? "input.bin";
|
|
1976
|
+
log("log", `Fetching input into /sandbox/inputs/${filename}…`);
|
|
1977
|
+
const r = await exec(sandbox, `mkdir -p /sandbox/inputs && curl -fsSL ${shellQuote(opts.input_url)} -o /sandbox/inputs/${shellQuote(filename)}`, { timeout_seconds: 120 });
|
|
1978
|
+
if (r.exit_code !== 0) {
|
|
1979
|
+
throw new Error(`input fetch failed: ${r.stderr.trim().slice(0, 400)}`);
|
|
1980
|
+
}
|
|
1981
|
+
log("log", "Input file ready.");
|
|
1982
|
+
}
|
|
1983
|
+
const mcpServerCommand = opts.mcp_server_command ?? defaultMcpServerCommand();
|
|
1984
|
+
const mcpConfigPath = join7(sandbox.artifact_dir, "mcp-config.json");
|
|
1985
|
+
const mcpConfig = {
|
|
1986
|
+
mcpServers: {
|
|
1987
|
+
"beevibe-sandbox": {
|
|
1988
|
+
command: mcpServerCommand.command,
|
|
1989
|
+
args: mcpServerCommand.args,
|
|
1990
|
+
env: {
|
|
1991
|
+
BEEVIBE_SANDBOX_ID: sandbox.id,
|
|
1992
|
+
BEEVIBE_SANDBOX_ARTIFACTS: sandbox.artifact_dir
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
1998
|
+
log("log", `MCP config written: ${mcpConfigPath}`);
|
|
1999
|
+
const userPrompt = buildUserPrompt(opts);
|
|
2000
|
+
const systemPromptAppend = DEFAULT_PROMPT_HEADER;
|
|
2001
|
+
state.status = "running";
|
|
2002
|
+
log("log", "Spawning child claude session…");
|
|
2003
|
+
emit();
|
|
2004
|
+
const claudeResult = await runClaude({
|
|
2005
|
+
claudeBin: opts.claude_bin ?? "claude",
|
|
2006
|
+
mcpConfigPath,
|
|
2007
|
+
systemPromptAppend,
|
|
2008
|
+
userPrompt,
|
|
2009
|
+
maxBudgetUsd: opts.max_budget_usd ?? 2,
|
|
2010
|
+
timeoutSeconds: opts.max_runtime_seconds ?? 600,
|
|
2011
|
+
onTranscript: (kind, text) => log(kind, text)
|
|
2012
|
+
});
|
|
2013
|
+
if (claudeResult.exit_code !== 0 && claudeResult.exit_code !== null) {
|
|
2014
|
+
const tail = claudeResult.stderr.slice(-500);
|
|
2015
|
+
log("error", `Child claude exited with code ${claudeResult.exit_code}: ${tail}`);
|
|
2016
|
+
state.status = claudeResult.timed_out ? "blocked" : "failed";
|
|
2017
|
+
state.error = claudeResult.timed_out ? `Run hit the ${opts.max_runtime_seconds ?? 600}s wall-clock budget — agent didn't finish in time.` : `Claude exited ${claudeResult.exit_code}.${tail ? " " + tail.slice(-200) : ""}`;
|
|
2018
|
+
state.finished_at = nowIso();
|
|
2019
|
+
emit();
|
|
2020
|
+
return state;
|
|
2021
|
+
}
|
|
2022
|
+
log("log", "Child claude exited cleanly. Collecting artifacts…");
|
|
2023
|
+
state.artifacts = await collectArtifacts(sandbox);
|
|
2024
|
+
if (state.artifacts.length === 0) {
|
|
2025
|
+
state.status = "blocked";
|
|
2026
|
+
state.error = "agent produced no artifacts";
|
|
2027
|
+
} else {
|
|
2028
|
+
state.status = "succeeded";
|
|
2029
|
+
}
|
|
2030
|
+
state.finished_at = nowIso();
|
|
2031
|
+
emit();
|
|
2032
|
+
return state;
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
log("error", err instanceof Error ? err.message : String(err));
|
|
2035
|
+
state.status = "failed";
|
|
2036
|
+
state.error = err instanceof Error ? err.message : String(err);
|
|
2037
|
+
state.finished_at = nowIso();
|
|
2038
|
+
emit();
|
|
2039
|
+
return state;
|
|
2040
|
+
} finally {
|
|
2041
|
+
if (sandbox) {
|
|
2042
|
+
try {
|
|
2043
|
+
log("log", `Destroying sandbox ${sandbox.id}…`);
|
|
2044
|
+
await destroySandbox(sandbox);
|
|
2045
|
+
} catch (err) {
|
|
2046
|
+
log("log", `Sandbox cleanup note: ${err instanceof Error ? err.message : String(err)}. Run with \`docker ps -a --filter name=bv-run-\` to inspect.`);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
function buildUserPrompt(opts) {
|
|
2052
|
+
const lines = [
|
|
2053
|
+
`Goal: ${opts.goal}`,
|
|
2054
|
+
`Repo: ${opts.repo_url}`
|
|
2055
|
+
];
|
|
2056
|
+
if (opts.input_url) {
|
|
2057
|
+
lines.push(`Input file (pre-fetched): /sandbox/inputs/${opts.input_filename ?? "input.bin"}`);
|
|
2058
|
+
}
|
|
2059
|
+
lines.push("");
|
|
2060
|
+
lines.push("Work inside /sandbox. Clone the repo to /sandbox/repo. Install in a venv. ");
|
|
2061
|
+
lines.push("Produce at least one artifact under /sandbox/artifacts/, then call ");
|
|
2062
|
+
lines.push("sandbox_export_artifact() on each one with a short, human-readable title. ");
|
|
2063
|
+
lines.push("Stop as soon as you've exported a useful artifact — don't keep exploring.");
|
|
2064
|
+
return lines.join(`
|
|
2065
|
+
`);
|
|
2066
|
+
}
|
|
2067
|
+
var ALLOWED_TOOLS = [
|
|
2068
|
+
"mcp__beevibe-sandbox__sandbox_exec",
|
|
2069
|
+
"mcp__beevibe-sandbox__sandbox_read_file",
|
|
2070
|
+
"mcp__beevibe-sandbox__sandbox_write_file",
|
|
2071
|
+
"mcp__beevibe-sandbox__sandbox_list",
|
|
2072
|
+
"mcp__beevibe-sandbox__sandbox_export_artifact"
|
|
2073
|
+
];
|
|
2074
|
+
var DISALLOWED_TOOLS = ["Bash", "Read", "Edit", "Write", "BashOutput", "KillBash"];
|
|
2075
|
+
function runClaude(args) {
|
|
2076
|
+
return new Promise((resolve2) => {
|
|
2077
|
+
const cliArgs = [
|
|
2078
|
+
"--print",
|
|
2079
|
+
"--mcp-config",
|
|
2080
|
+
args.mcpConfigPath,
|
|
2081
|
+
"--allowed-tools",
|
|
2082
|
+
ALLOWED_TOOLS.join(","),
|
|
2083
|
+
"--disallowed-tools",
|
|
2084
|
+
DISALLOWED_TOOLS.join(","),
|
|
2085
|
+
"--append-system-prompt",
|
|
2086
|
+
args.systemPromptAppend,
|
|
2087
|
+
"--max-budget-usd",
|
|
2088
|
+
String(args.maxBudgetUsd),
|
|
2089
|
+
"--output-format",
|
|
2090
|
+
"stream-json",
|
|
2091
|
+
"--verbose"
|
|
2092
|
+
];
|
|
2093
|
+
const proc = spawn3(args.claudeBin, cliArgs, {
|
|
2094
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2095
|
+
env: { ...process.env },
|
|
2096
|
+
detached: true
|
|
2097
|
+
});
|
|
2098
|
+
proc.unref();
|
|
2099
|
+
let stdout = "";
|
|
2100
|
+
let stderr = "";
|
|
2101
|
+
let timedOut = false;
|
|
2102
|
+
let stdoutBuffer = "";
|
|
2103
|
+
const timer = setTimeout(() => {
|
|
2104
|
+
timedOut = true;
|
|
2105
|
+
proc.kill("SIGTERM");
|
|
2106
|
+
setTimeout(() => proc.kill("SIGKILL"), 5000);
|
|
2107
|
+
}, args.timeoutSeconds * 1000);
|
|
2108
|
+
proc.stdout.on("data", (chunk) => {
|
|
2109
|
+
const s = chunk.toString("utf8");
|
|
2110
|
+
stdout += s;
|
|
2111
|
+
stdoutBuffer += s;
|
|
2112
|
+
let nl;
|
|
2113
|
+
while ((nl = stdoutBuffer.indexOf(`
|
|
2114
|
+
`)) !== -1) {
|
|
2115
|
+
const line = stdoutBuffer.slice(0, nl);
|
|
2116
|
+
stdoutBuffer = stdoutBuffer.slice(nl + 1);
|
|
2117
|
+
if (line.trim().length === 0)
|
|
2118
|
+
continue;
|
|
2119
|
+
try {
|
|
2120
|
+
const evt = JSON.parse(line);
|
|
2121
|
+
if (evt && typeof evt === "object" && evt.type === "system" && evt.subtype === "init") {
|
|
2122
|
+
const init = evt;
|
|
2123
|
+
const servers = init.mcp_servers;
|
|
2124
|
+
if (servers) {
|
|
2125
|
+
for (const sv of servers) {
|
|
2126
|
+
args.onTranscript("log", `mcp server ${sv.name}: ${sv.status}`);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
const tools = init.tools;
|
|
2130
|
+
const mcpTools = (tools ?? []).filter((t) => t.startsWith("mcp__beevibe-sandbox__"));
|
|
2131
|
+
args.onTranscript("log", `mcp tools exposed: ${mcpTools.length ? mcpTools.join(", ") : "none"}`);
|
|
2132
|
+
}
|
|
2133
|
+
handleStreamEvent(evt, args.onTranscript);
|
|
2134
|
+
} catch {}
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
proc.stderr.on("data", (chunk) => {
|
|
2138
|
+
stderr += chunk.toString("utf8");
|
|
2139
|
+
});
|
|
2140
|
+
proc.on("error", (err) => {
|
|
2141
|
+
clearTimeout(timer);
|
|
2142
|
+
const friendly = /ENOENT/i.test(err.message) ? `Claude CLI not found at ${args.claudeBin}. Set BEEVIBE_CLAUDE_BIN to the binary path.` : `claude spawn error: ${err.message}`;
|
|
2143
|
+
args.onTranscript("error", friendly);
|
|
2144
|
+
resolve2({ exit_code: -1, stdout, stderr, timed_out: timedOut });
|
|
2145
|
+
});
|
|
2146
|
+
proc.on("close", (code) => {
|
|
2147
|
+
clearTimeout(timer);
|
|
2148
|
+
if (code !== 0 && code !== null) {
|
|
2149
|
+
const tail = stderr.slice(-800).trim();
|
|
2150
|
+
if (tail) {
|
|
2151
|
+
args.onTranscript("error", `claude stderr tail: ${tail}`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
resolve2({ exit_code: code, stdout, stderr, timed_out: timedOut });
|
|
2155
|
+
});
|
|
2156
|
+
proc.stdin.write(args.userPrompt);
|
|
2157
|
+
proc.stdin.end();
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
function handleStreamEvent(evt, emit) {
|
|
2161
|
+
if (!evt || typeof evt !== "object")
|
|
2162
|
+
return;
|
|
2163
|
+
const e = evt;
|
|
2164
|
+
const t = e.type;
|
|
2165
|
+
if (t === "assistant" || t === "assistant_message") {
|
|
2166
|
+
const message = e.message;
|
|
2167
|
+
if (message && Array.isArray(message.content)) {
|
|
2168
|
+
for (const item of message.content) {
|
|
2169
|
+
if (!item || typeof item !== "object")
|
|
2170
|
+
continue;
|
|
2171
|
+
const it = item;
|
|
2172
|
+
if (it.type === "text" && typeof it.text === "string") {
|
|
2173
|
+
emit("agent", it.text);
|
|
2174
|
+
} else if (it.type === "tool_use") {
|
|
2175
|
+
const name = String(it.name ?? "unknown");
|
|
2176
|
+
const input = it.input ? compactJson(it.input) : "";
|
|
2177
|
+
emit("tool_call", `${name}(${input})`);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
} else if (typeof e.content === "string") {
|
|
2181
|
+
emit("agent", e.content);
|
|
2182
|
+
}
|
|
2183
|
+
} else if (t === "user" || t === "user_message") {
|
|
2184
|
+
const message = e.message;
|
|
2185
|
+
if (message && Array.isArray(message.content)) {
|
|
2186
|
+
for (const item of message.content) {
|
|
2187
|
+
if (!item || typeof item !== "object")
|
|
2188
|
+
continue;
|
|
2189
|
+
const it = item;
|
|
2190
|
+
if (it.type === "tool_result") {
|
|
2191
|
+
const content = it.content;
|
|
2192
|
+
let text;
|
|
2193
|
+
if (typeof content === "string") {
|
|
2194
|
+
text = content;
|
|
2195
|
+
} else if (Array.isArray(content)) {
|
|
2196
|
+
text = content.map((c) => c && typeof c === "object" && typeof c.text === "string" ? c.text : "").filter(Boolean).join(" ");
|
|
2197
|
+
} else {
|
|
2198
|
+
text = JSON.stringify(content ?? null);
|
|
2199
|
+
}
|
|
2200
|
+
const isErr = it.is_error === true;
|
|
2201
|
+
emit(isErr ? "error" : "tool_call", `→ ${text.slice(0, 300)}${text.length > 300 ? "…" : ""}`);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
} else if (t === "result" && typeof e.result === "string") {
|
|
2206
|
+
emit("agent", e.result);
|
|
2207
|
+
} else if (t === "error") {
|
|
2208
|
+
emit("error", typeof e.message === "string" ? e.message : "claude error");
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
function compactJson(v) {
|
|
2212
|
+
try {
|
|
2213
|
+
const s = JSON.stringify(v);
|
|
2214
|
+
return s.length > 200 ? s.slice(0, 200) + "…" : s;
|
|
2215
|
+
} catch {
|
|
2216
|
+
return "?";
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
async function collectArtifacts(sandbox) {
|
|
2220
|
+
const out = [];
|
|
2221
|
+
await mkdir2(sandbox.artifact_dir, { recursive: true });
|
|
2222
|
+
const entries = await readdir(sandbox.artifact_dir);
|
|
2223
|
+
const sidecars = new Map;
|
|
2224
|
+
for (const e of entries) {
|
|
2225
|
+
if (e.endsWith(".meta.json")) {
|
|
2226
|
+
try {
|
|
2227
|
+
const raw = await readFile2(join7(sandbox.artifact_dir, e), "utf8");
|
|
2228
|
+
sidecars.set(e.replace(/\.meta\.json$/, ""), JSON.parse(raw));
|
|
2229
|
+
} catch {}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
for (const e of entries) {
|
|
2233
|
+
if (e.endsWith(".meta.json") || e === "mcp-config.json")
|
|
2234
|
+
continue;
|
|
2235
|
+
const hostPath = join7(sandbox.artifact_dir, e);
|
|
2236
|
+
const stat = await readFile2(hostPath).then((b) => ({ size: b.byteLength }), () => null);
|
|
2237
|
+
if (!stat)
|
|
2238
|
+
continue;
|
|
2239
|
+
const meta = sidecars.get(e);
|
|
2240
|
+
out.push({
|
|
2241
|
+
filename: e,
|
|
2242
|
+
title: typeof meta?.title === "string" && meta.title || e.replace(/\.[^.]+$/, ""),
|
|
2243
|
+
size_bytes: stat.size,
|
|
2244
|
+
host_path: hostPath,
|
|
2245
|
+
sandbox_path: typeof meta?.sandbox_path === "string" && meta.sandbox_path || undefined
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
return out;
|
|
2249
|
+
}
|
|
2250
|
+
function defaultMcpServerCommand() {
|
|
2251
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
2252
|
+
const isTsxRun = here.endsWith("/src");
|
|
2253
|
+
const mcpServerPath = resolve(here, isTsxRun ? "./mcp-server.ts" : "./mcp-server.js");
|
|
2254
|
+
return isTsxRun ? { command: "npx", args: ["--no-install", "tsx", mcpServerPath] } : { command: "node", args: ["--enable-source-maps", mcpServerPath] };
|
|
2255
|
+
}
|
|
2256
|
+
function shellQuote(s) {
|
|
2257
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// src/repo-runs.ts
|
|
2261
|
+
var CLAUDE_BIN = process.env.BEEVIBE_CLAUDE_BIN ?? "claude";
|
|
2262
|
+
async function runRepoDispatch(deps, payload, abortSignal) {
|
|
2263
|
+
const rr = payload.run_repo;
|
|
2264
|
+
if (!rr) {
|
|
2265
|
+
throw new Error("run_repo dispatch missing payload.run_repo");
|
|
2266
|
+
}
|
|
2267
|
+
console.log(`[daemon/repo-run] sess=${payload.session_id} repo_run=${rr.repo_run_id} repo=${rr.repo_url}`);
|
|
2268
|
+
const buffer = [];
|
|
2269
|
+
let flushTimer;
|
|
2270
|
+
const flush = async () => {
|
|
2271
|
+
if (buffer.length === 0)
|
|
2272
|
+
return;
|
|
2273
|
+
const events = buffer.splice(0);
|
|
2274
|
+
try {
|
|
2275
|
+
await deps.api.post("/runtime/events", { events });
|
|
2276
|
+
} catch (err) {
|
|
2277
|
+
console.warn("[daemon/repo-run] /runtime/events POST failed:", err instanceof Error ? err.message : String(err));
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
const scheduleFlush = () => {
|
|
2281
|
+
if (flushTimer)
|
|
2282
|
+
return;
|
|
2283
|
+
flushTimer = setTimeout(() => {
|
|
2284
|
+
flushTimer = undefined;
|
|
2285
|
+
flush();
|
|
2286
|
+
}, 250);
|
|
2287
|
+
};
|
|
2288
|
+
let pushedUpTo = 0;
|
|
2289
|
+
const pushNew = (transcript) => {
|
|
2290
|
+
for (let i = pushedUpTo;i < transcript.length; i++) {
|
|
2291
|
+
const ev = transcript[i];
|
|
2292
|
+
if (!ev)
|
|
2293
|
+
continue;
|
|
2294
|
+
buffer.push({
|
|
2295
|
+
session_id: payload.session_id,
|
|
2296
|
+
kind: mapKind(ev.kind),
|
|
2297
|
+
content: ev.text
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
pushedUpTo = transcript.length;
|
|
2301
|
+
if (buffer.length >= 16)
|
|
2302
|
+
flush();
|
|
2303
|
+
else
|
|
2304
|
+
scheduleFlush();
|
|
2305
|
+
};
|
|
2306
|
+
const installLines = [];
|
|
2307
|
+
let invocation;
|
|
2308
|
+
const noteCommandFromToolCall = (text) => {
|
|
2309
|
+
const m = text.match(/sandbox_exec\(\{?"?cmd"?:?\s*"([^"]+)"/);
|
|
2310
|
+
if (!m)
|
|
2311
|
+
return;
|
|
2312
|
+
const cmd = m[1] ?? "";
|
|
2313
|
+
if (!cmd)
|
|
2314
|
+
return;
|
|
2315
|
+
if (isInstallCommand(cmd))
|
|
2316
|
+
installLines.push(cmd);
|
|
2317
|
+
else
|
|
2318
|
+
invocation = cmd;
|
|
2319
|
+
};
|
|
2320
|
+
const result = await runRepoAgent({
|
|
2321
|
+
run_id: rr.repo_run_id,
|
|
2322
|
+
repo_url: rr.repo_url,
|
|
2323
|
+
goal: rr.goal,
|
|
2324
|
+
input_url: rr.input_url,
|
|
2325
|
+
input_filename: rr.input_filename,
|
|
2326
|
+
claude_bin: CLAUDE_BIN,
|
|
2327
|
+
max_runtime_seconds: (rr.limits?.wall_clock_minutes ?? 20) * 60,
|
|
2328
|
+
on_state: (s) => {
|
|
2329
|
+
pushNew(s.transcript);
|
|
2330
|
+
for (let i = pushedUpTo - (s.transcript.length - 0);i < s.transcript.length; i++) {
|
|
2331
|
+
if (i < 0)
|
|
2332
|
+
continue;
|
|
2333
|
+
const ev = s.transcript[i];
|
|
2334
|
+
if (ev?.kind === "tool_call")
|
|
2335
|
+
noteCommandFromToolCall(ev.text);
|
|
2336
|
+
}
|
|
2337
|
+
if (abortSignal?.aborted) {
|
|
2338
|
+
buffer.push({
|
|
2339
|
+
session_id: payload.session_id,
|
|
2340
|
+
kind: "summary",
|
|
2341
|
+
content: "[run cancelled by user]"
|
|
2342
|
+
});
|
|
2343
|
+
flush();
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
if (flushTimer) {
|
|
2348
|
+
clearTimeout(flushTimer);
|
|
2349
|
+
flushTimer = undefined;
|
|
2350
|
+
}
|
|
2351
|
+
await flush();
|
|
2352
|
+
const status = result.status === "succeeded" ? "succeeded" : result.status === "blocked" ? "failed" : result.status === "failed" ? "failed" : "failed";
|
|
2353
|
+
const artifacts = result.artifacts.map((a) => ({
|
|
2354
|
+
filename: a.filename,
|
|
2355
|
+
title: a.title,
|
|
2356
|
+
size_bytes: a.size_bytes,
|
|
2357
|
+
host_path: a.host_path,
|
|
2358
|
+
sandbox_path: a.sandbox_path
|
|
2359
|
+
}));
|
|
2360
|
+
const done = {
|
|
2361
|
+
session_id: payload.session_id,
|
|
2362
|
+
status,
|
|
2363
|
+
result_summary: result.status === "succeeded" ? `Exported ${artifacts.length} artifact${artifacts.length === 1 ? "" : "s"}.` : result.error ?? "Run did not produce an artifact.",
|
|
2364
|
+
error: result.error,
|
|
2365
|
+
run_repo: {
|
|
2366
|
+
repo_run_id: rr.repo_run_id,
|
|
2367
|
+
install_log: installLines.length ? installLines.join(`
|
|
2368
|
+
`) : undefined,
|
|
2369
|
+
invocation,
|
|
2370
|
+
artifacts
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
if (status === "succeeded") {
|
|
2374
|
+
console.log(`[daemon/repo-run] sess=${payload.session_id} succeeded artifacts=${artifacts.length}`);
|
|
2375
|
+
} else {
|
|
2376
|
+
console.error(`[daemon/repo-run] sess=${payload.session_id} status=${status}` + (result.error ? `
|
|
2377
|
+
error:
|
|
2378
|
+
${result.error.split(`
|
|
2379
|
+
`).join(`
|
|
2380
|
+
`)}` : ""));
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
await deps.api.post("/runtime/done", done);
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
console.error("[daemon/repo-run] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function mapKind(kind) {
|
|
2389
|
+
switch (kind) {
|
|
2390
|
+
case "agent":
|
|
2391
|
+
return "agent";
|
|
2392
|
+
case "tool_call":
|
|
2393
|
+
return "tool_call";
|
|
2394
|
+
case "log":
|
|
2395
|
+
return "summary";
|
|
2396
|
+
case "error":
|
|
2397
|
+
return "tool_result";
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function isInstallCommand(cmd) {
|
|
2401
|
+
return /^(pip\s|pip3\s|apt-get\s|apt\s|brew\s|npm\s+(install|i)|yarn\s+(add|install)|pnpm\s+(add|install)|git\s+clone)/.test(cmd.trim());
|
|
2402
|
+
}
|
|
2403
|
+
|
|
1735
2404
|
// src/spawner.ts
|
|
1736
2405
|
async function runDispatch(deps, payload, abortSignal) {
|
|
2406
|
+
if (payload.type === "run_repo") {
|
|
2407
|
+
await runRepoDispatch({ api: deps.api }, payload, abortSignal);
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
1737
2410
|
const syntheticAgent = {
|
|
1738
2411
|
id: payload.agent_id,
|
|
1739
2412
|
api_key: payload.agent_api_key,
|
|
@@ -1745,7 +2418,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
1745
2418
|
const registry = deps.runtimeRegistry ?? createDefaultRuntimeRegistry();
|
|
1746
2419
|
const runtime3 = registry[payload.runtime_type];
|
|
1747
2420
|
if (!runtime3) {
|
|
1748
|
-
throw new Error(
|
|
2421
|
+
throw new Error(runtimeMissingError(payload.runtime_type));
|
|
1749
2422
|
}
|
|
1750
2423
|
const buffer = [];
|
|
1751
2424
|
let flushTimer;
|
|
@@ -1788,7 +2461,6 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
1788
2461
|
system_prompt_append: payload.system_prompt_append,
|
|
1789
2462
|
model: payload.model,
|
|
1790
2463
|
max_turns: payload.max_turns,
|
|
1791
|
-
disallowed_tools: payload.disallowed_tools,
|
|
1792
2464
|
env: payload.env,
|
|
1793
2465
|
resume_session_id: payload.resume_session_id,
|
|
1794
2466
|
abort_signal: abortSignal,
|
|
@@ -1964,14 +2636,14 @@ class Claimer {
|
|
|
1964
2636
|
// src/skills-cache.ts
|
|
1965
2637
|
import { promises as fs2 } from "node:fs";
|
|
1966
2638
|
import { homedir as homedir3 } from "node:os";
|
|
1967
|
-
import { join as
|
|
2639
|
+
import { join as join8 } from "node:path";
|
|
1968
2640
|
function skillsCacheDir() {
|
|
1969
|
-
return
|
|
2641
|
+
return join8(homedir3(), ".beevibe", "skills");
|
|
1970
2642
|
}
|
|
1971
2643
|
var VERSION_FILE = ".version";
|
|
1972
2644
|
async function readCachedVersion() {
|
|
1973
2645
|
try {
|
|
1974
|
-
return (await fs2.readFile(
|
|
2646
|
+
return (await fs2.readFile(join8(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
|
|
1975
2647
|
} catch {
|
|
1976
2648
|
return;
|
|
1977
2649
|
}
|
|
@@ -1992,19 +2664,19 @@ async function syncSkillsCache(api) {
|
|
|
1992
2664
|
if (!dirent.isDirectory())
|
|
1993
2665
|
continue;
|
|
1994
2666
|
if (dirent.name === "beevibe" || dirent.name.startsWith("beevibe-")) {
|
|
1995
|
-
await fs2.rm(
|
|
2667
|
+
await fs2.rm(join8(cache, dirent.name), { recursive: true, force: true });
|
|
1996
2668
|
}
|
|
1997
2669
|
}
|
|
1998
2670
|
for (const skill of res.skills) {
|
|
1999
|
-
const skillDir =
|
|
2671
|
+
const skillDir = join8(cache, skill.name);
|
|
2000
2672
|
await fs2.mkdir(skillDir, { recursive: true, mode: 448 });
|
|
2001
2673
|
for (const file of skill.files) {
|
|
2002
|
-
const filePath =
|
|
2003
|
-
await fs2.mkdir(
|
|
2674
|
+
const filePath = join8(skillDir, file.path);
|
|
2675
|
+
await fs2.mkdir(join8(filePath, ".."), { recursive: true });
|
|
2004
2676
|
await fs2.writeFile(filePath, file.content, { mode: 384 });
|
|
2005
2677
|
}
|
|
2006
2678
|
}
|
|
2007
|
-
await fs2.writeFile(
|
|
2679
|
+
await fs2.writeFile(join8(cache, VERSION_FILE), res.version, { mode: 384 });
|
|
2008
2680
|
return cache;
|
|
2009
2681
|
}
|
|
2010
2682
|
|
|
@@ -2137,8 +2809,8 @@ async function runSync() {
|
|
|
2137
2809
|
// src/update.ts
|
|
2138
2810
|
import { createHash } from "node:crypto";
|
|
2139
2811
|
import { createWriteStream, mkdtempSync, rmSync as rmSync2, chmodSync, renameSync } from "node:fs";
|
|
2140
|
-
import { tmpdir as
|
|
2141
|
-
import { join as
|
|
2812
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
2813
|
+
import { join as join9 } from "node:path";
|
|
2142
2814
|
import { Readable } from "node:stream";
|
|
2143
2815
|
import { pipeline } from "node:stream/promises";
|
|
2144
2816
|
import { createInterface } from "node:readline/promises";
|
|
@@ -2152,7 +2824,7 @@ var PLATFORM_ASSETS = {
|
|
|
2152
2824
|
"linux-arm64": "beevibe-daemon-linux-arm64"
|
|
2153
2825
|
};
|
|
2154
2826
|
function currentVersion() {
|
|
2155
|
-
return "0.1.
|
|
2827
|
+
return "0.1.4";
|
|
2156
2828
|
}
|
|
2157
2829
|
function isCompiledBinary() {
|
|
2158
2830
|
if (!process.versions.bun)
|
|
@@ -2265,8 +2937,8 @@ async function runUpdate(opts = {}) {
|
|
|
2265
2937
|
return;
|
|
2266
2938
|
}
|
|
2267
2939
|
}
|
|
2268
|
-
const stagingDir = mkdtempSync(
|
|
2269
|
-
const stagingPath =
|
|
2940
|
+
const stagingDir = mkdtempSync(join9(tmpdir5(), "beevibe-daemon-update-"));
|
|
2941
|
+
const stagingPath = join9(stagingDir, asset);
|
|
2270
2942
|
try {
|
|
2271
2943
|
console.log(`Downloading ${asset}…`);
|
|
2272
2944
|
const downloadUrl = `${DOWNLOAD_BASE}/${latest}/${asset}`;
|
package/package.json
CHANGED