@bunny-agent/daemon 0.9.28 → 0.9.29-beta.5

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/index.js CHANGED
@@ -263372,18 +263372,139 @@ var generateImageSchema = {
263372
263372
  required: ["prompt"],
263373
263373
  additionalProperties: false
263374
263374
  };
263375
- async function resolveB64(item) {
263375
+ async function resolveB64(item, apiKey) {
263376
263376
  if (item.b64_json)
263377
263377
  return item.b64_json;
263378
- if (item.url) {
263379
- const res = await fetch(item.url);
263378
+ if (item.b64Json)
263379
+ return item.b64Json;
263380
+ if (item.image_base64)
263381
+ return item.image_base64;
263382
+ if (item.imageBase64)
263383
+ return item.imageBase64;
263384
+ if (item.base64)
263385
+ return item.base64;
263386
+ if (typeof item.image === "string")
263387
+ return item.image;
263388
+ if (item.image?.b64_json)
263389
+ return item.image.b64_json;
263390
+ if (item.image?.base64)
263391
+ return item.image.base64;
263392
+ const url = item.url ?? item.image_url ?? item.imageUrl ?? item.image?.url;
263393
+ if (url) {
263394
+ const headers = {};
263395
+ if (apiKey) {
263396
+ headers.Authorization = `Bearer ${apiKey}`;
263397
+ }
263398
+ const res = await fetch(url, { headers });
263380
263399
  if (res.ok)
263381
263400
  return Buffer.from(await res.arrayBuffer()).toString("base64");
263382
263401
  }
263383
263402
  return void 0;
263384
263403
  }
263385
- async function saveImageItem(item, filePath) {
263386
- const b64 = await resolveB64(item);
263404
+ function pickImageItem(response) {
263405
+ const tryFromObject = (value2) => {
263406
+ if (!value2 || typeof value2 !== "object")
263407
+ return void 0;
263408
+ const obj = value2;
263409
+ return {
263410
+ b64_json: obj.b64_json ?? obj.b64Json,
263411
+ b64Json: obj.b64Json,
263412
+ url: obj.url ?? obj.imageUrl,
263413
+ image_base64: obj.image_base64 ?? obj.imageBase64,
263414
+ imageBase64: obj.imageBase64,
263415
+ image_url: obj.image_url ?? obj.imageUrl,
263416
+ imageUrl: obj.imageUrl,
263417
+ base64: obj.base64,
263418
+ image: obj.image
263419
+ };
263420
+ };
263421
+ const asItem = (value2) => {
263422
+ if (value2 == null)
263423
+ return void 0;
263424
+ if (typeof value2 === "string") {
263425
+ return { base64: value2 };
263426
+ }
263427
+ if (typeof value2 === "object") {
263428
+ const normalized = tryFromObject(value2);
263429
+ if (normalized)
263430
+ return normalized;
263431
+ }
263432
+ return void 0;
263433
+ };
263434
+ const fromDataArray = Array.isArray(response.data) ? asItem(response.data[0]) : void 0;
263435
+ if (fromDataArray)
263436
+ return fromDataArray;
263437
+ const fromDataValue = asItem(response.data);
263438
+ if (fromDataValue)
263439
+ return fromDataValue;
263440
+ const responseRecord = response;
263441
+ const imagesValue = responseRecord.images;
263442
+ const outputValue = responseRecord.output;
263443
+ const fromImagesArray = Array.isArray(imagesValue) ? asItem(imagesValue[0]) : void 0;
263444
+ if (fromImagesArray)
263445
+ return fromImagesArray;
263446
+ const fromImagesValue = asItem(imagesValue);
263447
+ if (fromImagesValue)
263448
+ return fromImagesValue;
263449
+ const fromOutputArray = Array.isArray(outputValue) ? asItem(outputValue[0]) : void 0;
263450
+ if (fromOutputArray)
263451
+ return fromOutputArray;
263452
+ const fromOutputValue = asItem(outputValue);
263453
+ if (fromOutputValue)
263454
+ return fromOutputValue;
263455
+ const fromTopLevel = asItem(response);
263456
+ if (fromTopLevel)
263457
+ return fromTopLevel;
263458
+ const queue = [response];
263459
+ while (queue.length > 0) {
263460
+ const current = queue.shift();
263461
+ if (current == null)
263462
+ continue;
263463
+ if (typeof current === "string") {
263464
+ if (/^[A-Za-z0-9+/=]{32,}$/.test(current))
263465
+ return { base64: current };
263466
+ continue;
263467
+ }
263468
+ if (typeof current !== "object")
263469
+ continue;
263470
+ const normalized = tryFromObject(current);
263471
+ if (normalized) {
263472
+ 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));
263473
+ if (hasUsefulField)
263474
+ return normalized;
263475
+ }
263476
+ if (Array.isArray(current)) {
263477
+ queue.push(...current);
263478
+ continue;
263479
+ }
263480
+ for (const value2 of Object.values(current)) {
263481
+ queue.push(value2);
263482
+ }
263483
+ }
263484
+ return {};
263485
+ }
263486
+ function detectImageMime(filePath) {
263487
+ const ext2 = extname2(filePath).toLowerCase();
263488
+ if (ext2 === ".jpg" || ext2 === ".jpeg")
263489
+ return "image/jpeg";
263490
+ if (ext2 === ".webp")
263491
+ return "image/webp";
263492
+ if (ext2 === ".gif")
263493
+ return "image/gif";
263494
+ return "image/png";
263495
+ }
263496
+ function buildPolicySafeEditPrompt(prompt) {
263497
+ const riskyPattern = /\b(watermark|watermarks|logo|logos|copyright|brand mark|remove branding)\b/i;
263498
+ if (!riskyPattern.test(prompt)) {
263499
+ return { prompt, rewritten: false };
263500
+ }
263501
+ return {
263502
+ prompt: "Clean up distracting overlay text or marks naturally while preserving the original scene, style, and layout. Keep the result seamless and high quality.",
263503
+ rewritten: true
263504
+ };
263505
+ }
263506
+ async function saveImageItem(item, filePath, apiKey) {
263507
+ const b64 = await resolveB64(item, apiKey);
263387
263508
  if (!b64)
263388
263509
  return void 0;
263389
263510
  mkdirSync10(dirname19(filePath), { recursive: true });
@@ -263403,7 +263524,7 @@ function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
263403
263524
  ],
263404
263525
  // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
263405
263526
  parameters: generateImageSchema,
263406
- async execute(_toolCallId, params, _signal, _onUpdate) {
263527
+ async execute(_toolCallId, params, signal, _onUpdate) {
263407
263528
  const p = params;
263408
263529
  const prompt = p.prompt;
263409
263530
  const size = p.size ?? "1024x1024";
@@ -263424,20 +263545,23 @@ function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
263424
263545
  prompt,
263425
263546
  n: 1,
263426
263547
  size,
263427
- quality
263428
- })
263548
+ quality,
263549
+ response_format: "b64_json",
263550
+ output_format: "png"
263551
+ }),
263552
+ signal
263429
263553
  });
