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