@hachej/boring-agent 0.1.6 → 0.1.9

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.
@@ -1389,6 +1389,10 @@ function createNodeWorkspace(root) {
1389
1389
  const absPath = await ensureExistingWorkspacePath(root, relPath);
1390
1390
  return await readFile3(absPath, "utf-8");
1391
1391
  },
1392
+ async readBinaryFile(relPath) {
1393
+ const absPath = await ensureExistingWorkspacePath(root, relPath);
1394
+ return new Uint8Array(await readFile3(absPath));
1395
+ },
1392
1396
  async writeFile(relPath, data) {
1393
1397
  const absPath = await ensureWritableWorkspacePath(root, relPath);
1394
1398
  await writeFile3(absPath, data, "utf-8");
@@ -1842,6 +1846,20 @@ function createVercelSandboxWorkspace(sandbox, workspaceOpts = {}) {
1842
1846
  }
1843
1847
  return Buffer.from(content).toString("utf-8");
1844
1848
  },
1849
+ async readBinaryFile(relPath) {
1850
+ const sandboxPath = toSandboxPath(relPath);
1851
+ if (remote.fs?.readFile) {
1852
+ const content2 = await remote.fs.readFile(sandboxPath);
1853
+ return new Uint8Array(Buffer.from(content2));
1854
+ }
1855
+ const content = await remote.readFileToBuffer?.({ path: sandboxPath });
1856
+ if (!content) {
1857
+ const err = new Error(`ENOENT: file not found, open '${sandboxPath}'`);
1858
+ err.code = "ENOENT";
1859
+ throw err;
1860
+ }
1861
+ return new Uint8Array(Buffer.from(content));
1862
+ },
1845
1863
  async writeFile(relPath, data) {
1846
1864
  const sandboxPath = toSandboxPath(relPath);
1847
1865
  await sandbox.writeFiles([
@@ -2509,6 +2527,13 @@ async function seedTemplateIntoVercelWorkspace(workspace, templatePath, logger)
2509
2527
  fileCount: files.length
2510
2528
  });
2511
2529
  }