263430
263554
  if (!res.ok) {
263431
263555
  throw new Error(`Image generation failed (${res.status}): ${await res.text()}`);
263432
263556
  }
263433
263557
  const json = await res.json();
263434
- const item = json.data?.[0] ?? {};
263435
- const savedPath = await saveImageItem(item, filePath);
263558
+ const item = pickImageItem(json);
263559
+ const savedPath = await saveImageItem(item, filePath, apiKey);
263436
263560
  return {
263437
263561
  content: [
263438
263562
  {
263439
263563
  type: "text",
263440
- text: savedPath ?? "Image generated but could not be saved."
263564
+ text: savedPath ?? `Image generated but could not be saved: no image payload returned; image_model: ${imageModelId}`
263441
263565
  }
263442
263566
  ],
263443
263567
  details: {
@@ -263457,13 +263581,200 @@ function buildImageGenerateTool(cwd, imageModelId, baseUrl, apiKey) {
263457
263581
  }
263458
263582
  };
263459
263583
  }
263584
+ var editImageSchema = {
263585
+ type: "object",
263586
+ properties: {
263587
+ image: {
263588
+ type: "string",
263589
+ description: "Path to the source image file to edit (relative to working directory or absolute)."
263590
+ },
263591
+ prompt: {
263592
+ type: "string",
263593
+ description: "Text description of the desired final image. Describe the full result, not just the change."
263594
+ },
263595
+ mask: {
263596
+ type: "string",
263597
+ 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."
263598
+ },
263599
+ filename: {
263600
+ type: "string",
263601
+ description: "Output filename with extension, e.g. 'edited_cat.png'. Defaults to a timestamp-based name."
263602
+ },
263603
+ size: {
263604
+ type: "string",
263605
+ enum: ["1024x1024", "1024x1536", "1536x1024", "auto"],
263606
+ description: "Output image dimensions. Optional; omit or set auto to let model decide."
263607
+ },
263608
+ quality: {
263609
+ type: "string",
263610
+ enum: ["low", "medium", "high", "auto"],
263611
+ description: "Image quality. Optional; omit or set auto to let model decide."
263612
+ }
263613
+ },
263614
+ required: ["image", "prompt"],
263615
+ additionalProperties: false
263616
+ };
263617
+ function buildMultipartBody(fields, files) {
263618
+ const boundary = `----SandagentBoundary${Date.now()}${Math.random().toString(36).slice(2)}`;
263619
+ const parts = [];
263620
+ for (const { name, value: value2 } of fields) {
263621
+ parts.push(Buffer.from(`--${boundary}\r
263622
+ Content-Disposition: form-data; name="${name}"\r
263623
+ \r
263624
+ ${value2}\r
263625
+ `));
263626
+ }
263627
+ for (const { name, filename, buffer, mime } of files) {
263628
+ parts.push(Buffer.from(`--${boundary}\r
263629
+ Content-Disposition: form-data; name="${name}"; filename="${filename}"\r
263630
+ Content-Type: ${mime}\r
263631
+ \r
263632
+ `));
263633
+ parts.push(buffer);
263634
+ parts.push(Buffer.from("\r\n"));
263635
+ }
263636
+ parts.push(Buffer.from(`--${boundary}--\r
263637
+ `));
263638
+ return {
263639
+ body: Buffer.concat(parts),
263640
+ contentType: `multipart/form-data; boundary=${boundary}`
263641
+ };
263642
+ }
263643
+ function buildImageEditTool(cwd, imageModelId, baseUrl, apiKey) {
263644
+ return {
263645
+ name: "edit_image",
263646
+ label: "edit image",
263647
+ 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.",
263648
+ promptSnippet: "edit_image(image, prompt, mask?, filename?, size?, quality?) - edit an existing image",
263649
+ promptGuidelines: [
263650
+ "Use edit_image when the user wants to modify, retouch, or transform an existing image.",
263651
+ "The prompt should describe the full desired final image, not just the change.",
263652
+ "Provide the source image path. Use a mask image (PNG with transparent areas) to control where edits happen.",
263653
+ "Without a mask, the model decides what to change based on the prompt."
263654
+ ],
263655
+ // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
263656
+ parameters: editImageSchema,
263657
+ async execute(_toolCallId, params, signal, _onUpdate) {
263658
+ const { readFileSync: readFileSync24, existsSync: existsSync31 } = await import("node:fs");
263659
+ const { resolve: resolve14, basename: basename12 } = await import("node:path");
263660
+ const p = params;
263661
+ const imagePath = p.image;
263662
+ const prompt = p.prompt;
263663
+ const maskPath = p.mask;
263664
+ const size = p.size;
263665
+ const quality = p.quality;
263666
+ const rawFilename = p.filename;
263667
+ const safePrompt = buildPolicySafeEditPrompt(prompt);
263668
+ const resolvedImage = resolve14(cwd, imagePath);
263669
+ if (!existsSync31(resolvedImage)) {
263670
+ return {
263671
+ content: [
263672
+ {
263673
+ type: "text",
263674
+ text: `Image edit error: source image not found at ${resolvedImage}`
263675
+ }
263676
+ ],
263677
+ details: void 0
263678
+ };
263679
+ }
263680
+ const filename = rawFilename ? extname2(rawFilename) ? rawFilename : `${rawFilename}.png` : `edited_${Date.now()}.png`;
263681
+ const filePath = join34(cwd, filename.replace(/[^a-zA-Z0-9_\-./]/g, "_"));
263682
+ try {
263683
+ const imageBuffer = readFileSync24(resolvedImage);
263684
+ const fields = [
263685
+ { name: "model", value: imageModelId },
263686
+ { name: "prompt", value: safePrompt.prompt },
263687
+ { name: "n", value: "1" },
263688
+ { name: "response_format", value: "b64_json" },
263689
+ { name: "output_format", value: "png" }
263690
+ ];
263691
+ if (size && size !== "auto") {
263692
+ fields.push({ name: "size", value: size });
263693
+ }
263694
+ if (quality && quality !== "auto") {
263695
+ fields.push({ name: "quality", value: quality });
263696
+ }
263697
+ const files = [
263698
+ {
263699
+ name: "image",
263700
+ filename: basename12(resolvedImage),
263701
+ buffer: imageBuffer,
263702
+ mime: detectImageMime(resolvedImage)
263703
+ }
263704
+ ];
263705
+ if (maskPath) {
263706
+ const resolvedMask = resolve14(cwd, maskPath);
263707
+ if (existsSync31(resolvedMask)) {
263708
+ files.push({
263709
+ name: "mask",
263710
+ filename: basename12(resolvedMask),
263711
+ buffer: readFileSync24(resolvedMask),
263712
+ mime: detectImageMime(resolvedMask)
263713
+ });
263714
+ }
263715
+ }
263716
+ const { body: multipartBody, contentType } = buildMultipartBody(fields, files);
263717
+ const url = `${baseUrl.replace(/\/$/, "")}/v1/images/edits`;
263718
+ const sendRequest = async (body, type) => {
263719
+ const res = await fetch(url, {
263720
+ method: "POST",
263721
+ headers: {
263722
+ "Content-Type": type,
263723
+ Authorization: `Bearer ${apiKey}`
263724
+ },
263725
+ body,
263726
+ signal
263727
+ });
263728
+ if (!res.ok) {
263729
+ throw new Error(`Image edit failed (${res.status}): ${await res.text()}`);
263730
+ }
263731
+ return await res.json();
263732
+ };
263733
+ let json = await sendRequest(multipartBody, contentType);
263734
+ const item = pickImageItem(json);
263735
+ let savedPath = await saveImageItem(item, filePath, apiKey);
263736
+ const firstResponseHasEmptyDataArray = Array.isArray(json.data) && json.data.length === 0;
263737
+ if (!savedPath && safePrompt.rewritten && firstResponseHasEmptyDataArray) {
263738
+ const retryFields = fields.map((f3) => f3.name === "prompt" ? {
263739
+ name: "prompt",
263740
+ value: "Remove only distracting overlay text artifacts naturally and keep all original content unchanged."
263741
+ } : f3);
263742
+ const retryMultipart = buildMultipartBody(retryFields, files);
263743
+ json = await sendRequest(retryMultipart.body, retryMultipart.contentType);
263744
+ const retryItem = pickImageItem(json);
263745
+ savedPath = await saveImageItem(retryItem, filePath, apiKey);
263746
+ }
263747
+ return {
263748
+ content: [
263749
+ {
263750
+ type: "text",
263751
+ text: savedPath ?? `Image edited but could not be saved: no image payload returned; image_model: ${imageModelId}`
263752
+ }
263753
+ ],
263754
+ details: {
263755
+ filePath: savedPath,
263756
+ response: json
263757
+ }
263758
+ };
263759
+ } catch (e2) {
263760
+ const msg = e2 instanceof Error ? e2.message : String(e2);
263761
+ return {
263762
+ content: [
263763
+ { type: "text", text: `Image edit error: ${msg}` }
263764
+ ],
263765
+ details: void 0
263766
+ };
263767
+ }
263768
+ }
263769
+ };
263770
+ }
263460
263771
 
