@poncho-ai/cli 0.10.2 → 0.11.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.
@@ -13,11 +13,15 @@ import {
13
13
  LocalMcpBridge,
14
14
  TelemetryEmitter,
15
15
  createConversationStore,
16
+ createUploadStore,
17
+ deriveUploadKey,
16
18
  ensureAgentIdentity as ensureAgentIdentity2,
17
19
  generateAgentId,
18
20
  loadPonchoConfig,
19
21
  resolveStateConfig
20
22
  } from "@poncho-ai/harness";
23
+ import { getTextContent } from "@poncho-ai/sdk";
24
+ import Busboy from "busboy";
21
25
  import { Command } from "commander";
22
26
  import dotenv from "dotenv";
23
27
  import YAML from "yaml";
@@ -897,7 +901,7 @@ var renderWebUiHtml = (options) => {
897
901
  border-radius: 24px;
898
902
  display: flex;
899
903
  align-items: end;
900
- padding: 4px 6px 4px 18px;
904
+ padding: 4px 6px 4px 6px;
901
905
  transition: border-color 0.15s;
902
906
  }
903
907
  .composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
@@ -910,7 +914,7 @@ var renderWebUiHtml = (options) => {
910
914
  min-height: 40px;
911
915
  max-height: 200px;
912
916
  resize: none;
913
- padding: 10px 0 8px;
917
+ padding: 11px 0 8px;
914
918
  font-size: 14px;
915
919
  line-height: 1.5;
916
920
  margin-top: -4px;
@@ -938,6 +942,149 @@ var renderWebUiHtml = (options) => {
938
942
  .send-btn.stop-mode:hover { background: #565656; }
939
943
  .send-btn:disabled { opacity: 0.2; cursor: default; }
940
944
  .send-btn:disabled:hover { background: #ededed; }
945
+ .attach-btn {
946
+ width: 32px;
947
+ height: 32px;
948
+ background: rgba(255,255,255,0.08);
949
+ border: 0;
950
+ border-radius: 50%;
951
+ color: #999;
952
+ cursor: pointer;
953
+ display: grid;
954
+ place-items: center;
955
+ flex-shrink: 0;
956
+ margin-bottom: 2px;
957
+ margin-right: 8px;
958
+ transition: color 0.15s, background 0.15s;
959
+ }
960
+ .attach-btn:hover { color: #ededed; background: rgba(255,255,255,0.14); }
961
+ .attachment-preview {
962
+ display: flex;
963
+ gap: 8px;
964
+ padding: 8px 0;
965
+ flex-wrap: wrap;
966
+ }
967
+ .attachment-chip {
968
+ display: inline-flex;
969
+ align-items: center;
970
+ gap: 6px;
971
+ background: rgba(0, 0, 0, 0.6);
972
+ border: 1px solid rgba(255, 255, 255, 0.12);
973
+ border-radius: 9999px;
974
+ padding: 4px 10px 4px 6px;
975
+ font-size: 11px;
976
+ color: #777;
977
+ max-width: 200px;
978
+ cursor: pointer;
979
+ backdrop-filter: blur(6px);
980
+ -webkit-backdrop-filter: blur(6px);
981
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
982
+ }
983
+ .attachment-chip:hover {
984
+ color: #ededed;
985
+ border-color: rgba(255, 255, 255, 0.25);
986
+ background: rgba(0, 0, 0, 0.75);
987
+ }
988
+ .attachment-chip img {
989
+ width: 20px;
990
+ height: 20px;
991
+ object-fit: cover;
992
+ border-radius: 50%;
993
+ flex-shrink: 0;
994
+ cursor: pointer;
995
+ }
996
+ .attachment-chip .file-icon {
997
+ width: 20px;
998
+ height: 20px;
999
+ border-radius: 50%;
1000
+ background: rgba(255,255,255,0.1);
1001
+ display: grid;
1002
+ place-items: center;
1003
+ font-size: 11px;
1004
+ flex-shrink: 0;
1005
+ }
1006
+ .attachment-chip .remove-attachment {
1007
+ cursor: pointer;
1008
+ color: #555;
1009
+ font-size: 14px;
1010
+ margin-left: 2px;
1011
+ line-height: 1;
1012
+ transition: color 0.15s;
1013
+ }
1014
+ .attachment-chip .remove-attachment:hover { color: #fff; }
1015
+ .attachment-chip .filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100px; }
1016
+ .user-bubble .user-file-attachments {
1017
+ display: flex;
1018
+ gap: 6px;
1019
+ flex-wrap: wrap;
1020
+ margin-top: 8px;
1021
+ }
1022
+ .user-file-attachments img {
1023
+ max-width: 200px;
1024
+ max-height: 160px;
1025
+ border-radius: 8px;
1026
+ object-fit: cover;
1027
+ cursor: pointer;
1028
+ transition: opacity 0.15s;
1029
+ }
1030
+ .user-file-attachments img:hover { opacity: 0.85; }
1031
+ .lightbox {
1032
+ position: fixed;
1033
+ inset: 0;
1034
+ z-index: 9999;
1035
+ display: flex;
1036
+ align-items: center;
1037
+ justify-content: center;
1038
+ background: rgba(0,0,0,0);
1039
+ backdrop-filter: blur(0px);
1040
+ cursor: zoom-out;
1041
+ transition: background 0.25s ease, backdrop-filter 0.25s ease;
1042
+ }
1043
+ .lightbox.active {
1044
+ background: rgba(0,0,0,0.85);
1045
+ backdrop-filter: blur(8px);
1046
+ }
1047
+ .lightbox img {
1048
+ max-width: 90vw;
1049
+ max-height: 90vh;
1050
+ border-radius: 8px;
1051
+ object-fit: contain;
1052
+ transform: scale(0.4);
1053
+ opacity: 0;
1054
+ transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.25s ease;
1055
+ }
1056
+ .lightbox.active img {
1057
+ transform: scale(1);
1058
+ opacity: 1;
1059
+ }
1060
+ .user-file-badge {
1061
+ display: inline-flex;
1062
+ align-items: center;
1063
+ gap: 4px;
1064
+ background: rgba(0,0,0,0.2);
1065
+ border-radius: 6px;
1066
+ padding: 4px 8px;
1067
+ font-size: 12px;
1068
+ color: rgba(255,255,255,0.8);
1069
+ }
1070
+ .drag-overlay {
1071
+ position: fixed;
1072
+ inset: 0;
1073
+ background: rgba(0,0,0,0.6);
1074
+ z-index: 9999;
1075
+ display: none;
1076
+ align-items: center;
1077
+ justify-content: center;
1078
+ pointer-events: none;
1079
+ }
1080
+ .drag-overlay.active { display: flex; }
1081
+ .drag-overlay-inner {
1082
+ border: 2px dashed rgba(255,255,255,0.4);
1083
+ border-radius: 16px;
1084
+ padding: 40px 60px;
1085
+ color: #fff;
1086
+ font-size: 16px;
1087
+ }
941
1088
  .disclaimer {
942
1089
  text-align: center;
943
1090
  color: #333;
@@ -1081,7 +1228,12 @@ var renderWebUiHtml = (options) => {
1081
1228
  </div>
1082
1229
  <form id="composer" class="composer">
1083
1230
  <div class="composer-inner">
1231
+ <div id="attachment-preview" class="attachment-preview" style="display:none"></div>
1084
1232
  <div class="composer-shell">
1233
+ <button id="attach-btn" class="attach-btn" type="button" title="Attach files">
1234
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
1235
+ </button>
1236
+ <input id="file-input" type="file" multiple accept="image/*,video/*,application/pdf,.txt,.csv,.json,.html" style="display:none" />
1085
1237
  <textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
1086
1238
  <button id="send" class="send-btn" type="submit">
1087
1239
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
@@ -1091,6 +1243,8 @@ var renderWebUiHtml = (options) => {
1091
1243
  </form>
1092
1244
  </main>
1093
1245
  </div>
1246
+ <div id="drag-overlay" class="drag-overlay"><div class="drag-overlay-inner">Drop files to attach</div></div>
1247
+ <div id="lightbox" class="lightbox" style="display:none"><img /></div>
1094
1248
 
1095
1249
  <script>
1096
1250
  // Marked library (inlined)
@@ -1113,7 +1267,8 @@ var renderWebUiHtml = (options) => {
1113
1267
  activeStreamRunId: null,
1114
1268
  isMessagesPinnedToBottom: true,
1115
1269
  confirmDeleteId: null,
1116
- approvalRequestsInFlight: {}
1270
+ approvalRequestsInFlight: {},
1271
+ pendingFiles: [],
1117
1272
  };
1118
1273
 
1119
1274
  const agentInitial = document.body.dataset.agentInitial || "A";
@@ -1135,7 +1290,12 @@ var renderWebUiHtml = (options) => {
1135
1290
  send: $("send"),
1136
1291
  shell: $("app"),
1137
1292
  sidebarToggle: $("sidebar-toggle"),
1138
- sidebarBackdrop: $("sidebar-backdrop")
1293
+ sidebarBackdrop: $("sidebar-backdrop"),
1294
+ attachBtn: $("attach-btn"),
1295
+ fileInput: $("file-input"),
1296
+ attachmentPreview: $("attachment-preview"),
1297
+ dragOverlay: $("drag-overlay"),
1298
+ lightbox: $("lightbox"),
1139
1299
  };
1140
1300
  const sendIconMarkup =
1141
1301
  '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
@@ -1631,7 +1791,47 @@ var renderWebUiHtml = (options) => {
1631
1791
  wrap.appendChild(content);
1632
1792
  row.appendChild(wrap);
1633
1793
  } else {
1634
- row.innerHTML = '<div class="user-bubble">' + escapeHtml(m.content) + '</div>';
1794
+ const bubble = document.createElement("div");
1795
+ bubble.className = "user-bubble";
1796
+ if (typeof m.content === "string") {
1797
+ bubble.textContent = m.content;
1798
+ } else if (Array.isArray(m.content)) {
1799
+ const textParts = m.content.filter(p => p.type === "text").map(p => p.text).join("");
1800
+ if (textParts) {
1801
+ const textEl = document.createElement("div");
1802
+ textEl.textContent = textParts;
1803
+ bubble.appendChild(textEl);
1804
+ }
1805
+ const fileParts = m.content.filter(p => p.type === "file");
1806
+ if (fileParts.length > 0) {
1807
+ const filesEl = document.createElement("div");
1808
+ filesEl.className = "user-file-attachments";
1809
+ fileParts.forEach(fp => {
1810
+ if (fp.mediaType && fp.mediaType.startsWith("image/")) {
1811
+ const img = document.createElement("img");
1812
+ if (fp._localBlob) {
1813
+ if (!fp._cachedUrl) fp._cachedUrl = URL.createObjectURL(fp._localBlob);
1814
+ img.src = fp._cachedUrl;
1815
+ } else if (fp.data && fp.data.startsWith("poncho-upload://")) {
1816
+ img.src = "/api/uploads/" + encodeURIComponent(fp.data.replace("poncho-upload://", ""));
1817
+ } else if (fp.data && (fp.data.startsWith("http://") || fp.data.startsWith("https://"))) {
1818
+ img.src = fp.data;
1819
+ } else if (fp.data) {
1820
+ img.src = "data:" + fp.mediaType + ";base64," + fp.data;
1821
+ }
1822
+ img.alt = fp.filename || "image";
1823
+ filesEl.appendChild(img);
1824
+ } else {
1825
+ const badge = document.createElement("span");
1826
+ badge.className = "user-file-badge";
1827
+ badge.textContent = "\u{1F4CE} " + (fp.filename || "file");
1828
+ filesEl.appendChild(badge);
1829
+ }
1830
+ });
1831
+ bubble.appendChild(filesEl);
1832
+ }
1833
+ }
1834
+ row.appendChild(bubble);
1635
1835
  }
1636
1836
  col.appendChild(row);
1637
1837
  });
@@ -2137,12 +2337,62 @@ var renderWebUiHtml = (options) => {
2137
2337
  }
2138
2338
  };
2139
2339
 
2340
+ const renderAttachmentPreview = () => {
2341
+ const el = elements.attachmentPreview;
2342
+ if (state.pendingFiles.length === 0) {
2343
+ el.style.display = "none";
2344
+ el.innerHTML = "";
2345
+ return;
2346
+ }
2347
+ el.style.display = "flex";
2348
+ el.innerHTML = state.pendingFiles.map((f, i) => {
2349
+ const isImage = f.type.startsWith("image/");
2350
+ const thumbHtml = isImage
2351
+ ? '<img src="' + URL.createObjectURL(f) + '" alt="" />'
2352
+ : '<span class="file-icon">\u{1F4CE}</span>';
2353
+ return '<div class="attachment-chip" data-idx="' + i + '">'
2354
+ + thumbHtml
2355
+ + '<span class="filename">' + escapeHtml(f.name) + '</span>'
2356
+ + '<span class="remove-attachment" data-idx="' + i + '">&times;</span>'
2357
+ + '</div>';
2358
+ }).join("");
2359
+ };
2360
+
2361
+ const addFiles = (fileList) => {
2362
+ for (const f of fileList) {
2363
+ if (f.size > 25 * 1024 * 1024) {
2364
+ alert("File too large: " + f.name + " (max 25MB)");
2365
+ continue;
2366
+ }
2367
+ state.pendingFiles.push(f);
2368
+ }
2369
+ renderAttachmentPreview();
2370
+ };
2371
+
2140
2372
  const sendMessage = async (text) => {
2141
2373
  const messageText = (text || "").trim();
2142
2374
  if (!messageText || state.isStreaming) {
2143
2375
  return;
2144
2376
  }
2145
- const localMessages = [...(state.activeMessages || []), { role: "user", content: messageText }];
2377
+ const filesToSend = [...state.pendingFiles];
2378
+ state.pendingFiles = [];
2379
+ renderAttachmentPreview();
2380
+ let userContent;
2381
+ if (filesToSend.length > 0) {
2382
+ userContent = [{ type: "text", text: messageText }];
2383
+ for (const f of filesToSend) {
2384
+ userContent.push({
2385
+ type: "file",
2386
+ data: URL.createObjectURL(f),
2387
+ mediaType: f.type,
2388
+ filename: f.name,
2389
+ _localBlob: f,
2390
+ });
2391
+ }
2392
+ } else {
2393
+ userContent = messageText;
2394
+ }
2395
+ const localMessages = [...(state.activeMessages || []), { role: "user", content: userContent }];
2146
2396
  let assistantMessage = {
2147
2397
  role: "assistant",
2148
2398
  content: "",
@@ -2185,13 +2435,38 @@ var renderWebUiHtml = (options) => {
2185
2435
  assistantMessage._currentText = "";
2186
2436
  }
2187
2437
  };
2188
- const response = await fetch("/api/conversations/" + encodeURIComponent(conversationId) + "/messages", {
2189
- method: "POST",
2190
- credentials: "include",
2191
- headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
2192
- body: JSON.stringify({ message: messageText }),
2193
- signal: streamAbortController.signal,
2194
- });
2438
+ let _continuationMessage = messageText;
2439
+ let _totalSteps = 0;
2440
+ let _maxSteps = 0;
2441
+ while (_continuationMessage) {
2442
+ let _shouldContinue = false;
2443
+ let fetchOpts;
2444
+ if (filesToSend.length > 0 && _continuationMessage === messageText) {
2445
+ const formData = new FormData();
2446
+ formData.append("message", _continuationMessage);
2447
+ for (const f of filesToSend) {
2448
+ formData.append("files", f, f.name);
2449
+ }
2450
+ fetchOpts = {
2451
+ method: "POST",
2452
+ credentials: "include",
2453
+ headers: { "x-csrf-token": state.csrfToken },
2454
+ body: formData,
2455
+ signal: streamAbortController.signal,
2456
+ };
2457
+ } else {
2458
+ fetchOpts = {
2459
+ method: "POST",
2460
+ credentials: "include",
2461
+ headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
2462
+ body: JSON.stringify({ message: _continuationMessage }),
2463
+ signal: streamAbortController.signal,
2464
+ };
2465
+ }
2466
+ const response = await fetch(
2467
+ "/api/conversations/" + encodeURIComponent(conversationId) + "/messages",
2468
+ fetchOpts,
2469
+ );
2195
2470
  if (!response.ok || !response.body) {
2196
2471
  throw new Error("Failed to stream response");
2197
2472
  }
@@ -2366,11 +2641,17 @@ var renderWebUiHtml = (options) => {
2366
2641
  renderIfActiveConversation(true);
2367
2642
  }
2368
2643
  if (eventName === "run:completed") {
2369
- finalizeAssistantMessage();
2370
- if (!assistantMessage.content || assistantMessage.content.length === 0) {
2371
- assistantMessage.content = String(payload.result?.response || "");
2644
+ _totalSteps += typeof payload.result?.steps === "number" ? payload.result.steps : 0;
2645
+ if (typeof payload.result?.maxSteps === "number") _maxSteps = payload.result.maxSteps;
2646
+ if (payload.result?.continuation === true && (_maxSteps <= 0 || _totalSteps < _maxSteps)) {
2647
+ _shouldContinue = true;
2648
+ } else {
2649
+ finalizeAssistantMessage();
2650
+ if (!assistantMessage.content || assistantMessage.content.length === 0) {
2651
+ assistantMessage.content = String(payload.result?.response || "");
2652
+ }
2653
+ renderIfActiveConversation(false);
2372
2654
  }
2373
- renderIfActiveConversation(false);
2374
2655
  }
2375
2656
  if (eventName === "run:cancelled") {
2376
2657
  finalizeAssistantMessage();
@@ -2388,6 +2669,9 @@ var renderWebUiHtml = (options) => {
2388
2669
  }
2389
2670
  });
2390
2671
  }
2672
+ if (!_shouldContinue) break;
2673
+ _continuationMessage = "Continue";
2674
+ }
2391
2675
  // Update active state only if user is still on this conversation.
2392
2676
  if (state.activeConversationId === streamConversationId) {
2393
2677
  state.activeMessages = localMessages;
@@ -2534,6 +2818,83 @@ var renderWebUiHtml = (options) => {
2534
2818
  await sendMessage(value);
2535
2819
  });
2536
2820
 
2821
+ elements.attachBtn.addEventListener("click", () => elements.fileInput.click());
2822
+ elements.fileInput.addEventListener("change", () => {
2823
+ if (elements.fileInput.files && elements.fileInput.files.length > 0) {
2824
+ addFiles(elements.fileInput.files);
2825
+ elements.fileInput.value = "";
2826
+ }
2827
+ });
2828
+ elements.attachmentPreview.addEventListener("click", (e) => {
2829
+ const rm = e.target.closest(".remove-attachment");
2830
+ if (rm) {
2831
+ const idx = parseInt(rm.dataset.idx, 10);
2832
+ state.pendingFiles.splice(idx, 1);
2833
+ renderAttachmentPreview();
2834
+ }
2835
+ });
2836
+
2837
+ let dragCounter = 0;
2838
+ document.addEventListener("dragenter", (e) => {
2839
+ e.preventDefault();
2840
+ dragCounter++;
2841
+ if (dragCounter === 1) elements.dragOverlay.classList.add("active");
2842
+ });
2843
+ document.addEventListener("dragleave", (e) => {
2844
+ e.preventDefault();
2845
+ dragCounter--;
2846
+ if (dragCounter <= 0) { dragCounter = 0; elements.dragOverlay.classList.remove("active"); }
2847
+ });
2848
+ document.addEventListener("dragover", (e) => e.preventDefault());
2849
+ document.addEventListener("drop", (e) => {
2850
+ e.preventDefault();
2851
+ dragCounter = 0;
2852
+ elements.dragOverlay.classList.remove("active");
2853
+ if (e.dataTransfer && e.dataTransfer.files.length > 0) {
2854
+ addFiles(e.dataTransfer.files);
2855
+ }
2856
+ });
2857
+
2858
+ // Lightbox: open/close helpers
2859
+ const lightboxImg = elements.lightbox.querySelector("img");
2860
+ const openLightbox = (src) => {
2861
+ lightboxImg.src = src;
2862
+ elements.lightbox.style.display = "flex";
2863
+ requestAnimationFrame(() => {
2864
+ requestAnimationFrame(() => elements.lightbox.classList.add("active"));
2865
+ });
2866
+ };
2867
+ const closeLightbox = () => {
2868
+ elements.lightbox.classList.remove("active");
2869
+ elements.lightbox.addEventListener("transitionend", function handler() {
2870
+ elements.lightbox.removeEventListener("transitionend", handler);
2871
+ elements.lightbox.style.display = "none";
2872
+ lightboxImg.src = "";
2873
+ });
2874
+ };
2875
+ elements.lightbox.addEventListener("click", closeLightbox);
2876
+ document.addEventListener("keydown", (e) => {
2877
+ if (e.key === "Escape" && elements.lightbox.style.display !== "none") closeLightbox();
2878
+ });
2879
+
2880
+ // Lightbox from message images
2881
+ elements.messages.addEventListener("click", (e) => {
2882
+ const img = e.target;
2883
+ if (!(img instanceof HTMLImageElement) || !img.closest(".user-file-attachments")) return;
2884
+ openLightbox(img.src);
2885
+ });
2886
+
2887
+ // Lightbox from attachment preview chips
2888
+ elements.attachmentPreview.addEventListener("click", (e) => {
2889
+ if (e.target.closest(".remove-attachment")) return;
2890
+ const chip = e.target.closest(".attachment-chip");
2891
+ if (!chip) return;
2892
+ const img = chip.querySelector("img");
2893
+ if (!img) return;
2894
+ e.stopPropagation();
2895
+ openLightbox(img.src);
2896
+ });
2897
+
2537
2898
  elements.messages.addEventListener("click", async (event) => {
2538
2899
  const target = event.target;
2539
2900
  if (!(target instanceof Element)) {
@@ -3374,6 +3735,24 @@ var writeHtml = (response, statusCode, payload) => {
3374
3735
  response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
3375
3736
  response.end(payload);
3376
3737
  };
3738
+ var EXT_MIME_MAP = {
3739
+ jpg: "image/jpeg",
3740
+ jpeg: "image/jpeg",
3741
+ png: "image/png",
3742
+ gif: "image/gif",
3743
+ webp: "image/webp",
3744
+ svg: "image/svg+xml",
3745
+ pdf: "application/pdf",
3746
+ mp4: "video/mp4",
3747
+ webm: "video/webm",
3748
+ mp3: "audio/mpeg",
3749
+ wav: "audio/wav",
3750
+ txt: "text/plain",
3751
+ json: "application/json",
3752
+ csv: "text/csv",
3753
+ html: "text/html"
3754
+ };
3755
+ var extToMime = (ext) => EXT_MIME_MAP[ext] ?? "application/octet-stream";
3377
3756
  var readRequestBody = async (request) => {
3378
3757
  const chunks = [];
3379
3758
  for await (const chunk of request) {
@@ -3382,6 +3761,38 @@ var readRequestBody = async (request) => {
3382
3761
  const body = Buffer.concat(chunks).toString("utf8");
3383
3762
  return body.length > 0 ? JSON.parse(body) : {};
3384
3763
  };
3764
+ var MAX_UPLOAD_SIZE = 25 * 1024 * 1024;
3765
+ var parseMultipartRequest = (request) => new Promise((resolve4, reject) => {
3766
+ const result = { message: "", files: [] };
3767
+ const bb = Busboy({
3768
+ headers: request.headers,
3769
+ limits: { fileSize: MAX_UPLOAD_SIZE }
3770
+ });
3771
+ bb.on("field", (name, value) => {
3772
+ if (name === "message") result.message = value;
3773
+ if (name === "parameters") {
3774
+ try {
3775
+ result.parameters = JSON.parse(value);
3776
+ } catch {
3777
+ }
3778
+ }
3779
+ });
3780
+ bb.on("file", (_name, stream, info) => {
3781
+ const chunks = [];
3782
+ stream.on("data", (chunk) => chunks.push(chunk));
3783
+ stream.on("end", () => {
3784
+ const buf = Buffer.concat(chunks);
3785
+ result.files.push({
3786
+ data: buf.toString("base64"),
3787
+ mediaType: info.mimeType,
3788
+ filename: info.filename
3789
+ });
3790
+ });
3791
+ });
3792
+ bb.on("finish", () => resolve4(result));
3793
+ bb.on("error", (err) => reject(err));
3794
+ request.pipe(bb);
3795
+ });
3385
3796
  var resolveHarnessEnvironment = () => {
3386
3797
  if (process.env.PONCHO_ENV) {
3387
3798
  const value = process.env.PONCHO_ENV.toLowerCase();
@@ -3903,7 +4314,14 @@ var writeScaffoldFile = async (filePath, content, options) => {
3903
4314
  await writeFile3(filePath, content, "utf8");
3904
4315
  options.writtenPaths.push(relative(options.baseDir, filePath));
3905
4316
  };
3906
- var ensureRuntimeCliDependency = async (projectDir, cliVersion) => {
4317
+ var UPLOAD_PROVIDER_DEPS = {
4318
+ "vercel-blob": [{ name: "@vercel/blob", fallback: "^2.3.0" }],
4319
+ s3: [
4320
+ { name: "@aws-sdk/client-s3", fallback: "^3.700.0" },
4321
+ { name: "@aws-sdk/s3-request-presigner", fallback: "^3.700.0" }
4322
+ ]
4323
+ };
4324
+ var ensureRuntimeCliDependency = async (projectDir, cliVersion, config) => {
3907
4325
  const packageJsonPath = resolve3(projectDir, "package.json");
3908
4326
  const content = await readFile3(packageJsonPath, "utf8");
3909
4327
  const parsed = JSON.parse(content);
@@ -3917,10 +4335,20 @@ var ensureRuntimeCliDependency = async (projectDir, cliVersion) => {
3917
4335
  }
3918
4336
  dependencies.marked = await readCliDependencyVersion("marked", "^17.0.2");
3919
4337
  dependencies["@poncho-ai/cli"] = `^${cliVersion}`;
4338
+ const addedDeps = [];
4339
+ const uploadsProvider = config?.uploads?.provider;
4340
+ if (uploadsProvider && UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
4341
+ for (const dep of UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
4342
+ if (!dependencies[dep.name]) {
4343
+ dependencies[dep.name] = dep.fallback;
4344
+ addedDeps.push(dep.name);
4345
+ }
4346
+ }
4347
+ }
3920
4348
  parsed.dependencies = dependencies;
3921
4349
  await writeFile3(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
3922
4350
  `, "utf8");
3923
- return [relative(projectDir, packageJsonPath)];
4351
+ return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
3924
4352
  };
3925
4353
  var scaffoldDeployTarget = async (projectDir, target, options) => {
3926
4354
  const writtenPaths = [];
@@ -4053,10 +4481,16 @@ CMD ["node","server.js"]
4053
4481
  baseDir: projectDir
4054
4482
  });
4055
4483
  }
4056
- const packagePaths = await ensureRuntimeCliDependency(projectDir, cliVersion);
4057
- for (const path of packagePaths) {
4058
- if (!writtenPaths.includes(path)) {
4059
- writtenPaths.push(path);
4484
+ const config = await loadPonchoConfig(projectDir);
4485
+ const { paths: packagePaths, addedDeps } = await ensureRuntimeCliDependency(
4486
+ projectDir,
4487
+ cliVersion,
4488
+ config
4489
+ );
4490
+ const depNote = addedDeps.length > 0 ? ` (added ${addedDeps.join(", ")})` : "";
4491
+ for (const p of packagePaths) {
4492
+ if (!writtenPaths.includes(p)) {
4493
+ writtenPaths.push(depNote ? `${p}${depNote}` : p);
4060
4494
  }
4061
4495
  }
4062
4496
  return writtenPaths;
@@ -4312,9 +4746,11 @@ var createRequestHandler = async (options) => {
4312
4746
  }
4313
4747
  await persistConversationPendingApprovals(conversationId);
4314
4748
  };
4749
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
4315
4750
  const harness = new AgentHarness({
4316
4751
  workingDir,
4317
4752
  environment: resolveHarnessEnvironment(),
4753
+ uploadStore,
4318
4754
  approvalHandler: async (request) => new Promise((resolveApproval) => {
4319
4755
  const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
4320
4756
  const conversationIdForRun = runConversations.get(request.runId) ?? null;
@@ -4712,6 +5148,40 @@ var createRequestHandler = async (options) => {
4712
5148
  });
4713
5149
  return;
4714
5150
  }
5151
+ const uploadMatch = pathname.match(/^\/api\/uploads\/(.+)$/);
5152
+ if (uploadMatch && request.method === "GET") {
5153
+ const key = decodeURIComponent(uploadMatch[1] ?? "");
5154
+ try {
5155
+ const data = await uploadStore.get(key);
5156
+ const ext = key.split(".").pop() ?? "";
5157
+ const mimeMap = {
5158
+ jpg: "image/jpeg",
5159
+ jpeg: "image/jpeg",
5160
+ png: "image/png",
5161
+ gif: "image/gif",
5162
+ webp: "image/webp",
5163
+ svg: "image/svg+xml",
5164
+ pdf: "application/pdf",
5165
+ mp4: "video/mp4",
5166
+ webm: "video/webm",
5167
+ mp3: "audio/mpeg",
5168
+ wav: "audio/wav",
5169
+ txt: "text/plain",
5170
+ json: "application/json",
5171
+ csv: "text/csv",
5172
+ html: "text/html"
5173
+ };
5174
+ response.writeHead(200, {
5175
+ "Content-Type": mimeMap[ext] ?? "application/octet-stream",
5176
+ "Content-Length": data.length,
5177
+ "Cache-Control": "public, max-age=86400"
5178
+ });
5179
+ response.end(data);
5180
+ } catch {
5181
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Upload not found" });
5182
+ }
5183
+ return;
5184
+ }
4715
5185
  const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
4716
5186
  if (conversationMessageMatch && request.method === "POST") {
4717
5187
  const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
@@ -4723,8 +5193,25 @@ var createRequestHandler = async (options) => {
4723
5193
  });
4724
5194
  return;
4725
5195
  }
4726
- const body = await readRequestBody(request);
4727
- const messageText = body.message?.trim() ?? "";
5196
+ let messageText = "";
5197
+ let bodyParameters;
5198
+ let files = [];
5199
+ const contentType = request.headers["content-type"] ?? "";
5200
+ if (contentType.includes("multipart/form-data")) {
5201
+ const parsed = await parseMultipartRequest(request);
5202
+ messageText = parsed.message.trim();
5203
+ bodyParameters = parsed.parameters;
5204
+ files = parsed.files;
5205
+ } else {
5206
+ const body = await readRequestBody(request);
5207
+ messageText = body.message?.trim() ?? "";
5208
+ bodyParameters = body.parameters;
5209
+ if (Array.isArray(body.files)) {
5210
+ files = body.files.filter(
5211
+ (f) => typeof f.data === "string" && typeof f.mediaType === "string"
5212
+ );
5213
+ }
5214
+ }
4728
5215
  if (!messageText) {
4729
5216
  writeJson(response, 400, {
4730
5217
  code: "VALIDATION_ERROR",
@@ -4766,8 +5253,43 @@ var createRequestHandler = async (options) => {
4766
5253
  let currentText = "";
4767
5254
  let currentTools = [];
4768
5255
  let runCancelled = false;
5256
+ let userContent = messageText;
5257
+ if (files.length > 0) {
5258
+ try {
5259
+ const uploadedParts = await Promise.all(
5260
+ files.map(async (f) => {
5261
+ const buf = Buffer.from(f.data, "base64");
5262
+ const key = deriveUploadKey(buf, f.mediaType);
5263
+ const ref = await uploadStore.put(key, buf, f.mediaType);
5264
+ return {
5265
+ type: "file",
5266
+ data: ref,
5267
+ mediaType: f.mediaType,
5268
+ filename: f.filename
5269
+ };
5270
+ })
5271
+ );
5272
+ userContent = [
5273
+ { type: "text", text: messageText },
5274
+ ...uploadedParts
5275
+ ];
5276
+ } catch (uploadErr) {
5277
+ const errMsg = uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
5278
+ console.error("[poncho] File upload failed:", errMsg);
5279
+ const errorEvent = {
5280
+ type: "run:error",
5281
+ runId: "",
5282
+ error: { code: "UPLOAD_ERROR", message: `File upload failed: ${errMsg}` }
5283
+ };
5284
+ broadcastEvent(conversationId, errorEvent);
5285
+ finishConversationStream(conversationId);
5286
+ activeConversationRuns.delete(conversationId);
5287
+ response.end();
5288
+ return;
5289
+ }
5290
+ }
4769
5291
  try {
4770
- conversation.messages = [...historyMessages, { role: "user", content: messageText }];
5292
+ conversation.messages = [...historyMessages, { role: "user", content: userContent }];
4771
5293
  conversation.updatedAt = Date.now();
4772
5294
  await conversationStore.update(conversation);
4773
5295
  const persistDraftAssistantTurn = async () => {
@@ -4789,7 +5311,7 @@ var createRequestHandler = async (options) => {
4789
5311
  }
4790
5312
  conversation.messages = [
4791
5313
  ...historyMessages,
4792
- { role: "user", content: messageText },
5314
+ { role: "user", content: userContent },
4793
5315
  {
4794
5316
  role: "assistant",
4795
5317
  content: assistantResponse,
@@ -4806,16 +5328,17 @@ var createRequestHandler = async (options) => {
4806
5328
  conversationId: item.conversationId,
4807
5329
  title: item.title,
4808
5330
  updatedAt: item.updatedAt,
4809
- content: item.messages.slice(-6).map((message) => `${message.role}: ${message.content}`).join("\n").slice(0, 2e3)
5331
+ content: item.messages.slice(-6).map((message) => `${message.role}: ${typeof message.content === "string" ? message.content : getTextContent(message)}`).join("\n").slice(0, 2e3)
4810
5332
  })).filter((item) => item.content.length > 0);
4811
5333
  for await (const event of harness.runWithTelemetry({
4812
5334
  task: messageText,
4813
5335
  parameters: {
4814
- ...body.parameters ?? {},
5336
+ ...bodyParameters ?? {},
4815
5337
  __conversationRecallCorpus: recallCorpus,
4816
5338
  __activeConversationId: conversationId
4817
5339
  },
4818
5340
  messages: historyMessages,
5341
+ files: files.length > 0 ? files : void 0,
4819
5342
  abortSignal: abortController.signal
4820
5343
  })) {
4821
5344
  if (event.type === "run:started") {
@@ -4857,6 +5380,9 @@ var createRequestHandler = async (options) => {
4857
5380
  toolTimeline.push(toolText);
4858
5381
  currentTools.push(toolText);
4859
5382
  }
5383
+ if (event.type === "step:completed") {
5384
+ await persistDraftAssistantTurn();
5385
+ }
4860
5386
  if (event.type === "tool:approval:required") {
4861
5387
  const toolText = `- approval required \`${event.tool}\``;
4862
5388
  toolTimeline.push(toolText);
@@ -4894,7 +5420,7 @@ var createRequestHandler = async (options) => {
4894
5420
  const hasAssistantContent = assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
4895
5421
  conversation.messages = hasAssistantContent ? [
4896
5422
  ...historyMessages,
4897
- { role: "user", content: messageText },
5423
+ { role: "user", content: userContent },
4898
5424
  {
4899
5425
  role: "assistant",
4900
5426
  content: assistantResponse,
@@ -4903,7 +5429,7 @@ var createRequestHandler = async (options) => {
4903
5429
  sections: sections.length > 0 ? sections : void 0
4904
5430
  } : void 0
4905
5431
  }
4906
- ] : [...historyMessages, { role: "user", content: messageText }];
5432
+ ] : [...historyMessages, { role: "user", content: userContent }];
4907
5433
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
4908
5434
  conversation.pendingApprovals = [];
4909
5435
  conversation.updatedAt = Date.now();
@@ -4920,7 +5446,7 @@ var createRequestHandler = async (options) => {
4920
5446
  if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
4921
5447
  conversation.messages = [
4922
5448
  ...historyMessages,
4923
- { role: "user", content: messageText },
5449
+ { role: "user", content: userContent },
4924
5450
  {
4925
5451
  role: "assistant",
4926
5452
  content: assistantResponse,
@@ -4958,7 +5484,7 @@ var createRequestHandler = async (options) => {
4958
5484
  if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
4959
5485
  conversation.messages = [
4960
5486
  ...historyMessages,
4961
- { role: "user", content: messageText },
5487
+ { role: "user", content: userContent },
4962
5488
  {
4963
5489
  role: "assistant",
4964
5490
  content: assistantResponse,
@@ -5016,21 +5542,26 @@ var runOnce = async (task, options) => {
5016
5542
  const workingDir = options.workingDir ?? process.cwd();
5017
5543
  dotenv.config({ path: resolve3(workingDir, ".env") });
5018
5544
  const config = await loadPonchoConfig(workingDir);
5019
- const harness = new AgentHarness({ workingDir });
5545
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
5546
+ const harness = new AgentHarness({ workingDir, uploadStore });
5020
5547
  const telemetry = new TelemetryEmitter(config?.telemetry);
5021
5548
  await harness.initialize();
5022
- const fileBlobs = await Promise.all(
5023
- options.filePaths.map(async (path) => {
5024
- const content = await readFile3(resolve3(workingDir, path), "utf8");
5025
- return `# File: ${path}
5026
- ${content}`;
5549
+ const fileInputs = await Promise.all(
5550
+ options.filePaths.map(async (filePath) => {
5551
+ const absPath = resolve3(workingDir, filePath);
5552
+ const buf = await readFile3(absPath);
5553
+ const ext = absPath.split(".").pop()?.toLowerCase() ?? "";
5554
+ return {
5555
+ data: buf.toString("base64"),
5556
+ mediaType: extToMime(ext),
5557
+ filename: basename2(filePath)
5558
+ };
5027
5559
  })
5028
5560
  );
5029
5561
  const input2 = {
5030
- task: fileBlobs.length > 0 ? `${task}
5031
-
5032
- ${fileBlobs.join("\n\n")}` : task,
5033
- parameters: options.params
5562
+ task,
5563
+ parameters: options.params,
5564
+ files: fileInputs.length > 0 ? fileInputs : void 0
5034
5565
  };
5035
5566
  if (options.json) {
5036
5567
  const output = await harness.runToCompletion(input2);
@@ -5079,15 +5610,17 @@ var runInteractive = async (workingDir, params) => {
5079
5610
  }
5080
5611
  });
5081
5612
  };
5613
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
5082
5614
  const harness = new AgentHarness({
5083
5615
  workingDir,
5084
5616
  environment: resolveHarnessEnvironment(),
5085
- approvalHandler
5617
+ approvalHandler,
5618
+ uploadStore
5086
5619
  });
5087
5620
  await harness.initialize();
5088
5621
  const identity = await ensureAgentIdentity2(workingDir);
5089
5622
  try {
5090
- const { runInteractiveInk } = await import("./run-interactive-ink-Z3U5SV4C.js");
5623
+ const { runInteractiveInk } = await import("./run-interactive-ink-QR3RIAJH.js");
5091
5624
  await runInteractiveInk({
5092
5625
  harness,
5093
5626
  params,