@poncho-ai/cli 0.10.2 → 0.11.1

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,100 @@ 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
+ // Paste files/images from clipboard
2859
+ elements.prompt.addEventListener("paste", (e) => {
2860
+ const items = e.clipboardData && e.clipboardData.items;
2861
+ if (!items) return;
2862
+ const files = [];
2863
+ for (let i = 0; i < items.length; i++) {
2864
+ if (items[i].kind === "file") {
2865
+ const f = items[i].getAsFile();
2866
+ if (f) files.push(f);
2867
+ }
2868
+ }
2869
+ if (files.length > 0) {
2870
+ e.preventDefault();
2871
+ addFiles(files);
2872
+ }
2873
+ });
2874
+
2875
+ // Lightbox: open/close helpers
2876
+ const lightboxImg = elements.lightbox.querySelector("img");
2877
+ const openLightbox = (src) => {
2878
+ lightboxImg.src = src;
2879
+ elements.lightbox.style.display = "flex";
2880
+ requestAnimationFrame(() => {
2881
+ requestAnimationFrame(() => elements.lightbox.classList.add("active"));
2882
+ });
2883
+ };
2884
+ const closeLightbox = () => {
2885
+ elements.lightbox.classList.remove("active");
2886
+ elements.lightbox.addEventListener("transitionend", function handler() {
2887
+ elements.lightbox.removeEventListener("transitionend", handler);
2888
+ elements.lightbox.style.display = "none";
2889
+ lightboxImg.src = "";
2890
+ });
2891
+ };
2892
+ elements.lightbox.addEventListener("click", closeLightbox);
2893
+ document.addEventListener("keydown", (e) => {
2894
+ if (e.key === "Escape" && elements.lightbox.style.display !== "none") closeLightbox();
2895
+ });
2896
+
2897
+ // Lightbox from message images
2898
+ elements.messages.addEventListener("click", (e) => {
2899
+ const img = e.target;
2900
+ if (!(img instanceof HTMLImageElement) || !img.closest(".user-file-attachments")) return;
2901
+ openLightbox(img.src);
2902
+ });
2903
+
2904
+ // Lightbox from attachment preview chips
2905
+ elements.attachmentPreview.addEventListener("click", (e) => {
2906
+ if (e.target.closest(".remove-attachment")) return;
2907
+ const chip = e.target.closest(".attachment-chip");
2908
+ if (!chip) return;
2909
+ const img = chip.querySelector("img");
2910
+ if (!img) return;
2911
+ e.stopPropagation();
2912
+ openLightbox(img.src);
2913
+ });
2914
+
2537
2915
  elements.messages.addEventListener("click", async (event) => {
2538
2916
  const target = event.target;
2539
2917
  if (!(target instanceof Element)) {
@@ -3374,6 +3752,24 @@ var writeHtml = (response, statusCode, payload) => {
3374
3752
  response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
3375
3753
  response.end(payload);
3376
3754
  };
3755
+ var EXT_MIME_MAP = {
3756
+ jpg: "image/jpeg",
3757
+ jpeg: "image/jpeg",
3758
+ png: "image/png",
3759
+ gif: "image/gif",
3760
+ webp: "image/webp",
3761
+ svg: "image/svg+xml",
3762
+ pdf: "application/pdf",
3763
+ mp4: "video/mp4",
3764
+ webm: "video/webm",
3765
+ mp3: "audio/mpeg",
3766
+ wav: "audio/wav",
3767
+ txt: "text/plain",
3768
+ json: "application/json",
3769
+ csv: "text/csv",
3770
+ html: "text/html"
3771
+ };
3772
+ var extToMime = (ext) => EXT_MIME_MAP[ext] ?? "application/octet-stream";
3377
3773
  var readRequestBody = async (request) => {
3378
3774
  const chunks = [];
3379
3775
  for await (const chunk of request) {
@@ -3382,6 +3778,38 @@ var readRequestBody = async (request) => {
3382
3778
  const body = Buffer.concat(chunks).toString("utf8");
3383
3779
  return body.length > 0 ? JSON.parse(body) : {};
3384
3780
  };
3781
+ var MAX_UPLOAD_SIZE = 25 * 1024 * 1024;
3782
+ var parseMultipartRequest = (request) => new Promise((resolve4, reject) => {
3783
+ const result = { message: "", files: [] };
3784
+ const bb = Busboy({
3785
+ headers: request.headers,
3786
+ limits: { fileSize: MAX_UPLOAD_SIZE }
3787
+ });
3788
+ bb.on("field", (name, value) => {
3789
+ if (name === "message") result.message = value;
3790
+ if (name === "parameters") {
3791
+ try {
3792
+ result.parameters = JSON.parse(value);
3793
+ } catch {
3794
+ }
3795
+ }
3796
+ });
3797
+ bb.on("file", (_name, stream, info) => {
3798
+ const chunks = [];
3799
+ stream.on("data", (chunk) => chunks.push(chunk));
3800
+ stream.on("end", () => {
3801
+ const buf = Buffer.concat(chunks);
3802
+ result.files.push({
3803
+ data: buf.toString("base64"),
3804
+ mediaType: info.mimeType,
3805
+ filename: info.filename
3806
+ });
3807
+ });
3808
+ });
3809
+ bb.on("finish", () => resolve4(result));
3810
+ bb.on("error", (err) => reject(err));
3811
+ request.pipe(bb);
3812
+ });
3385
3813
  var resolveHarnessEnvironment = () => {
3386
3814
  if (process.env.PONCHO_ENV) {
3387
3815
  const value = process.env.PONCHO_ENV.toLowerCase();
@@ -3903,7 +4331,14 @@ var writeScaffoldFile = async (filePath, content, options) => {
3903
4331
  await writeFile3(filePath, content, "utf8");
3904
4332
  options.writtenPaths.push(relative(options.baseDir, filePath));
3905
4333
  };
3906
- var ensureRuntimeCliDependency = async (projectDir, cliVersion) => {
4334
+ var UPLOAD_PROVIDER_DEPS = {
4335
+ "vercel-blob": [{ name: "@vercel/blob", fallback: "^2.3.0" }],
4336
+ s3: [
4337
+ { name: "@aws-sdk/client-s3", fallback: "^3.700.0" },
4338
+ { name: "@aws-sdk/s3-request-presigner", fallback: "^3.700.0" }
4339
+ ]
4340
+ };
4341
+ var ensureRuntimeCliDependency = async (projectDir, cliVersion, config) => {
3907
4342
  const packageJsonPath = resolve3(projectDir, "package.json");
3908
4343
  const content = await readFile3(packageJsonPath, "utf8");
3909
4344
  const parsed = JSON.parse(content);
@@ -3917,10 +4352,20 @@ var ensureRuntimeCliDependency = async (projectDir, cliVersion) => {
3917
4352
  }
3918
4353
  dependencies.marked = await readCliDependencyVersion("marked", "^17.0.2");
3919
4354
  dependencies["@poncho-ai/cli"] = `^${cliVersion}`;
4355
+ const addedDeps = [];
4356
+ const uploadsProvider = config?.uploads?.provider;
4357
+ if (uploadsProvider && UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
4358
+ for (const dep of UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
4359
+ if (!dependencies[dep.name]) {
4360
+ dependencies[dep.name] = dep.fallback;
4361
+ addedDeps.push(dep.name);
4362
+ }
4363
+ }
4364
+ }
3920
4365
  parsed.dependencies = dependencies;
3921
4366
  await writeFile3(packageJsonPath, `${JSON.stringify(parsed, null, 2)}
3922
4367
  `, "utf8");
3923
- return [relative(projectDir, packageJsonPath)];
4368
+ return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
3924
4369
  };
3925
4370
  var scaffoldDeployTarget = async (projectDir, target, options) => {
3926
4371
  const writtenPaths = [];
@@ -4053,10 +4498,16 @@ CMD ["node","server.js"]
4053
4498
  baseDir: projectDir
4054
4499
  });
4055
4500
  }
4056
- const packagePaths = await ensureRuntimeCliDependency(projectDir, cliVersion);
4057
- for (const path of packagePaths) {
4058
- if (!writtenPaths.includes(path)) {
4059
- writtenPaths.push(path);
4501
+ const config = await loadPonchoConfig(projectDir);
4502
+ const { paths: packagePaths, addedDeps } = await ensureRuntimeCliDependency(
4503
+ projectDir,
4504
+ cliVersion,
4505
+ config
4506
+ );
4507
+ const depNote = addedDeps.length > 0 ? ` (added ${addedDeps.join(", ")})` : "";
4508
+ for (const p of packagePaths) {
4509
+ if (!writtenPaths.includes(p)) {
4510
+ writtenPaths.push(depNote ? `${p}${depNote}` : p);
4060
4511
  }
4061
4512
  }
4062
4513
  return writtenPaths;
@@ -4312,9 +4763,11 @@ var createRequestHandler = async (options) => {
4312
4763
  }
4313
4764
  await persistConversationPendingApprovals(conversationId);
4314
4765
  };
4766
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
4315
4767
  const harness = new AgentHarness({
4316
4768
  workingDir,
4317
4769
  environment: resolveHarnessEnvironment(),
4770
+ uploadStore,
4318
4771
  approvalHandler: async (request) => new Promise((resolveApproval) => {
4319
4772
  const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
4320
4773
  const conversationIdForRun = runConversations.get(request.runId) ?? null;
@@ -4712,6 +5165,40 @@ var createRequestHandler = async (options) => {
4712
5165
  });
4713
5166
  return;
4714
5167
  }
5168
+ const uploadMatch = pathname.match(/^\/api\/uploads\/(.+)$/);
5169
+ if (uploadMatch && request.method === "GET") {
5170
+ const key = decodeURIComponent(uploadMatch[1] ?? "");
5171
+ try {
5172
+ const data = await uploadStore.get(key);
5173
+ const ext = key.split(".").pop() ?? "";
5174
+ const mimeMap = {
5175
+ jpg: "image/jpeg",
5176
+ jpeg: "image/jpeg",
5177
+ png: "image/png",
5178
+ gif: "image/gif",
5179
+ webp: "image/webp",
5180
+ svg: "image/svg+xml",
5181
+ pdf: "application/pdf",
5182
+ mp4: "video/mp4",
5183
+ webm: "video/webm",
5184
+ mp3: "audio/mpeg",
5185
+ wav: "audio/wav",
5186
+ txt: "text/plain",
5187
+ json: "application/json",
5188
+ csv: "text/csv",
5189
+ html: "text/html"
5190
+ };
5191
+ response.writeHead(200, {
5192
+ "Content-Type": mimeMap[ext] ?? "application/octet-stream",
5193
+ "Content-Length": data.length,
5194
+ "Cache-Control": "public, max-age=86400"
5195
+ });
5196
+ response.end(data);
5197
+ } catch {
5198
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Upload not found" });
5199
+ }
5200
+ return;
5201
+ }
4715
5202
  const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
4716
5203
  if (conversationMessageMatch && request.method === "POST") {
4717
5204
  const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
@@ -4723,8 +5210,25 @@ var createRequestHandler = async (options) => {
4723
5210
  });
4724
5211
  return;
4725
5212
  }
4726
- const body = await readRequestBody(request);
4727
- const messageText = body.message?.trim() ?? "";
5213
+ let messageText = "";
5214
+ let bodyParameters;
5215
+ let files = [];
5216
+ const contentType = request.headers["content-type"] ?? "";
5217
+ if (contentType.includes("multipart/form-data")) {
5218
+ const parsed = await parseMultipartRequest(request);
5219
+ messageText = parsed.message.trim();
5220
+ bodyParameters = parsed.parameters;
5221
+ files = parsed.files;
5222
+ } else {
5223
+ const body = await readRequestBody(request);
5224
+ messageText = body.message?.trim() ?? "";
5225
+ bodyParameters = body.parameters;
5226
+ if (Array.isArray(body.files)) {
5227
+ files = body.files.filter(
5228
+ (f) => typeof f.data === "string" && typeof f.mediaType === "string"
5229
+ );
5230
+ }
5231
+ }
4728
5232
  if (!messageText) {
4729
5233
  writeJson(response, 400, {
4730
5234
  code: "VALIDATION_ERROR",
@@ -4766,8 +5270,43 @@ var createRequestHandler = async (options) => {
4766
5270
  let currentText = "";
4767
5271
  let currentTools = [];
4768
5272
  let runCancelled = false;
5273
+ let userContent = messageText;
5274
+ if (files.length > 0) {
5275
+ try {
5276
+ const uploadedParts = await Promise.all(
5277
+ files.map(async (f) => {
5278
+ const buf = Buffer.from(f.data, "base64");
5279
+ const key = deriveUploadKey(buf, f.mediaType);
5280
+ const ref = await uploadStore.put(key, buf, f.mediaType);
5281
+ return {
5282
+ type: "file",
5283
+ data: ref,
5284
+ mediaType: f.mediaType,
5285
+ filename: f.filename
5286
+ };
5287
+ })
5288
+ );
5289
+ userContent = [
5290
+ { type: "text", text: messageText },
5291
+ ...uploadedParts
5292
+ ];
5293
+ } catch (uploadErr) {
5294
+ const errMsg = uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
5295
+ console.error("[poncho] File upload failed:", errMsg);
5296
+ const errorEvent = {
5297
+ type: "run:error",
5298
+ runId: "",
5299
+ error: { code: "UPLOAD_ERROR", message: `File upload failed: ${errMsg}` }
5300
+ };
5301
+ broadcastEvent(conversationId, errorEvent);
5302
+ finishConversationStream(conversationId);
5303
+ activeConversationRuns.delete(conversationId);
5304
+ response.end();
5305
+ return;
5306
+ }
5307
+ }
4769
5308
  try {
4770
- conversation.messages = [...historyMessages, { role: "user", content: messageText }];
5309
+ conversation.messages = [...historyMessages, { role: "user", content: userContent }];
4771
5310
  conversation.updatedAt = Date.now();
4772
5311
  await conversationStore.update(conversation);
4773
5312
  const persistDraftAssistantTurn = async () => {
@@ -4789,7 +5328,7 @@ var createRequestHandler = async (options) => {
4789
5328
  }
4790
5329
  conversation.messages = [
4791
5330
  ...historyMessages,
4792
- { role: "user", content: messageText },
5331
+ { role: "user", content: userContent },
4793
5332
  {
4794
5333
  role: "assistant",
4795
5334
  content: assistantResponse,
@@ -4806,16 +5345,17 @@ var createRequestHandler = async (options) => {
4806
5345
  conversationId: item.conversationId,
4807
5346
  title: item.title,
4808
5347
  updatedAt: item.updatedAt,
4809
- content: item.messages.slice(-6).map((message) => `${message.role}: ${message.content}`).join("\n").slice(0, 2e3)
5348
+ content: item.messages.slice(-6).map((message) => `${message.role}: ${typeof message.content === "string" ? message.content : getTextContent(message)}`).join("\n").slice(0, 2e3)
4810
5349
  })).filter((item) => item.content.length > 0);
4811
5350
  for await (const event of harness.runWithTelemetry({
4812
5351
  task: messageText,
4813
5352
  parameters: {
4814
- ...body.parameters ?? {},
5353
+ ...bodyParameters ?? {},
4815
5354
  __conversationRecallCorpus: recallCorpus,
4816
5355
  __activeConversationId: conversationId
4817
5356
  },
4818
5357
  messages: historyMessages,
5358
+ files: files.length > 0 ? files : void 0,
4819
5359
  abortSignal: abortController.signal
4820
5360
  })) {
4821
5361
  if (event.type === "run:started") {
@@ -4857,6 +5397,9 @@ var createRequestHandler = async (options) => {
4857
5397
  toolTimeline.push(toolText);
4858
5398
  currentTools.push(toolText);
4859
5399
  }
5400
+ if (event.type === "step:completed") {
5401
+ await persistDraftAssistantTurn();
5402
+ }
4860
5403
  if (event.type === "tool:approval:required") {
4861
5404
  const toolText = `- approval required \`${event.tool}\``;
4862
5405
  toolTimeline.push(toolText);
@@ -4894,7 +5437,7 @@ var createRequestHandler = async (options) => {
4894
5437
  const hasAssistantContent = assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
4895
5438
  conversation.messages = hasAssistantContent ? [
4896
5439
  ...historyMessages,
4897
- { role: "user", content: messageText },
5440
+ { role: "user", content: userContent },
4898
5441
  {
4899
5442
  role: "assistant",
4900
5443
  content: assistantResponse,
@@ -4903,7 +5446,7 @@ var createRequestHandler = async (options) => {
4903
5446
  sections: sections.length > 0 ? sections : void 0
4904
5447
  } : void 0
4905
5448
  }
4906
- ] : [...historyMessages, { role: "user", content: messageText }];
5449
+ ] : [...historyMessages, { role: "user", content: userContent }];
4907
5450
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
4908
5451
  conversation.pendingApprovals = [];
4909
5452
  conversation.updatedAt = Date.now();
@@ -4920,7 +5463,7 @@ var createRequestHandler = async (options) => {
4920
5463
  if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
4921
5464
  conversation.messages = [
4922
5465
  ...historyMessages,
4923
- { role: "user", content: messageText },
5466
+ { role: "user", content: userContent },
4924
5467
  {
4925
5468
  role: "assistant",
4926
5469
  content: assistantResponse,
@@ -4958,7 +5501,7 @@ var createRequestHandler = async (options) => {
4958
5501
  if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
4959
5502
  conversation.messages = [
4960
5503
  ...historyMessages,
4961
- { role: "user", content: messageText },
5504
+ { role: "user", content: userContent },
4962
5505
  {
4963
5506
  role: "assistant",
4964
5507
  content: assistantResponse,
@@ -5016,21 +5559,26 @@ var runOnce = async (task, options) => {
5016
5559
  const workingDir = options.workingDir ?? process.cwd();
5017
5560
  dotenv.config({ path: resolve3(workingDir, ".env") });
5018
5561
  const config = await loadPonchoConfig(workingDir);
5019
- const harness = new AgentHarness({ workingDir });
5562
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
5563
+ const harness = new AgentHarness({ workingDir, uploadStore });
5020
5564
  const telemetry = new TelemetryEmitter(config?.telemetry);
5021
5565
  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}`;
5566
+ const fileInputs = await Promise.all(
5567
+ options.filePaths.map(async (filePath) => {
5568
+ const absPath = resolve3(workingDir, filePath);
5569
+ const buf = await readFile3(absPath);
5570
+ const ext = absPath.split(".").pop()?.toLowerCase() ?? "";
5571
+ return {
5572
+ data: buf.toString("base64"),
5573
+ mediaType: extToMime(ext),
5574
+ filename: basename2(filePath)
5575
+ };
5027
5576
  })
5028
5577
  );
5029
5578
  const input2 = {
5030
- task: fileBlobs.length > 0 ? `${task}
5031
-
5032
- ${fileBlobs.join("\n\n")}` : task,
5033
- parameters: options.params
5579
+ task,
5580
+ parameters: options.params,
5581
+ files: fileInputs.length > 0 ? fileInputs : void 0
5034
5582
  };
5035
5583
  if (options.json) {
5036
5584
  const output = await harness.runToCompletion(input2);
@@ -5079,15 +5627,17 @@ var runInteractive = async (workingDir, params) => {
5079
5627
  }
5080
5628
  });
5081
5629
  };
5630
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
5082
5631
  const harness = new AgentHarness({
5083
5632
  workingDir,
5084
5633
  environment: resolveHarnessEnvironment(),
5085
- approvalHandler
5634
+ approvalHandler,
5635
+ uploadStore
5086
5636
  });
5087
5637
  await harness.initialize();
5088
5638
  const identity = await ensureAgentIdentity2(workingDir);
5089
5639
  try {
5090
- const { runInteractiveInk } = await import("./run-interactive-ink-Z3U5SV4C.js");
5640
+ const { runInteractiveInk } = await import("./run-interactive-ink-7FP5PT7Q.js");
5091
5641
  await runInteractiveInk({
5092
5642
  harness,
5093
5643
  params,