2530
+ async function ensureTemplateExecutables(sandbox) {
2531
+ if (!sandbox.runCommand) return;
2532
+ await sandbox.runCommand({
2533
+ cmd: "sh",
2534
+ args: ["-c", `chmod +x ${VERCEL_SANDBOX_REMOTE_ROOT}/.boring-agent/bin/* 2>/dev/null || true`]
2535
+ });
2536
+ }
2512
2537
  var DEFAULT_MODE_LOGGER = {
2513
2538
  info(message, metadata) {
2514
2539
  process.stderr.write(`${message} ${JSON.stringify(metadata)}
@@ -2627,6 +2652,7 @@ function createVercelSandboxModeAdapter(opts = {}) {
2627
2652
  });
2628
2653
  }
2629
2654
  await seedTemplateIntoVercelWorkspace(workspace, ctx.templatePath, logger);
2655
+ await ensureTemplateExecutables(sandboxHandle);
2630
2656
  }
2631
2657
  const sandbox = createVercelSandboxExec(sandboxHandle, {
2632
2658
  onMutation: markDirty
@@ -2679,6 +2705,8 @@ import Fastify from "fastify";
2679
2705
  import { basename as basename2 } from "path";
2680
2706
 
2681
2707
  // src/server/harness/pi-coding-agent/createHarness.ts
2708
+ import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
2709
+ import { extname, join as join6 } from "path";
2682
2710
  import {
2683
2711
  createAgentSession,
2684
2712
  SessionManager,
@@ -2690,6 +2718,86 @@ import {
2690
2718
  loadSkills
2691
2719
  } from "@mariozechner/pi-coding-agent";
2692
2720
 
2721
+ // src/server/logging.ts
2722
+ var SENSITIVE_KEYS = new Set([
2723
+ "apiKey",
2724
+ "api_key",
2725
+ "token",
2726
+ "secret",
2727
+ "password",
2728
+ "authorization",
2729
+ "cookie",
2730
+ "oidcToken",
2731
+ "accessToken",
2732
+ "refreshToken",
2733
+ "ANTHROPIC_API_KEY",
2734
+ "OPENAI_API_KEY",
2735
+ "VERCEL_OIDC_TOKEN",
2736
+ "VERCEL_TEAM_ID"
2737
+ ].map((key) => key.toLowerCase()));
2738
+ function isSensitiveKey(key) {
2739
+ return SENSITIVE_KEYS.has(key.toLowerCase());
2740
+ }
2741
+ function redactValue(key, value, seen) {
2742
+ if (key && isSensitiveKey(key) && value != null) return "***";
2743
+ if (value == null || typeof value !== "object") return value;
2744
+ if (value instanceof Date) return value;
2745
+ if (seen.has(value)) return "[Circular]";
2746
+ seen.add(value);
2747
+ if (Array.isArray(value)) {
2748
+ const out2 = value.map((item) => redactValue(void 0, item, seen));
2749
+ seen.delete(value);
2750
+ return out2;
2751
+ }
2752
+ const out = {};
2753
+ for (const [childKey, childValue] of Object.entries(value)) {
2754
+ out[childKey] = redactValue(childKey, childValue, seen);
2755
+ }
2756
+ seen.delete(value);
2757
+ return out;
2758
+ }
2759
+ function redact(fields) {
2760
+ const out = {};
2761
+ const seen = /* @__PURE__ */ new WeakSet();
2762
+ for (const [k, v] of Object.entries(fields)) {
2763
+ out[k] = redactValue(k, v, seen);
2764
+ }
2765
+ return out;
2766
+ }
2767
+ var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
2768
+ function createLogger(prefix) {
2769
+ function emit(level, msg, fields) {
2770
+ const entry = {
2771
+ level,
2772
+ prefix,
2773
+ msg,
2774
+ ...fields ? redact(fields) : {},
2775
+ t: (/* @__PURE__ */ new Date()).toISOString()
2776
+ };
2777
+ if (level === "error") {
2778
+ console.error(JSON.stringify(entry));
2779
+ } else if (level === "warn") {
2780
+ console.warn(JSON.stringify(entry));
2781
+ } else {
2782
+ console.log(JSON.stringify(entry));
2783
+ }
2784
+ }
2785
+ return {
2786
+ debug(msg, fields) {
2787
+ if (verbose) emit("debug", msg, fields);
2788
+ },
2789
+ info(msg, fields) {
2790
+ emit("info", msg, fields);
2791
+ },
2792
+ warn(msg, fields) {
2793
+ emit("warn", msg, fields);
2794
+ },
2795
+ error(msg, fields) {
2796
+ emit("error", msg, fields);
2797
+ }
2798
+ };
2799
+ }
2800
+
2693
2801
  // src/server/harness/pi-coding-agent/tool-adapter.ts
2694
2802
  function adaptToolForPi(tool) {
2695
2803
  return {
@@ -3254,86 +3362,6 @@ function piMessagesToUIMessages(messages) {
3254
3362
  return result;
3255
3363
  }
3256
3364
 
3257
- // src/server/logging.ts
3258
- var SENSITIVE_KEYS = new Set([
3259
- "apiKey",
3260
- "api_key",
3261
- "token",
3262
- "secret",
3263
- "password",
3264
- "authorization",
3265
- "cookie",
3266
- "oidcToken",
3267
- "accessToken",
3268
- "refreshToken",
3269
- "ANTHROPIC_API_KEY",
3270
- "OPENAI_API_KEY",
3271
- "VERCEL_OIDC_TOKEN",
3272
- "VERCEL_TEAM_ID"
3273
- ].map((key) => key.toLowerCase()));
3274
- function isSensitiveKey(key) {
3275
- return SENSITIVE_KEYS.has(key.toLowerCase());
3276
- }
3277
- function redactValue(key, value, seen) {
3278
- if (key && isSensitiveKey(key) && value != null) return "***";
3279
- if (value == null || typeof value !== "object") return value;
3280
- if (value instanceof Date) return value;
3281
- if (seen.has(value)) return "[Circular]";
3282
- seen.add(value);
3283
- if (Array.isArray(value)) {
3284
- const out2 = value.map((item) => redactValue(void 0, item, seen));
3285
- seen.delete(value);
3286
- return out2;
3287
- }
3288
- const out = {};
3289
- for (const [childKey, childValue] of Object.entries(value)) {
3290
- out[childKey] = redactValue(childKey, childValue, seen);
3291
- }
3292
- seen.delete(value);
3293
- return out;
3294
- }
3295
- function redact(fields) {
3296
- const out = {};
3297
- const seen = /* @__PURE__ */ new WeakSet();
3298
- for (const [k, v] of Object.entries(fields)) {
3299
- out[k] = redactValue(k, v, seen);
3300
- }
3301
- return out;
3302
- }
3303
- var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
3304
- function createLogger(prefix) {
3305
- function emit(level, msg, fields) {
3306
- const entry = {
3307
- level,
3308
- prefix,
3309
- msg,
3310
- ...fields ? redact(fields) : {},
3311
- t: (/* @__PURE__ */ new Date()).toISOString()
3312
- };
3313
- if (level === "error") {
3314
- console.error(JSON.stringify(entry));
3315
- } else if (level === "warn") {
3316
- console.warn(JSON.stringify(entry));
3317
- } else {
3318
- console.log(JSON.stringify(entry));
3319
- }
3320
- }
3321
- return {
3322
- debug(msg, fields) {
3323
- if (verbose) emit("debug", msg, fields);
3324
- },
3325
- info(msg, fields) {
3326
- emit("info", msg, fields);
3327
- },
3328
- warn(msg, fields) {
3329
- emit("warn", msg, fields);
3330
- },
3331
- error(msg, fields) {
3332
- emit("error", msg, fields);
3333
- }
3334
- };
3335
- }
3336
-
3337
3365
  // src/server/harness/pi-coding-agent/sessionTitle.ts
3338
3366
  var DEFAULT_SESSION_TITLE = "New session";
3339
3367
  var FALLBACK_PREFIX = "New chat";
@@ -3732,6 +3760,24 @@ async function applyRequestedSessionOptions(handle, input) {
3732
3760
  handle.piSession.setThinkingLevel(input.thinkingLevel);
3733
3761
  }
3734
3762
  }
3763
+ var log2 = createLogger("pi-harness");
3764
+ var DEFAULT_ATTACHMENT_DIR = "assets/images";
3765
+ function extForAttachment(filename, contentType) {
3766
+ const fromName = extname(filename).toLowerCase().replace(/^\./, "");
3767
+ if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
3768
+ if (contentType === "image/jpeg") return "jpg";
3769
+ if (contentType === "image/png") return "png";
3770
+ if (contentType === "image/gif") return "gif";
3771
+ if (contentType === "image/webp") return "webp";
3772
+ if (contentType === "image/svg+xml") return "svg";
3773
+ return "bin";
3774
+ }
3775
+ function basenameForAttachment(filename) {
3776
+ const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
3777
+ const withoutExt = base.replace(/\.[^.]*$/, "");
3778
+ const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
3779
+ return safe || "image";
3780
+ }
3735
3781
  function createPiCodingAgentHarness(opts) {
3736
3782
  const sessionStore = new PiSessionStore(opts.cwd);
3737
3783
  const piSessions = /* @__PURE__ */ new Map();
@@ -3828,10 +3874,17 @@ function createPiCodingAgentHarness(opts) {
3828
3874
  await originalDelete(ctx, sessionId);
3829
3875
  disposePiSession(sessionId);
3830
3876
  };
3877
+ const harnessFollowUpQueues = /* @__PURE__ */ new Map();
3831
3878
  return {
3832
3879
  id: "pi-coding-agent",
3833
3880
  placement: "server",
3834
3881
  sessions: sessionStore,
3882
+ followUp(sessionId, text, attachments) {
3883
+ harnessFollowUpQueues.set(sessionId, { message: text, attachments });
3884
+ },
3885
+ clearFollowUp(sessionId) {
3886
+ harnessFollowUpQueues.delete(sessionId);
3887
+ },
3835
3888
  /**
3836
3889
  * Pi exposes the resolved system prompt as a getter on AgentSession.
3837
3890
  * Sessions are created lazily on first sendMessage, so callers may see
@@ -3847,6 +3900,7 @@ function createPiCodingAgentHarness(opts) {
3847
3900
  input,
3848
3901
  ctx
3849
3902
  );
3903
+ harnessFollowUpQueues.delete(input.sessionId);
3850
3904
  const chunks = [];
3851
3905
  let done = false;
3852
3906
  let streamError = null;
@@ -3895,6 +3949,20 @@ function createPiCodingAgentHarness(opts) {
3895
3949
  return out;
3896
3950
  }
3897
3951
  let sawTextChunk = false;
3952
+ let inlineTurnIndex = 0;
3953
+ function namespaceInlinePartIds(input2) {
3954
+ if (inlineTurnIndex === 0) return input2;
3955
+ return input2.map((chunk2) => {
3956
+ const rec = chunk2;
3957
+ if ((rec.type === "text-start" || rec.type === "text-delta" || rec.type === "text-end" || rec.type === "reasoning-start" || rec.type === "reasoning-delta" || rec.type === "reasoning-end") && typeof rec.id === "string") {
3958
+ return { ...rec, id: `turn-${inlineTurnIndex}:${rec.id}` };
3959
+ }
3960
+ return chunk2;
3961
+ });
3962
+ }
3963
+ let pendingFollowUp;
3964
+ let promptSettled = false;
3965
+ let promptPromise = Promise.resolve();
3898
3966
  const unsubscribe = piSession.subscribe((event) => {
3899
3967
  if (event.type === "message_update") {
3900
3968
  const ame = event.assistantMessageEvent;
@@ -3914,11 +3982,11 @@ function createPiCodingAgentHarness(opts) {
3914
3982
  activeTools.delete(event.toolCallId);
3915
3983
  if (activeTools.size === 0) stopHeartbeat();
3916
3984
  }
3917
- let converted = dedupStartChunks(piEventToChunks(event));
3985
+ let converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
3918
3986
  if (event.type === "message_update") {
3919
3987
  const ame = event.assistantMessageEvent;
3920
3988
  if (ame.type === "text_end" && typeof ame.content === "string" && ame.content.length > 0 && !textDeltaSeen.has(ame.contentIndex)) {
3921
- const id = String(ame.contentIndex);
3989
+ const id = inlineTurnIndex === 0 ? String(ame.contentIndex) : `turn-${inlineTurnIndex}:${ame.contentIndex}`;
3922
3990
  converted = [
3923
3991
  ...textStartSeen.has(ame.contentIndex) ? [] : [{ type: "text-start", id }],
3924
3992
  { type: "text-delta", id, delta: ame.content },
@@ -3943,10 +4011,11 @@ function createPiCodingAgentHarness(opts) {
3943
4011
  chunks.push({ type: "error", errorText });
3944
4012
  sawTextChunk = true;
3945
4013
  } else if (role === "assistant" && text.length > 0) {
4014
+ const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
3946
4015
  chunks.push(
3947
- { type: "text-start", id: "0" },
3948
- { type: "text-delta", id: "0", delta: text },
3949
- { type: "text-end", id: "0" }
4016
+ { type: "text-start", id },
4017
+ { type: "text-delta", id, delta: text },
4018
+ { type: "text-end", id }
3950
4019
  );
3951
4020
  sawTextChunk = true;
3952
4021
  assistantText += text;
@@ -3963,30 +4032,95 @@ function createPiCodingAgentHarness(opts) {
3963
4032
  chunks.push({ type: "error", errorText });
3964
4033
  sawTextChunk = true;
3965
4034
  } else if (role === "assistant" && text.length > 0) {
4035
+ const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
3966
4036
  chunks.push(
3967
- { type: "text-start", id: "0" },
3968
- { type: "text-delta", id: "0", delta: text },
3969
- { type: "text-end", id: "0" }
4037
+ { type: "text-start", id },
4038
+ { type: "text-delta", id, delta: text },
4039
+ { type: "text-end", id }
3970
4040
  );
3971
4041
  sawTextChunk = true;
3972
4042
  assistantText += text;
3973
4043
  }
3974
4044
  }
3975
- done = true;
3976
- stopHeartbeat();
4045
+ const followUp = harnessFollowUpQueues.get(input.sessionId);
4046
+ if (followUp !== void 0 && !ctx.abortSignal.aborted) {
4047
+ harnessFollowUpQueues.delete(input.sessionId);
4048
+ chunks.push({ type: "data-followup-consumed" });
4049
+ inlineTurnIndex += 1;
4050
+ sawTextChunk = false;
4051
+ textStartSeen.clear();
4052
+ textDeltaSeen.clear();
4053
+ assistantText = "";
4054
+ pendingFollowUp = followUp;
4055
+ } else {
4056
+ done = true;
4057
+ stopHeartbeat();
4058
+ }
3977
4059
  }
3978
4060
  if (wake) wake();
3979
4061
  });
3980
- let promptSettled = false;
3981
- const promptPromise = piSession.prompt(input.message).then(() => {
3982
- promptSettled = true;
3983
- }).catch((err) => {
3984
- promptSettled = true;
3985
- streamError = err;
3986
- done = true;
3987
- stopHeartbeat();
3988
- if (wake) wake();
3989
- });
4062
+ async function prepareTurn(message, attachments) {
4063
+ const initialImages = [];
4064
+ const savedPaths = [];
4065
+ const writeErrors = [];
4066
+ for (const a of attachments ?? []) {
4067
+ const match = a.url.match(/^data:(image\/[^;]+);base64,(.+)$/);
4068
+ if (!match) continue;
4069
+ const [, contentType, b64] = match;
4070
+ initialImages.push({ type: "image", mimeType: contentType, data: b64 });
4071
+ if (!ctx.workdir) {
4072
+ writeErrors.push(`${a.filename ?? "image"}: no workdir`);
4073
+ continue;
4074
+ }
4075
+ try {
4076
+ const bytes = Buffer.from(b64, "base64");
4077
+ const ext = extForAttachment(a.filename ?? "image", contentType);
4078
+ const base = basenameForAttachment(a.filename ?? "image");
4079
+ const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
4080
+ const relPath = `${DEFAULT_ATTACHMENT_DIR}/${base}-${unique}.${ext}`;
4081
+ await mkdir9(join6(ctx.workdir, DEFAULT_ATTACHMENT_DIR), { recursive: true });
4082
+ await writeFile7(join6(ctx.workdir, relPath), bytes);
4083
+ savedPaths.push(relPath);
4084
+ } catch (err) {
4085
+ const msg = err instanceof Error ? err.message : String(err);
4086
+ log2.error("attachment write failed", { workdir: ctx.workdir, error: msg });
4087
+ writeErrors.push(`${a.filename ?? "image"}: ${msg}`);
4088
+ }
4089
+ }
4090
+ const attachmentNotes = [];
4091
+ if (savedPaths.length > 0) {
4092
+ attachmentNotes.push(`[Attached file(s) saved to workspace:
4093
+ ${savedPaths.map((p) => `- ${p}`).join("\n")}]`);
4094
+ }
4095
+ if (writeErrors.length > 0) {
4096
+ attachmentNotes.push(`[Warning: failed to save attachment(s) to workspace \u2014 ${writeErrors.join("; ")}. The image is available via vision only.]`);
4097
+ }
4098
+ return {
4099
+ message: attachmentNotes.length > 0 ? `${message}
4100
+
4101
+ ${attachmentNotes.join("\n")}` : message,
4102
+ promptOpts: initialImages.length > 0 ? { images: initialImages } : void 0
4103
+ };
4104
+ }
4105
+ async function startTurn(message, attachments) {
4106
+ promptSettled = false;
4107
+ const prepared = await prepareTurn(message, attachments);
4108
+ return piSession.prompt(prepared.message, prepared.promptOpts).then(() => {
4109
+ promptSettled = true;
4110
+ if (pendingFollowUp !== void 0 && !ctx.abortSignal.aborted) {
4111
+ const next = pendingFollowUp;
4112
+ pendingFollowUp = void 0;
4113
+ promptPromise = startTurn(next.message, next.attachments);
4114
+ }
4115
+ }).catch((err) => {
4116
+ promptSettled = true;
4117
+ streamError = err;
4118
+ done = true;
4119
+ stopHeartbeat();
4120
+ if (wake) wake();
4121
+ });
4122
+ }
4123
+ promptPromise = startTurn(input.message, input.attachments);
3990
4124
  const onAbort = () => {
3991
4125
  if (promptSettled) {
3992
4126
  streamError = new Error("Aborted");
@@ -4034,11 +4168,11 @@ function createPiCodingAgentHarness(opts) {
4034
4168
 
4035
4169
  // src/server/harness/pi-coding-agent/pluginLoader.ts
4036
4170
  import { readdir as readdir5, stat as stat5, readFile as readFile7 } from "fs/promises";
4037
- import { join as join6, extname, resolve as resolve4, sep as sep3 } from "path";
4171
+ import { join as join7, extname as extname2, resolve as resolve4, sep as sep3 } from "path";
4038
4172
  import { homedir as homedir4 } from "os";
4039
4173
  import { pathToFileURL } from "url";
4040
4174
  var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
4041
- var GLOBAL_DIR = join6(homedir4(), ".pi", "agent", "extensions");
4175
+ var GLOBAL_DIR = join7(homedir4(), ".pi", "agent", "extensions");
4042
4176
  var LOCAL_DIR = ".pi/extensions";
4043
4177
  var EXTENSIONS_JSON = ".pi/extensions.json";
4044
4178
  async function dirExists(path4) {
@@ -4060,7 +4194,7 @@ async function fileExists2(path4) {
4060
4194
  async function discoverFromDir(dir, source) {
4061
4195
  if (!await dirExists(dir)) return [];
4062
4196
  const entries = await readdir5(dir);
4063
- return entries.filter((e) => VALID_EXTENSIONS.has(extname(e))).map((e) => ({ path: join6(dir, e), source }));
4197
+ return entries.filter((e) => VALID_EXTENSIONS.has(extname2(e))).map((e) => ({ path: join7(dir, e), source }));
4064
4198
  }
4065
4199
  function extractTools(mod) {
4066
4200
  const tools = [];
@@ -4090,17 +4224,17 @@ async function loadModule(filePath, importFn) {
4090
4224
  return extractTools(mod);
4091
4225
  }
4092
4226
  async function discoverNpmPlugins(cwd) {
4093
- const nodeModulesDir = join6(cwd, "node_modules");
4227
+ const nodeModulesDir = join7(cwd, "node_modules");
4094
4228
  if (!await dirExists(nodeModulesDir)) return [];
4095
4229
  try {
4096
4230
  const entries = await readdir5(nodeModulesDir);
4097
- return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join6(nodeModulesDir, e));
4231
+ return entries.filter((e) => e.startsWith("pi-plugin-")).map((e) => join7(nodeModulesDir, e));
4098
4232
  } catch {
4099
4233
  return [];
4100
4234
  }
4101
4235
  }
4102
4236
  async function loadNpmPlugin(pkgDir, importFn) {
4103
- const pkgJsonPath = join6(pkgDir, "package.json");
4237
+ const pkgJsonPath = join7(pkgDir, "package.json");
4104
4238
  if (!await fileExists2(pkgJsonPath)) return [];
4105
4239
  const pkgJson = JSON.parse(await readFile7(pkgJsonPath, "utf-8"));
4106
4240
  const main = pkgJson.main ?? "index.js";
@@ -4112,7 +4246,7 @@ async function loadNpmPlugin(pkgDir, importFn) {
4112
4246
  return loadModule(resolvedMain, importFn);
4113
4247
  }
4114
4248
  async function loadExtensionsJson(cwd) {
4115
- const configPath = join6(cwd, EXTENSIONS_JSON);
4249
+ const configPath = join7(cwd, EXTENSIONS_JSON);
4116
4250
  if (!await fileExists2(configPath)) return null;
4117
4251
  try {
4118
4252
  const raw = await readFile7(configPath, "utf-8");
@@ -4130,7 +4264,7 @@ async function loadPlugins(options) {
4130
4264
  const globals = await discoverFromDir(GLOBAL_DIR, "global");
4131
4265
  candidates.push(...globals);
4132
4266
  }
4133
- const localDir = join6(options.cwd, LOCAL_DIR);
4267
+ const localDir = join7(options.cwd, LOCAL_DIR);
4134
4268
  const locals = await discoverFromDir(localDir, "local");
4135
4269
  candidates.push(...locals);
4136
4270
  const npmDirs = await discoverNpmPlugins(options.cwd);
@@ -4140,7 +4274,7 @@ async function loadPlugins(options) {
4140
4274
  const config = await loadExtensionsJson(options.cwd);
4141
4275
  if (config?.npm) {
4142
4276
  for (const pkg of config.npm) {
4143
- const pkgDir = join6(options.cwd, "node_modules", pkg);
4277
+ const pkgDir = join7(options.cwd, "node_modules", pkg);
4144
4278
  if (await dirExists(pkgDir)) {
4145
4279
  const already = candidates.some((c) => c.path === pkgDir);
4146
4280
  if (!already) {
@@ -4189,7 +4323,7 @@ import {
4189
4323
 
4190
4324
  // src/server/tools/operations/bound.ts
4191
4325
  import { constants as constants3 } from "fs";
4192
- import { access as access3, lstat as lstat3, mkdir as mkdir9, readFile as readFile8, readdir as readdir6, readlink, realpath as realpath2, stat as stat6, writeFile as writeFile7 } from "fs/promises";
4326
+ import { access as access3, lstat as lstat3, mkdir as mkdir10, readFile as readFile8, readdir as readdir6, readlink, realpath as realpath2, stat as stat6, writeFile as writeFile8 } from "fs/promises";
4193
4327
  import { dirname as dirname6, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve5 } from "path";
4194
4328
  function toPosixPath(value) {
4195
4329
  return value.split("\\").join("/");
@@ -4327,12 +4461,12 @@ function boundFs(workspaceRoot) {
4327
4461
  const write = {
4328
4462
  async writeFile(absolutePath, content) {
4329
4463
  await assertWithinWorkspace(workspaceRoot, absolutePath);
4330
- await mkdir9(dirname6(absolutePath), { recursive: true });
4331
- await writeFile7(absolutePath, content);
4464
+ await mkdir10(dirname6(absolutePath), { recursive: true });
4465
+ await writeFile8(absolutePath, content);
4332
4466
  },
4333
4467
  async mkdir(dir) {
4334
4468
  await assertWithinWorkspace(workspaceRoot, dir);
4335
- await mkdir9(dir, { recursive: true });
4469
+ await mkdir10(dir, { recursive: true });
4336
4470
  }
4337
4471
  };
4338
4472
  const edit = {
@@ -4342,7 +4476,7 @@ function boundFs(workspaceRoot) {
4342
4476
  },
4343
4477
  async writeFile(absolutePath, content) {
4344
4478
  await assertWithinWorkspace(workspaceRoot, absolutePath);
4345
- await writeFile7(absolutePath, content);
4479
+ await writeFile8(absolutePath, content);
4346
4480
  },
4347
4481
  async access(absolutePath) {
4348
4482
  await assertWithinWorkspace(workspaceRoot, absolutePath);
@@ -5015,7 +5149,7 @@ function healthRoutes(app, opts, done) {
5015
5149
  }
5016
5150
 
5017
5151
  // src/server/http/routes/file.ts
5018
- import { dirname as dirname7, extname as extname2, relative as relative7 } from "path/posix";
5152
+ import { dirname as dirname7, extname as extname3, relative as relative7 } from "path/posix";
5019
5153
  var BORING_SETTINGS_PATH = ".boring/settings";
5020
5154
  var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
5021
5155
  var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
@@ -5098,7 +5232,7 @@ function normalizeUploadDir(value) {
5098
5232
  return dir.replace(/\/+$/, "");
5099
5233
  }
5100
5234
  function extForUpload(filename, contentType) {
5101
- const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
5235
+ const fromName = extname3(filename).toLowerCase().replace(/^\./, "");
5102
5236
  if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
5103
5237
  if (contentType === "image/jpeg") return "jpg";
5104
5238
  if (contentType === "image/png") return "png";
@@ -5119,12 +5253,56 @@ function markdownUrlFor(sourcePath, assetPath) {
5119
5253
  const rel = relative7(fromDir === "." ? "" : fromDir, assetPath);
5120
5254
  return rel && !rel.startsWith(".") ? rel : rel || assetPath;
5121
5255
  }
5256
+ function contentTypeForPath(path4) {
5257
+ switch (extname3(path4).toLowerCase()) {
5258
+ case ".avif":
5259
+ return "image/avif";
5260
+ case ".gif":
5261
+ return "image/gif";
5262
+ case ".jpg":
5263
+ case ".jpeg":
5264
+ return "image/jpeg";
5265
+ case ".png":
5266
+ return "image/png";
5267
+ case ".svg":
5268
+ return "image/svg+xml";
5269
+ case ".webp":
5270
+ return "image/webp";
5271
+ case ".pdf":
5272
+ return "application/pdf";
5273
+ default:
5274
+ return "application/octet-stream";
5275
+ }
5276
+ }
5122
5277
  function fileRoutes(app, opts, done) {
5123
5278
  async function resolveWorkspace(request) {
5124
5279
  if (opts.getWorkspace) return await opts.getWorkspace(request);
5125
5280
  if (opts.workspace) return opts.workspace;
5126
5281
  throw new Error("file route requires workspace or getWorkspace");
5127
5282
  }
5283
+ app.get("/api/v1/files/raw", async (request, reply) => {
5284
+ const query = request.query;
5285
+ const path4 = requireStringParam(query.path, "path", reply);
5286
+ if (path4 === null) return;
5287
+ try {
5288
+ const workspace = await resolveWorkspace(request);
5289
+ if (!workspace.readBinaryFile) {
5290
+ return reply.code(501).send({
5291
+ error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
5292
+ });
5293
+ }
5294
+ const stat7 = await workspace.stat(path4);
5295
+ if (stat7.kind !== "file") {
5296
+ return reply.code(400).send({
5297
+ error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
5298
+ });
5299
+ }
5300
+ const bytes = await workspace.readBinaryFile(path4);
5301
+ return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
5302
+ } catch (err) {
5303
+ return classifyError(err, reply, "file");
5304
+ }
5305
+ });
5128
5306
  app.get("/api/v1/files", async (request, reply) => {
5129
5307
  const query = request.query;
5130
5308
  const path4 = requireStringParam(query.path, "path", reply);
@@ -5803,7 +5981,7 @@ function chatRoutes(app, opts, done) {
5803
5981
  "/api/v1/agent/chat",
5804
5982
  { preHandler: validateBody },
5805
5983
  async (request, reply) => {
5806
- const { sessionId, message, model, thinkingLevel } = request.body;
5984
+ const { sessionId, message, model, thinkingLevel, attachments } = request.body;
5807
5985
  const turnId = randomUUID3();
5808
5986
  request.log.info({ sessionId, turnId, model, thinkingLevel }, "[chat] start");
5809
5987
  const runtime = await resolveRuntime(request);
@@ -5822,7 +6000,7 @@ function chatRoutes(app, opts, done) {
5822
6000
  const buf = buffers.create(sessionId, turnId);
5823
6001
  try {
5824
6002
  const chunks = runtime.harness.sendMessage(
5825
- { sessionId, message, model, thinkingLevel },
6003
+ { sessionId, message, model, thinkingLevel, attachments },
5826
6004
  ctx
5827
6005
  );
5828
6006
  const stream = createUIMessageStream({
@@ -5954,6 +6132,36 @@ function chatRoutes(app, opts, done) {
5954
6132
  }
5955
6133
  }
5956
6134
  );
6135
+ app.post(
6136
+ "/api/v1/agent/chat/:sessionId/followup",
6137
+ async (request, reply) => {
6138
+ const { sessionId } = request.params;
6139
+ const body = request.body;
6140
+ if (typeof body?.message !== "string" || body.message.length === 0) {
6141
+ return reply.code(400).send({
6142
+ error: { code: ERROR_CODE_VALIDATION_ERROR, message: "message is required" }
6143
+ });
6144
+ }
6145
+ const parsedAttachments = chatBodySchema.shape.attachments.safeParse(body.attachments);
6146
+ if (!parsedAttachments.success) {
6147
+ return reply.code(400).send({
6148
+ error: { code: ERROR_CODE_VALIDATION_ERROR, message: "attachments is invalid" }
6149
+ });
6150
+ }
6151
+ const runtime = await resolveRuntime(request);
6152
+ runtime.harness.followUp?.(sessionId, body.message, parsedAttachments.data);
6153
+ return reply.code(202).send({ queued: true });
6154
+ }
6155
+ );
6156
+ app.delete(
6157
+ "/api/v1/agent/chat/:sessionId/followup",
6158
+ async (request, reply) => {
6159
+ const { sessionId } = request.params;
6160
+ const runtime = await resolveRuntime(request);
6161
+ runtime.harness.clearFollowUp?.(sessionId);
6162
+ return reply.code(204).send();
6163
+ }
6164
+ );
5957
6165
  app.put(
5958
6166
  "/api/v1/agent/chat/:sessionId/messages",
5959
6167
  async (request, reply) => {
@@ -6460,7 +6668,7 @@ async function createAgentApp(opts = {}) {
6460
6668
  const workspaceRoot = opts.workspaceRoot ?? process.cwd();
6461
6669
  const sessionId = opts.sessionId ?? DEFAULT_SESSION_ID;
6462
6670
  const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
6463
- const app = Fastify({ logger: opts.logger ?? true });
6671
+ const app = Fastify({ logger: opts.logger ?? true, bodyLimit: 16 * 1024 * 1024 });
6464
6672
  const resolvedMode = opts.mode ?? autoDetectMode();
6465
6673
  const runtimeBundle = await resolveMode(resolvedMode).create({
6466
6674
  workspaceRoot,
@@ -6592,6 +6800,113 @@ function mergeTools(options) {
6592
6800
  return [...merged.values()];
6593
6801
  }
6594
6802
 
6803
+ // src/server/tools/upload/index.ts
6804
+ import { readFile as readFile9 } from "fs/promises";
6805
+ import { extname as extname4, join as join8 } from "path";
6806
+ var DEFAULT_UPLOAD_DIR = "assets/images";
6807
+ var MAX_UPLOAD_BYTES2 = 10 * 1024 * 1024;
6808
+ function contentTypeFromExt(path4) {
6809
+ switch (extname4(path4).toLowerCase()) {
6810
+ case ".jpg":
6811
+ case ".jpeg":
6812
+ return "image/jpeg";
6813
+ case ".png":
6814
+ return "image/png";
6815
+ case ".gif":
6816
+ return "image/gif";
6817
+ case ".webp":
6818
+ return "image/webp";
6819
+ case ".svg":
6820
+ return "image/svg+xml";
6821
+ case ".avif":
6822
+ return "image/avif";
6823
+ default:
6824
+ return "application/octet-stream";
6825
+ }
6826
+ }
6827
+ function extForUpload2(filename, contentType) {
6828
+ const fromName = extname4(filename).toLowerCase().replace(/^\./, "");
6829
+ if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
6830
+ if (contentType === "image/jpeg") return "jpg";
6831
+ if (contentType === "image/png") return "png";
6832
+ if (contentType === "image/gif") return "gif";
6833
+ if (contentType === "image/webp") return "webp";
6834
+ if (contentType === "image/svg+xml") return "svg";
6835
+ return "bin";
6836
+ }
6837
+ function basenameForUpload2(filename) {
6838
+ const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
6839
+ const withoutExt = base.replace(/\.[^.]*$/, "");
6840
+ const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
6841
+ return safe || "image";
6842
+ }
6843
+ function buildUploadAgentTools(bundle) {
6844
+ const { workspace } = bundle;
6845
+ const cwd = workspace.root;
6846
+ return [
6847
+ {
6848
+ name: "upload_file",
6849
+ description: "Copy a workspace file into artifact storage (assets/images by default) and return its workspace-relative path. Use this when you want to embed an image in markdown or make a generated file accessible as a stable artifact. The returned path can be used directly in markdown image syntax: ![](returned-path)",
6850
+ parameters: {
6851
+ type: "object",
6852
+ properties: {
6853
+ path: {
6854
+ type: "string",
6855
+ description: 'Workspace-relative path of the source file (e.g. "chart.png" or "output/plot.png")'
6856
+ },
6857
+ directory: {
6858
+ type: "string",
6859
+ description: `Destination directory inside the workspace. Defaults to "${DEFAULT_UPLOAD_DIR}".`
6860
+ }
6861
+ },
6862
+ required: ["path"],
6863
+ additionalProperties: false
6864
+ },
6865
+ async execute(input) {
6866
+ const filePath = typeof input.path === "string" ? input.path.trim() : "";
6867
+ if (!filePath || filePath.includes("\0") || filePath.startsWith("/") || filePath.split("/").includes("..")) {
6868
+ return { content: [{ type: "text", text: "invalid path" }], isError: true };
6869
+ }
6870
+ const rawDir = typeof input.directory === "string" ? input.directory.trim() : "";
6871
+ const dir = rawDir ? rawDir.replace(/^\.\/+/, "").replace(/\/+$/, "") : DEFAULT_UPLOAD_DIR;
6872
+ try {
6873
+ const bytes = await readFile9(join8(cwd, filePath));
6874
+ if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES2) {
6875
+ return {
6876
+ content: [{ type: "text", text: `file must be between 1 byte and ${MAX_UPLOAD_BYTES2} bytes` }],
6877
+ isError: true
6878
+ };
6879
+ }
6880
+ const contentType = contentTypeFromExt(filePath);
6881
+ const ext = extForUpload2(filePath, contentType);
6882
+ const base = basenameForUpload2(filePath);
6883
+ const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
6884
+ const destPath = `${dir}/${base}-${unique}.${ext}`;
6885
+ await workspace.mkdir(dir, { recursive: true });
6886
+ if (workspace.writeBinaryFile) {
6887
+ await workspace.writeBinaryFile(destPath, bytes);
6888
+ } else {
6889
+ return {
6890
+ content: [{ type: "text", text: "workspace does not support binary file writes" }],
6891
+ isError: true
6892
+ };
6893
+ }
6894
+ return {
6895
+ content: [{ type: "text", text: `Uploaded to ${destPath}` }],
6896
+ isError: false,
6897
+ details: { path: destPath }
6898
+ };
6899
+ } catch (err) {
6900
+ return {
6901
+ content: [{ type: "text", text: err instanceof Error ? err.message : "upload failed" }],
6902
+ isError: true
6903
+ };
6904
+ }
6905
+ }
6906
+ }
6907
+ ];
6908
+ }
6909
+
6595
6910
  // src/server/registerAgentRoutes.ts
6596
6911
  var DEFAULT_VERSION2 = "0.1.0-dev";
6597
6912
  var DEFAULT_WORKSPACE_ID3 = "default";
@@ -6672,7 +6987,8 @@ var registerAgentRoutes = async (app, opts) => {
6672
6987
  });
6673
6988
  const standardTools = [
6674
6989
  ...buildHarnessAgentTools(runtimeBundle),
6675
- ...buildFilesystemAgentTools(runtimeBundle)
6990
+ ...buildFilesystemAgentTools(runtimeBundle),
6991
+ ...buildUploadAgentTools(runtimeBundle)
6676
6992
  ];
6677
6993
  const pluginTools = [];
6678
6994
  if (resolvedMode !== "vercel-sandbox") {