263461
263772
  // ../../packages/runner-pi/dist/web-tools.js
263462
263773
  var braveProvider = {
263463
263774
  id: "brave",
263464
263775
  label: "Brave Search",
263465
263776
  envKeys: ["BRAVE_API_KEY"],
263466
- async search({ apiKey, query, count, country, freshness }) {
263777
+ async search({ apiKey, query, count, country, freshness, signal }) {
263467
263778
  const params = new URLSearchParams({
263468
263779
  q: query,
263469
263780
  count: String(Math.min(count, 20))
@@ -263477,7 +263788,8 @@ var braveProvider = {
263477
263788
  Accept: "application/json",
263478
263789
  "Accept-Encoding": "gzip",
263479
263790
  "X-Subscription-Token": apiKey
263480
- }
263791
+ },
263792
+ signal
263481
263793
  });
263482
263794
  if (!res.ok) {
263483
263795
  const body = await res.text().catch(() => "");
@@ -263505,7 +263817,7 @@ var tavilyProvider = {
263505
263817
  id: "tavily",
263506
263818
  label: "Tavily",
263507
263819
  envKeys: ["TAVILY_API_KEY"],
263508
- async search({ apiKey, query, count }) {
263820
+ async search({ apiKey, query, count, signal }) {
263509
263821
  const res = await fetch("https://api.tavily.com/search", {
263510
263822
  method: "POST",
263511
263823
  headers: { "Content-Type": "application/json" },
@@ -263514,7 +263826,8 @@ var tavilyProvider = {
263514
263826
  query,
263515
263827
  max_results: Math.min(count, 10),
263516
263828
  include_answer: false
263517
- })
263829
+ }),
263830
+ signal
263518
263831
  });
263519
263832
  if (!res.ok) {
263520
263833
  const body = await res.text().catch(() => "");
@@ -263567,9 +263880,12 @@ var BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/53
263567
263880
  function htmlToText(html2) {
263568
263881
  return html2.replace(/<(script|style|noscript)[^>]*>[\s\S]*?<\/\1>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|h[1-6]|li|tr)>/gi, "\n").replace(/<(p|div|h[1-6]|li|tr)[^>]*>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
263569
263882
  }
263570
- async function fetchPageContent(url) {
263883
+ async function fetchPageContent(url, externalSignal) {
263571
263884
  const controller = new AbortController();
263572
263885
  const timeout = setTimeout(() => controller.abort(), 15e3);
263886
+ externalSignal?.addEventListener("abort", () => controller.abort(), {
263887
+ once: true
263888
+ });
263573
263889
  try {
263574
263890
  const res = await fetch(url, {
263575
263891
  headers: {
@@ -263666,7 +263982,7 @@ function buildWebSearchTool(env2) {
263666
263982
  ],
263667
263983
  // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
263668
263984
  parameters: webSearchSchema,
263669
- async execute(_toolCallId, params, _signal, _onUpdate) {
263985
+ async execute(_toolCallId, params, signal, _onUpdate) {
263670
263986
  const p = params;
263671
263987
  const query = p.query;
263672
263988
  const count = p.count ?? 5;
@@ -263681,11 +263997,12 @@ function buildWebSearchTool(env2) {
263681
263997
  query,
263682
263998
  count,
263683
263999
  country,
263684
- freshness
264000
+ freshness,
264001
+ signal
263685
264002
  });
263686
264003
  if (shouldFetchContent) {
263687
264004
  for (const r2 of results) {
263688
- r2.content = await fetchPageContent(r2.link);
264005
+ r2.content = await fetchPageContent(r2.link, signal);
263689
264006
  }
263690
264007
  }
263691
264008
  return {
@@ -263731,11 +264048,11 @@ function buildWebFetchTool() {
263731
264048
  ],
263732
264049
  // biome-ignore lint/suspicious/noExplicitAny: plain JSON Schema compatible with TypeBox TSchema
263733
264050
  parameters: webFetchSchema,
263734
- async execute(_toolCallId, params, _signal, _onUpdate) {
264051
+ async execute(_toolCallId, params, signal, _onUpdate) {
263735
264052
  const p = params;
263736
264053
  const url = p.url;
263737
264054
  try {
263738
- const content = await fetchPageContent(url);
264055
+ const content = await fetchPageContent(url, signal);
263739
264056
  return {
263740
264057
  content: [{ type: "text", text: content }],
263741
264058
  details: void 0
@@ -264028,7 +264345,7 @@ function createPiRunner(options2 = {}) {
264028
264345
  const customTools = options2.env && Object.keys(options2.env).length > 0 ? buildSecretAwareTools(cwd, options2.env) : [];
264029
264346
  if (imageModelName) {
264030
264347
  const apiKey = await modelRegistry2.authStorage.getApiKey(provider) ?? "";
264031
- customTools.push(buildImageGenerateTool(cwd, imageModelName, model.baseUrl, apiKey));
264348
+ customTools.push(buildImageGenerateTool(cwd, imageModelName, model.baseUrl, apiKey), buildImageEditTool(cwd, imageModelName, model.baseUrl, apiKey));
264032
264349
  }
264033
264350
  const { session } = await createAgentSession({
264034
264351
  cwd,
@@ -264199,7 +264516,7 @@ function createPiRunner(options2 = {}) {
264199
264516
  if (options2.env && Object.keys(options2.env).length > 0) {
264200
264517
  output = redactSecrets(output, options2.env);
264201
264518
  }
264202
- if (event.toolName === "generate_image" && event.result !== null && typeof event.result === "object") {
264519
+ if ((event.toolName === "generate_image" || event.toolName === "edit_image") && event.result !== null && typeof event.result === "object") {
264203
264520
  const details = event.result.details;
264204
264521
  const u = details?.response?.usage;
264205
264522
  if (u) {
@@ -264432,7 +264749,9 @@ async function bunnyAgentRun(req, res, env2) {
264432
264749
  cwd: req.cwd ?? process.env.BUNNY_AGENT_ROOT ?? "/workspace",
264433
264750
  yolo: req.yolo,
264434
264751
  env: env2,
264435
- abortController
264752
+ abortController,
264753
+ // API: caller owns resume/session; do not read/write cwd/.bunny-agent or auto-load CLAUDE.md.
264754
+ autoInject: false
264436
264755
  });
264437
264756
  for await (const chunk of stream2) {
264438
264757
  res.write(chunk);