@bunny-agent/runner-cli 0.9.28 → 0.9.29-beta.0
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/bundle.mjs +320 -11
- package/package.json +4 -4
package/dist/bundle.mjs
CHANGED
|
@@ -1406,18 +1406,139 @@ var generateImageSchema = {
|
|
|
1406
1406
|
required: ["prompt"],
|
|
1407
1407
|
additionalProperties: false
|
|
1408
1408
|
};
|
|
1409
|
-
async function resolveB64(item) {
|
|
1409
|
+
async function resolveB64(item, apiKey) {
|
|
1410
1410
|
if (item.b64_json)
|
|
1411
1411
|
return item.b64_json;
|
|
1412
|
-
if (item.
|
|
1413
|
-
|
|
1412
|
+
if (item.b64Json)
|
|
1413
|
+
return item.b64Json;
|
|
1414
|
+
if (item.image_base64)
|
|
1415
|
+
return item.image_base64;
|
|
1416
|
+
if (item.imageBase64)
|
|
1417
|
+
return item.imageBase64;
|
|
1418
|
+
if (item.base64)
|
|
1419
|
+
return item.base64;
|
|
1420
|
+
if (typeof item.image === "string")
|
|
1421
|
+
return item.image;
|
|
1422
|
+
if (item.image?.b64_json)
|
|
1423
|
+
return item.image.b64_json;
|
|
1424
|
+
if (item.image?.base64)
|
|
1425
|
+
return item.image.base64;
|
|
1426
|
+
const url = item.url ?? item.image_url ?? item.imageUrl ?? item.image?.url;
|
|
1427
|
+
if (url) {
|
|
1428
|
+
const headers = {};
|
|
1429
|
+
if (apiKey) {
|
|
1430
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
1431
|
+
}
|
|
1432
|
+
const res = await fetch(url, { headers });
|
|
1414
1433
|
if (res.ok)
|
|
1415
1434
|
return Buffer.from(await res.arrayBuffer()).toString("base64");
|
|
1416
1435
|
}
|
|
1417
1436
|
return void 0;
|
|
1418
1437
|
}
|
|
1419
|
-
|
|
1420
|
-
const
|
|
1438
|
+
function pickImageItem(response) {
|
|
1439
|
+
const tryFromObject = (value) => {
|
|
1440
|
+
if (!value || typeof value !== "object")
|
|
1441
|
+
return void 0;
|
|
1442
|
+
const obj = value;
|
|
1443
|
+
return {
|
|
1444
|
+
b64_json: obj.b64_json ?? obj.b64Json,
|
|
1445
|
+
b64Json: obj.b64Json,
|
|
1446
|
+
url: obj.url ?? obj.imageUrl,
|
|
1447
|
+
image_base64: obj.image_base64 ?? obj.imageBase64,
|
|
1448
|
+
imageBase64: obj.imageBase64,
|
|
1449
|
+
image_url: obj.image_url ?? obj.imageUrl,
|
|
1450
|
+
imageUrl: obj.imageUrl,
|
|
1451
|
+
base64: obj.base64,
|
|
1452
|
+
image: obj.image
|
|
1453
|
+
};
|
|
1454
|
+
};
|
|
1455
|
+
const asItem = (value) => {
|
|
1456
|
+
if (value == null)
|
|
1457
|
+
return void 0;
|
|
1458
|
+
if (typeof value === "string") {
|
|
1459
|
+
return { base64: value };
|
|
1460
|
+
}
|
|
1461
|
+
if (typeof value === "object") {
|
|
1462
|
+
const normalized = tryFromObject(value);
|
|
1463
|
+
if (normalized)
|
|
1464
|
+
return normalized;
|
|
1465
|
+
}
|
|
1466
|
+
return void 0;
|
|
1467
|
+
};
|
|
1468
|
+
const fromDataArray = Array.isArray(response.data) ? asItem(response.data[0]) : void 0;
|
|
1469
|
+
if (fromDataArray)
|
|
1470
|
+
return fromDataArray;
|
|
1471
|
+
const fromDataValue = asItem(response.data);
|
|
1472
|
+
if (fromDataValue)
|
|
1473
|
+
return fromDataValue;
|
|
1474
|
+
const responseRecord = response;
|
|
1475
|
+
const imagesValue = responseRecord.images;
|
|
1476
|
+
const outputValue = responseRecord.output;
|
|
1477
|
+
const fromImagesArray = Array.isArray(imagesValue) ? asItem(imagesValue[0]) : void 0;
|
|
1478
|
+
if (fromImagesArray)
|
|
1479
|
+
return fromImagesArray;
|
|
1480
|
+
const fromImagesValue = asItem(imagesValue);
|
|
1481
|
+
if (fromImagesValue)
|
|
1482
|
+
return fromImagesValue;
|
|
1483
|
+
const fromOutputArray = Array.isArray(outputValue) ? asItem(outputValue[0]) : void 0;
|
|
1484
|
+
if (fromOutputArray)
|
|
1485
|
+
return fromOutputArray;
|
|
1486
|
+
const fromOutputValue = asItem(outputValue);
|
|
1487
|
+
if (fromOutputValue)
|
|
1488
|
+
return fromOutputValue;
|
|
1489
|
+
const fromTopLevel = asItem(response);
|
|
1490
|
+
if (fromTopLevel)
|
|
1491
|
+
return fromTopLevel;
|
|
1492
|
+
const queue = [response];
|
|
1493
|
+
while (queue.length > 0) {
|
|
1494
|
+
const current = queue.shift();
|
|
1495
|
+
if (current == null)
|
|
1496
|
+
continue;
|
|
1497
|
+
if (typeof current === "string") {
|
|
1498
|
+
if (/^[A-Za-z0-9+/=]{32,}$/.test(current))
|
|
1499
|
+
return { base64: current };
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (typeof current !== "object")
|
|
1503
|
+
continue;
|
|
1504
|
+
const normalized = tryFromObject(current);
|
|
1505
|
+
if (normalized) {
|
|
1506
|
+
const hasUsefulField = Boolean(normalized.b64_json ?? normalized.b64Json ?? normalized.image_base64 ?? normalized.imageBase64 ?? normalized.base64 ?? normalized.url ?? normalized.image_url ?? normalized.imageUrl ?? (typeof normalized.image === "string" ? normalized.image : normalized.image?.b64_json ?? normalized.image?.base64 ?? normalized.image?.url));
|
|
1507
|
+
if (hasUsefulField)
|
|
1508
|
+
return normalized;
|
|
1509
|
+
}
|
|
1510
|
+
if (Array.isArray(current)) {
|
|
1511
|
+
queue.push(...current);
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
for (const value of Object.values(current)) {
|
|
1515
|
+
queue.push(value);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return {};
|
|
1519
|
+
}
|
|
1520
|
+
function detectImageMime(filePath) {
|
|
1521
|
+
const ext = extname(filePath).toLowerCase();
|
|
1522
|
+
if (ext === ".jpg" || ext === ".jpeg")
|
|
1523
|
+
return "image/jpeg";
|
|
1524
|
+
if (ext === ".webp")
|
|
1525
|
+
return "image/webp";
|
|
1526
|
+
if (ext === ".gif")
|
|
1527
|
+
return "image/gif";
|
|
1528
|
+
return "image/png";
|
|
1529
|
+
}
|
|
1530
|
+
function buildPolicySafeEditPrompt(prompt) {
|
|
1531
|
+
const riskyPattern = /\b(watermark|watermarks|logo|logos|copyright|brand mark|remove branding)\b/i;
|
|
1532
|
+
if (!riskyPattern.test(prompt)) {
|
|
1533
|
+
return { prompt, rewritten: false };
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
prompt: "Clean up distracting overlay text or marks naturally while preserving the original scene, style, and layout. Keep the result seamless and high quality.",
|
|
1537
|
+
rewritten: true
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
async function saveImageItem(item, filePath, apiKey) {
|
|
1541
|
+
const b64 = await resolveB64(item, apiKey);
|
|
1421
1542
|
if (!b64)
|
|
1422
1543
|
return void 0;
|
|
1423
1544
|
mkdirSync2(dirname3(filePath), { recursive: true });
|
|
@@ -1458,20 +1579,22 @@ function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
|
|
|
1458
1579
|
prompt,
|
|
1459
1580
|
n: 1,
|
|
1460
1581
|
size,
|
|
1461
|
-
quality
|
|
1582
|
+
quality,
|
|
1583
|
+
response_format: "b64_json",
|
|
1584
|
+
output_format: "png"
|
|
1462
1585
|
})
|
|
1463
1586
|
});
|
|
1464
1587
|
if (!res.ok) {
|
|
1465
1588
|
throw new Error(`Image generation failed (${res.status}): ${await res.text()}`);
|
|
1466
1589
|
}
|
|
1467
1590
|
const json = await res.json();
|
|
1468
|
-
const item = json
|
|
1469
|
-
const savedPath = await saveImageItem(item, filePath);
|
|
1591
|
+
const item = pickImageItem(json);
|
|
1592
|
+
const savedPath = await saveImageItem(item, filePath, apiKey);
|
|
1470
1593
|
return {
|
|
1471
1594
|
content: [
|
|
1472
1595
|
{
|
|
1473
1596
|
type: "text",
|
|
1474
|
-
text: savedPath ??
|
|
1597
|
+
text: savedPath ?? `Image generated but could not be saved: no image payload returned; image_model: ${imageModelId}`
|
|
1475
1598
|
}
|
|
1476
1599
|
],
|
|
1477
1600
|
details: {
|
|
@@ -1491,6 +1614,192 @@ function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
|
|
|
1491
1614
|
}
|
|
1492
1615
|
};
|
|
1493
1616
|
}
|
|
1617
|
+
var editImageSchema = {
|
|
1618
|
+
type: "object",
|
|
1619
|
+
properties: {
|
|
1620
|
+
image: {
|
|
1621
|
+
type: "string",
|
|
1622
|
+
description: "Path to the source image file to edit (relative to working directory or absolute)."
|
|
1623
|
+
},
|
|
1624
|
+
prompt: {
|
|
1625
|
+
type: "string",
|
|
1626
|
+
description: "Text description of the desired final image. Describe the full result, not just the change."
|
|
1627
|
+
},
|
|
1628
|
+
mask: {
|
|
1629
|
+
type: "string",
|
|
1630
|
+
description: "Optional path to a mask image (PNG with transparent areas indicating where to edit). If omitted, the model decides what to change based on the prompt."
|
|
1631
|
+
},
|
|
1632
|
+
filename: {
|
|
1633
|
+
type: "string",
|
|
1634
|
+
description: "Output filename with extension, e.g. 'edited_cat.png'. Defaults to a timestamp-based name."
|
|
1635
|
+
},
|
|
1636
|
+
size: {
|
|
1637
|
+
type: "string",
|
|
1638
|
+
enum: ["1024x1024", "1024x1536", "1536x1024", "auto"],
|
|
1639
|
+
description: "Output image dimensions. Optional; omit or set auto to let model decide."
|
|
1640
|
+
},
|
|
1641
|
+
quality: {
|
|
1642
|
+
type: "string",
|
|
1643
|
+
enum: ["low", "medium", "high", "auto"],
|
|
1644
|
+
description: "Image quality. Optional; omit or set auto to let model decide."
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
required: ["image", "prompt"],
|
|
1648
|
+
additionalProperties: false
|
|
1649
|
+
};
|
|
1650
|
+
function buildMultipartBody(fields, files) {
|
|
1651
|
+
const boundary = `----SandagentBoundary${Date.now()}${Math.random().toString(36).slice(2)}`;
|
|
1652
|
+
const parts = [];
|
|
1653
|
+
for (const { name, value } of fields) {
|
|
1654
|
+
parts.push(Buffer.from(`--${boundary}\r
|
|
1655
|
+
Content-Disposition: form-data; name="${name}"\r
|
|
1656
|
+
\r
|
|
1657
|
+
${value}\r
|
|
1658
|
+
`));
|
|
1659
|
+
}
|
|
1660
|
+
for (const { name, filename, buffer, mime } of files) {
|
|
1661
|
+
parts.push(Buffer.from(`--${boundary}\r
|
|
1662
|
+
Content-Disposition: form-data; name="${name}"; filename="${filename}"\r
|
|
1663
|
+
Content-Type: ${mime}\r
|
|
1664
|
+
\r
|
|
1665
|
+
`));
|
|
1666
|
+
parts.push(buffer);
|
|
1667
|
+
parts.push(Buffer.from("\r\n"));
|
|
1668
|
+
}
|
|
1669
|
+
parts.push(Buffer.from(`--${boundary}--\r
|
|
1670
|
+
`));
|
|
1671
|
+
return {
|
|
1672
|
+
body: Buffer.concat(parts),
|
|
1673
|
+
contentType: `multipart/form-data; boundary=${boundary}`
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function buildImageEditTool(cwd, imageModelId, baseUrl, apiKey) {
|
|
1677
|
+
return {
|
|
1678
|
+
name: "edit_image",
|
|
1679
|
+
label: "edit image",
|
|
1680
|
+
description: "Edit an existing image based on a text prompt. Optionally use a mask to control which areas to modify. Saves the result to disk and returns the file path.",
|
|
1681
|
+
promptSnippet: "edit_image(image, prompt, mask?, filename?, size?, quality?) - edit an existing image",
|
|
1682
|
+
promptGuidelines: [
|
|
1683
|
+
"Use edit_image when the user wants to modify, retouch, or transform an existing image.",
|
|
1684
|
+
"The prompt should describe the full desired final image, not just the change.",
|
|
1685
|
+
"Provide the source image path. Use a mask image (PNG with transparent areas) to control where edits happen.",
|
|
1686
|
+
"Without a mask, the model decides what to change based on the prompt."
|
|
1687
|
+
],
|
|
1688
|
+
// biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
|
|
1689
|
+
parameters: editImageSchema,
|
|
1690
|
+
async execute(_toolCallId, params, _signal, _onUpdate) {
|
|
1691
|
+
const { readFileSync: readFileSync4, existsSync: existsSync8 } = await import("node:fs");
|
|
1692
|
+
const { resolve: resolve4, basename: basename2 } = await import("node:path");
|
|
1693
|
+
const p = params;
|
|
1694
|
+
const imagePath = p.image;
|
|
1695
|
+
const prompt = p.prompt;
|
|
1696
|
+
const maskPath = p.mask;
|
|
1697
|
+
const size = p.size;
|
|
1698
|
+
const quality = p.quality;
|
|
1699
|
+
const rawFilename = p.filename;
|
|
1700
|
+
const safePrompt = buildPolicySafeEditPrompt(prompt);
|
|
1701
|
+
const resolvedImage = resolve4(cwd, imagePath);
|
|
1702
|
+
if (!existsSync8(resolvedImage)) {
|
|
1703
|
+
return {
|
|
1704
|
+
content: [
|
|
1705
|
+
{
|
|
1706
|
+
type: "text",
|
|
1707
|
+
text: `Image edit error: source image not found at ${resolvedImage}`
|
|
1708
|
+
}
|
|
1709
|
+
],
|
|
1710
|
+
details: void 0
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
const filename = rawFilename ? extname(rawFilename) ? rawFilename : `${rawFilename}.png` : `edited_${Date.now()}.png`;
|
|
1714
|
+
const filePath = join6(cwd, filename.replace(/[^a-zA-Z0-9_\-./]/g, "_"));
|
|
1715
|
+
try {
|
|
1716
|
+
const imageBuffer = readFileSync4(resolvedImage);
|
|
1717
|
+
const fields = [
|
|
1718
|
+
{ name: "model", value: imageModelId },
|
|
1719
|
+
{ name: "prompt", value: safePrompt.prompt },
|
|
1720
|
+
{ name: "n", value: "1" },
|
|
1721
|
+
{ name: "response_format", value: "b64_json" },
|
|
1722
|
+
{ name: "output_format", value: "png" }
|
|
1723
|
+
];
|
|
1724
|
+
if (size && size !== "auto") {
|
|
1725
|
+
fields.push({ name: "size", value: size });
|
|
1726
|
+
}
|
|
1727
|
+
if (quality && quality !== "auto") {
|
|
1728
|
+
fields.push({ name: "quality", value: quality });
|
|
1729
|
+
}
|
|
1730
|
+
const files = [
|
|
1731
|
+
{
|
|
1732
|
+
name: "image",
|
|
1733
|
+
filename: basename2(resolvedImage),
|
|
1734
|
+
buffer: imageBuffer,
|
|
1735
|
+
mime: detectImageMime(resolvedImage)
|
|
1736
|
+
}
|
|
1737
|
+
];
|
|
1738
|
+
if (maskPath) {
|
|
1739
|
+
const resolvedMask = resolve4(cwd, maskPath);
|
|
1740
|
+
if (existsSync8(resolvedMask)) {
|
|
1741
|
+
files.push({
|
|
1742
|
+
name: "mask",
|
|
1743
|
+
filename: basename2(resolvedMask),
|
|
1744
|
+
buffer: readFileSync4(resolvedMask),
|
|
1745
|
+
mime: detectImageMime(resolvedMask)
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const { body: multipartBody, contentType } = buildMultipartBody(fields, files);
|
|
1750
|
+
const url = `${baseUrl.replace(/\/$/, "")}/v1/images/edits`;
|
|
1751
|
+
const sendRequest = async (body, type) => {
|
|
1752
|
+
const res = await fetch(url, {
|
|
1753
|
+
method: "POST",
|
|
1754
|
+
headers: {
|
|
1755
|
+
"Content-Type": type,
|
|
1756
|
+
Authorization: `Bearer ${apiKey}`
|
|
1757
|
+
},
|
|
1758
|
+
body
|
|
1759
|
+
});
|
|
1760
|
+
if (!res.ok) {
|
|
1761
|
+
throw new Error(`Image edit failed (${res.status}): ${await res.text()}`);
|
|
1762
|
+
}
|
|
1763
|
+
return await res.json();
|
|
1764
|
+
};
|
|
1765
|
+
let json = await sendRequest(multipartBody, contentType);
|
|
1766
|
+
const item = pickImageItem(json);
|
|
1767
|
+
let savedPath = await saveImageItem(item, filePath, apiKey);
|
|
1768
|
+
const firstResponseHasEmptyDataArray = Array.isArray(json.data) && json.data.length === 0;
|
|
1769
|
+
if (!savedPath && safePrompt.rewritten && firstResponseHasEmptyDataArray) {
|
|
1770
|
+
const retryFields = fields.map((f) => f.name === "prompt" ? {
|
|
1771
|
+
name: "prompt",
|
|
1772
|
+
value: "Remove only distracting overlay text artifacts naturally and keep all original content unchanged."
|
|
1773
|
+
} : f);
|
|
1774
|
+
const retryMultipart = buildMultipartBody(retryFields, files);
|
|
1775
|
+
json = await sendRequest(retryMultipart.body, retryMultipart.contentType);
|
|
1776
|
+
const retryItem = pickImageItem(json);
|
|
1777
|
+
savedPath = await saveImageItem(retryItem, filePath, apiKey);
|
|
1778
|
+
}
|
|
1779
|
+
return {
|
|
1780
|
+
content: [
|
|
1781
|
+
{
|
|
1782
|
+
type: "text",
|
|
1783
|
+
text: savedPath ?? `Image edited but could not be saved: no image payload returned; image_model: ${imageModelId}`
|
|
1784
|
+
}
|
|
1785
|
+
],
|
|
1786
|
+
details: {
|
|
1787
|
+
filePath: savedPath,
|
|
1788
|
+
response: json
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
} catch (e) {
|
|
1792
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1793
|
+
return {
|
|
1794
|
+
content: [
|
|
1795
|
+
{ type: "text", text: `Image edit error: ${msg}` }
|
|
1796
|
+
],
|
|
1797
|
+
details: void 0
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1494
1803
|
|
|
1495
1804
|
// ../../packages/runner-pi/dist/tool-overrides.js
|
|
1496
1805
|
import { createBashTool, createReadTool } from "@mariozechner/pi-coding-agent";
|
|
@@ -2065,7 +2374,7 @@ function createPiRunner(options = {}) {
|
|
|
2065
2374
|
const customTools = options.env && Object.keys(options.env).length > 0 ? buildSecretAwareTools(cwd, options.env) : [];
|
|
2066
2375
|
if (imageModelName) {
|
|
2067
2376
|
const apiKey = await modelRegistry.authStorage.getApiKey(provider) ?? "";
|
|
2068
|
-
customTools.push(buildImageGenerateTool(cwd, imageModelName, model.baseUrl, apiKey));
|
|
2377
|
+
customTools.push(buildImageGenerateTool(cwd, imageModelName, model.baseUrl, apiKey), buildImageEditTool(cwd, imageModelName, model.baseUrl, apiKey));
|
|
2069
2378
|
}
|
|
2070
2379
|
const { session } = await createAgentSession({
|
|
2071
2380
|
cwd,
|
|
@@ -2236,7 +2545,7 @@ function createPiRunner(options = {}) {
|
|
|
2236
2545
|
if (options.env && Object.keys(options.env).length > 0) {
|
|
2237
2546
|
output = redactSecrets(output, options.env);
|
|
2238
2547
|
}
|
|
2239
|
-
if (event.toolName === "generate_image" && event.result !== null && typeof event.result === "object") {
|
|
2548
|
+
if ((event.toolName === "generate_image" || event.toolName === "edit_image") && event.result !== null && typeof event.result === "object") {
|
|
2240
2549
|
const details = event.result.details;
|
|
2241
2550
|
const u = details?.response?.usage;
|
|
2242
2551
|
if (u) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunny-agent/runner-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.29-beta.0",
|
|
4
4
|
"description": "BunnyAgent Runner CLI - Like gemini-cli or claude-code, runs in your local terminal with AI SDK UI streaming",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"esbuild": "^0.27.2",
|
|
54
54
|
"typescript": "^5.3.0",
|
|
55
55
|
"vitest": "^1.6.1",
|
|
56
|
-
"@bunny-agent/runner-harness": "0.1.1-beta.0",
|
|
57
56
|
"@bunny-agent/runner-claude": "0.6.2",
|
|
58
57
|
"@bunny-agent/runner-codex": "0.6.2",
|
|
59
|
-
"@bunny-agent/runner-
|
|
58
|
+
"@bunny-agent/runner-harness": "0.1.1-beta.0",
|
|
60
59
|
"@bunny-agent/runner-opencode": "0.6.2",
|
|
61
|
-
"@bunny-agent/runner-pi": "0.6.4-beta.0"
|
|
60
|
+
"@bunny-agent/runner-pi": "0.6.4-beta.0",
|
|
61
|
+
"@bunny-agent/runner-gemini": "0.6.2"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "tsc && pnpm bundle",
|