@poncho-ai/cli 0.17.0 → 0.19.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.
@@ -310,6 +310,22 @@ var WEB_UI_STYLES = `
310
310
  flex-direction: column;
311
311
  padding: 12px 8px;
312
312
  }
313
+ .sidebar-header {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 8px;
317
+ }
318
+ .sidebar-agent-name {
319
+ font-size: 14px;
320
+ font-weight: 500;
321
+ color: var(--fg-strong);
322
+ flex: 1;
323
+ min-width: 0;
324
+ overflow: hidden;
325
+ text-overflow: ellipsis;
326
+ white-space: nowrap;
327
+ padding-left: 10px;
328
+ }
313
329
  .new-chat-btn {
314
330
  background: transparent;
315
331
  border: 0;
@@ -322,6 +338,7 @@ var WEB_UI_STYLES = `
322
338
  gap: 8px;
323
339
  font-size: 13px;
324
340
  cursor: pointer;
341
+ flex-shrink: 0;
325
342
  transition: background 0.15s, color 0.15s;
326
343
  }
327
344
  .new-chat-btn:hover { color: var(--fg); }
@@ -824,6 +841,30 @@ var WEB_UI_STYLES = `
824
841
  opacity: 0.55;
825
842
  cursor: not-allowed;
826
843
  }
844
+ .approval-batch-actions {
845
+ display: flex;
846
+ gap: 6px;
847
+ margin-bottom: 8px;
848
+ }
849
+ .approval-batch-btn {
850
+ border-radius: 6px;
851
+ border: 1px solid var(--border-5);
852
+ background: var(--surface-4);
853
+ color: var(--fg-approval-btn);
854
+ font-size: 11px;
855
+ font-weight: 600;
856
+ padding: 4px 10px;
857
+ cursor: pointer;
858
+ }
859
+ .approval-batch-btn:hover { background: var(--surface-7); }
860
+ .approval-batch-btn.approve {
861
+ border-color: var(--approve-border);
862
+ color: var(--approve);
863
+ }
864
+ .approval-batch-btn.deny {
865
+ border-color: var(--deny-border);
866
+ color: var(--deny);
867
+ }
827
868
  .user-bubble {
828
869
  background: var(--bg-elevated);
829
870
  border: 1px solid var(--border-2);
@@ -1220,6 +1261,9 @@ var WEB_UI_STYLES = `
1220
1261
  .shell.sidebar-open .sidebar { transform: translateX(0); }
1221
1262
  .sidebar-toggle { display: grid; place-items: center; }
1222
1263
  .topbar-new-chat { display: grid; place-items: center; }
1264
+ .sidebar-header { padding-right: 130px; }
1265
+ .sidebar-agent-name { padding-left: 0; }
1266
+ .new-chat-btn { order: -1; }
1223
1267
  .poncho-badge {
1224
1268
  display: none;
1225
1269
  position: fixed;
@@ -1359,15 +1403,17 @@ var WEB_UI_STYLES = `
1359
1403
  font-size: 13px;
1360
1404
  }
1361
1405
  @media (max-width: 768px) {
1406
+ .main-body { flex-direction: column; }
1362
1407
  .browser-panel {
1363
- position: fixed;
1364
- inset: 0;
1365
- width: 100% !important;
1408
+ position: relative;
1409
+ order: -1;
1410
+ max-height: 35vh;
1366
1411
  flex: none !important;
1367
- z-index: 200;
1412
+ width: auto !important;
1413
+ border-bottom: 1px solid var(--border-1);
1368
1414
  }
1369
1415
  .browser-panel-resize { display: none !important; }
1370
- .main-chat.has-browser { flex: 1 1 auto !important; min-width: 0; }
1416
+ .main-chat.has-browser { flex: 1 1 auto !important; min-width: 0; min-height: 0; }
1371
1417
  }
1372
1418
 
1373
1419
  /* --- Subagent UI --- */
@@ -1649,6 +1695,62 @@ var getWebUiClientScript = (markedSource2) => `
1649
1695
  }
1650
1696
  };
1651
1697
 
1698
+ // During streaming, incomplete backtick sequences cause marked to
1699
+ // swallow all subsequent text into an invisible code element. This
1700
+ // helper detects unclosed fences and inline code delimiters and
1701
+ // appends the missing closing so marked can render partial text.
1702
+ const closeStreamingMarkdown = (text) => {
1703
+ const BT = "\\x60";
1704
+ let result = text;
1705
+
1706
+ // 1. Unclosed fenced code blocks (lines starting with 3+ backticks)
1707
+ const lines = result.split("\\n");
1708
+ let openFenceLen = 0;
1709
+ for (let li = 0; li < lines.length; li++) {
1710
+ const trimmed = lines[li].trimStart();
1711
+ let btCount = 0;
1712
+ while (btCount < trimmed.length && trimmed[btCount] === BT) btCount++;
1713
+ if (btCount >= 3) {
1714
+ if (openFenceLen === 0) {
1715
+ openFenceLen = btCount;
1716
+ } else if (btCount >= openFenceLen) {
1717
+ openFenceLen = 0;
1718
+ }
1719
+ }
1720
+ }
1721
+ if (openFenceLen > 0) {
1722
+ let fence = "";
1723
+ for (let k = 0; k < openFenceLen; k++) fence += BT;
1724
+ return result + "\\n" + fence;
1725
+ }
1726
+
1727
+ // 2. Unclosed inline code delimiters
1728
+ let idx = 0;
1729
+ let inCode = false;
1730
+ let delimLen = 0;
1731
+ while (idx < result.length) {
1732
+ if (result[idx] === BT) {
1733
+ let run = 0;
1734
+ while (idx < result.length && result[idx] === BT) { run++; idx++; }
1735
+ if (!inCode) {
1736
+ inCode = true;
1737
+ delimLen = run;
1738
+ } else if (run === delimLen) {
1739
+ inCode = false;
1740
+ }
1741
+ } else {
1742
+ idx++;
1743
+ }
1744
+ }
1745
+ if (inCode) {
1746
+ let closing = "";
1747
+ for (let k = 0; k < delimLen; k++) closing += BT;
1748
+ result += closing;
1749
+ }
1750
+
1751
+ return result;
1752
+ };
1753
+
1652
1754
  const extractToolActivity = (value) => {
1653
1755
  const source = String(value || "");
1654
1756
  let markerIndex = source.lastIndexOf("\\n### Tool activity\\n");
@@ -1715,8 +1817,15 @@ var getWebUiClientScript = (markedSource2) => `
1715
1817
  );
1716
1818
  })
1717
1819
  .join("");
1820
+ const batchButtons = requests.length > 1
1821
+ ? '<div class="approval-batch-actions">' +
1822
+ '<button class="approval-batch-btn approve" data-approval-batch="approve">Approve all (' + requests.length + ')</button>' +
1823
+ '<button class="approval-batch-btn deny" data-approval-batch="deny">Deny all (' + requests.length + ')</button>' +
1824
+ "</div>"
1825
+ : "";
1718
1826
  return (
1719
1827
  '<div class="approval-requests">' +
1828
+ batchButtons +
1720
1829
  rows +
1721
1830
  "</div>"
1722
1831
  );
@@ -2246,7 +2355,7 @@ var getWebUiClientScript = (markedSource2) => `
2246
2355
  // Show current text being typed
2247
2356
  if (isStreaming && i === messages.length - 1 && m._currentText) {
2248
2357
  const textDiv = document.createElement("div");
2249
- textDiv.innerHTML = renderAssistantMarkdown(m._currentText);
2358
+ textDiv.innerHTML = renderAssistantMarkdown(closeStreamingMarkdown(m._currentText));
2250
2359
  content.appendChild(textDiv);
2251
2360
  }
2252
2361
  } else {
@@ -2547,8 +2656,32 @@ var getWebUiClientScript = (markedSource2) => `
2547
2656
  updateContextRing();
2548
2657
  }
2549
2658
  }
2659
+ if (eventName === "tool:generating") {
2660
+ const toolName = payload.tool || "tool";
2661
+ if (!Array.isArray(assistantMessage._activeActivities)) {
2662
+ assistantMessage._activeActivities = [];
2663
+ }
2664
+ assistantMessage._activeActivities.push({
2665
+ kind: "generating",
2666
+ tool: toolName,
2667
+ label: "Preparing " + toolName,
2668
+ });
2669
+ if (assistantMessage._currentText.length > 0) {
2670
+ assistantMessage._sections.push({
2671
+ type: "text",
2672
+ content: assistantMessage._currentText,
2673
+ });
2674
+ assistantMessage._currentText = "";
2675
+ }
2676
+ const prepText =
2677
+ "- preparing \\x60" + toolName + "\\x60";
2678
+ assistantMessage._currentTools.push(prepText);
2679
+ assistantMessage.metadata.toolActivity.push(prepText);
2680
+ renderIfActiveConversation(true);
2681
+ }
2550
2682
  if (eventName === "tool:started") {
2551
2683
  const toolName = payload.tool || "tool";
2684
+ removeActiveActivityForTool(assistantMessage, toolName);
2552
2685
  const startedActivity = addActiveActivityFromToolStart(
2553
2686
  assistantMessage,
2554
2687
  payload,
@@ -2560,14 +2693,24 @@ var getWebUiClientScript = (markedSource2) => `
2560
2693
  });
2561
2694
  assistantMessage._currentText = "";
2562
2695
  }
2696
+ const tick = "\\x60";
2697
+ const prepPrefix = "- preparing " + tick + toolName + tick;
2698
+ const prepToolIdx = assistantMessage._currentTools.indexOf(prepPrefix);
2699
+ if (prepToolIdx >= 0) {
2700
+ assistantMessage._currentTools.splice(prepToolIdx, 1);
2701
+ }
2702
+ const prepMetaIdx = assistantMessage.metadata.toolActivity.indexOf(prepPrefix);
2703
+ if (prepMetaIdx >= 0) {
2704
+ assistantMessage.metadata.toolActivity.splice(prepMetaIdx, 1);
2705
+ }
2563
2706
  const detail =
2564
2707
  startedActivity && typeof startedActivity.detail === "string"
2565
2708
  ? startedActivity.detail.trim()
2566
2709
  : "";
2567
2710
  const toolText =
2568
- "- start \\x60" +
2711
+ "- start " + tick +
2569
2712
  toolName +
2570
- "\\x60" +
2713
+ tick +
2571
2714
  (detail ? " (" + detail + ")" : "");
2572
2715
  assistantMessage._currentTools.push(toolText);
2573
2716
  assistantMessage.metadata.toolActivity.push(toolText);
@@ -3290,26 +3433,57 @@ var getWebUiClientScript = (markedSource2) => `
3290
3433
  updateContextRing();
3291
3434
  }
3292
3435
  }
3436
+ if (eventName === "tool:generating") {
3437
+ const toolName = payload.tool || "tool";
3438
+ if (!Array.isArray(assistantMessage._activeActivities)) {
3439
+ assistantMessage._activeActivities = [];
3440
+ }
3441
+ assistantMessage._activeActivities.push({
3442
+ kind: "generating",
3443
+ tool: toolName,
3444
+ label: "Preparing " + toolName,
3445
+ });
3446
+ if (assistantMessage._currentText.length > 0) {
3447
+ assistantMessage._sections.push({ type: "text", content: assistantMessage._currentText });
3448
+ assistantMessage._currentText = "";
3449
+ }
3450
+ if (!assistantMessage.metadata) assistantMessage.metadata = {};
3451
+ if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
3452
+ const prepText = "- preparing \\x60" + toolName + "\\x60";
3453
+ assistantMessage._currentTools.push(prepText);
3454
+ assistantMessage.metadata.toolActivity.push(prepText);
3455
+ renderIfActiveConversation(true);
3456
+ }
3293
3457
  if (eventName === "tool:started") {
3294
3458
  const toolName = payload.tool || "tool";
3459
+ removeActiveActivityForTool(assistantMessage, toolName);
3295
3460
  const startedActivity = addActiveActivityFromToolStart(
3296
3461
  assistantMessage,
3297
3462
  payload,
3298
3463
  );
3299
- // If we have text accumulated, push it as a text section
3300
3464
  if (assistantMessage._currentText.length > 0) {
3301
3465
  assistantMessage._sections.push({ type: "text", content: assistantMessage._currentText });
3302
3466
  assistantMessage._currentText = "";
3303
3467
  }
3468
+ if (!assistantMessage.metadata) assistantMessage.metadata = {};
3469
+ if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
3470
+ const tick = "\\x60";
3471
+ const prepPrefix = "- preparing " + tick + toolName + tick;
3472
+ const prepToolIdx = assistantMessage._currentTools.indexOf(prepPrefix);
3473
+ if (prepToolIdx >= 0) {
3474
+ assistantMessage._currentTools.splice(prepToolIdx, 1);
3475
+ }
3476
+ const prepMetaIdx = assistantMessage.metadata.toolActivity.indexOf(prepPrefix);
3477
+ if (prepMetaIdx >= 0) {
3478
+ assistantMessage.metadata.toolActivity.splice(prepMetaIdx, 1);
3479
+ }
3304
3480
  const detail =
3305
3481
  startedActivity && typeof startedActivity.detail === "string"
3306
3482
  ? startedActivity.detail.trim()
3307
3483
  : "";
3308
3484
  const toolText =
3309
- "- start \\x60" + toolName + "\\x60" + (detail ? " (" + detail + ")" : "");
3485
+ "- start " + tick + toolName + tick + (detail ? " (" + detail + ")" : "");
3310
3486
  assistantMessage._currentTools.push(toolText);
3311
- if (!assistantMessage.metadata) assistantMessage.metadata = {};
3312
- if (!assistantMessage.metadata.toolActivity) assistantMessage.metadata.toolActivity = [];
3313
3487
  assistantMessage.metadata.toolActivity.push(toolText);
3314
3488
  renderIfActiveConversation(true);
3315
3489
  }
@@ -3763,45 +3937,20 @@ var getWebUiClientScript = (markedSource2) => `
3763
3937
  openLightbox(img.src);
3764
3938
  });
3765
3939
 
3766
- elements.messages.addEventListener("click", async (event) => {
3767
- const target = event.target;
3768
- if (!(target instanceof Element)) {
3769
- return;
3770
- }
3771
- const button = target.closest(".approval-action-btn");
3772
- if (!button) {
3773
- return;
3774
- }
3775
- const approvalId = button.getAttribute("data-approval-id") || "";
3776
- const decision = button.getAttribute("data-approval-decision") || "";
3777
- if (!approvalId || (decision !== "approve" && decision !== "deny")) {
3778
- return;
3779
- }
3780
- if (state.approvalRequestsInFlight[approvalId]) {
3781
- return;
3782
- }
3940
+ const submitApproval = async (approvalId, decision, opts) => {
3941
+ const wasStreaming = opts && opts.wasStreaming;
3783
3942
  state.approvalRequestsInFlight[approvalId] = true;
3784
- const wasStreaming = state.isStreaming;
3785
- if (!wasStreaming) {
3786
- setStreaming(true);
3787
- }
3788
3943
  updatePendingApproval(approvalId, (request) => ({
3789
3944
  ...request,
3790
3945
  state: "submitting",
3791
3946
  pendingDecision: decision,
3792
3947
  }));
3793
- renderMessages(state.activeMessages, state.isStreaming);
3794
3948
  try {
3795
3949
  await api("/api/approvals/" + encodeURIComponent(approvalId), {
3796
3950
  method: "POST",
3797
3951
  body: JSON.stringify({ approved: decision === "approve" }),
3798
3952
  });
3799
3953
  updatePendingApproval(approvalId, () => null);
3800
- renderMessages(state.activeMessages, state.isStreaming);
3801
- loadConversations();
3802
- if (!wasStreaming && state.activeConversationId) {
3803
- await streamConversationEvents(state.activeConversationId, { liveOnly: true });
3804
- }
3805
3954
  } catch (error) {
3806
3955
  const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
3807
3956
  if (isStale) {
@@ -3815,13 +3964,77 @@ var getWebUiClientScript = (markedSource2) => `
3815
3964
  _error: errMsg,
3816
3965
  }));
3817
3966
  }
3818
- renderMessages(state.activeMessages, state.isStreaming);
3819
3967
  } finally {
3968
+ delete state.approvalRequestsInFlight[approvalId];
3969
+ }
3970
+ };
3971
+
3972
+ elements.messages.addEventListener("click", async (event) => {
3973
+ const target = event.target;
3974
+ if (!(target instanceof Element)) {
3975
+ return;
3976
+ }
3977
+
3978
+ // Batch approve/deny all
3979
+ const batchBtn = target.closest(".approval-batch-btn");
3980
+ if (batchBtn) {
3981
+ const decision = batchBtn.getAttribute("data-approval-batch") || "";
3982
+ if (decision !== "approve" && decision !== "deny") return;
3983
+ const messages = state.activeMessages || [];
3984
+ const pending = [];
3985
+ for (const m of messages) {
3986
+ if (Array.isArray(m._pendingApprovals)) {
3987
+ for (const req of m._pendingApprovals) {
3988
+ if (req.approvalId && req.state !== "submitting" && !state.approvalRequestsInFlight[req.approvalId]) {
3989
+ pending.push(req.approvalId);
3990
+ }
3991
+ }
3992
+ }
3993
+ }
3994
+ if (pending.length === 0) return;
3995
+ const wasStreaming = state.isStreaming;
3996
+ if (!wasStreaming) setStreaming(true);
3997
+ renderMessages(state.activeMessages, state.isStreaming);
3998
+ await Promise.all(pending.map((aid) => submitApproval(aid, decision, { wasStreaming })));
3999
+ renderMessages(state.activeMessages, state.isStreaming);
4000
+ loadConversations();
4001
+ if (!wasStreaming && state.activeConversationId) {
4002
+ await streamConversationEvents(state.activeConversationId, { liveOnly: true });
4003
+ }
3820
4004
  if (!wasStreaming) {
3821
4005
  setStreaming(false);
3822
4006
  renderMessages(state.activeMessages, false);
3823
4007
  }
3824
- delete state.approvalRequestsInFlight[approvalId];
4008
+ return;
4009
+ }
4010
+
4011
+ // Individual approve/deny
4012
+ const button = target.closest(".approval-action-btn");
4013
+ if (!button) {
4014
+ return;
4015
+ }
4016
+ const approvalId = button.getAttribute("data-approval-id") || "";
4017
+ const decision = button.getAttribute("data-approval-decision") || "";
4018
+ if (!approvalId || (decision !== "approve" && decision !== "deny")) {
4019
+ return;
4020
+ }
4021
+ if (state.approvalRequestsInFlight[approvalId]) {
4022
+ return;
4023
+ }
4024
+ const wasStreaming = state.isStreaming;
4025
+ if (!wasStreaming) {
4026
+ setStreaming(true);
4027
+ }
4028
+ renderMessages(state.activeMessages, state.isStreaming);
4029
+ await submitApproval(approvalId, decision, { wasStreaming });
4030
+ renderMessages(state.activeMessages, state.isStreaming);
4031
+ loadConversations();
4032
+ if (!wasStreaming && state.activeConversationId) {
4033
+ await streamConversationEvents(state.activeConversationId, { liveOnly: true });
4034
+ }
4035
+ if (!wasStreaming) {
4036
+ setStreaming(false);
4037
+ renderMessages(state.activeMessages, false);
3825
4038
  }
3826
4039
  });
3827
4040
 
@@ -4378,7 +4591,7 @@ var getWebUiClientScript = (markedSource2) => `
4378
4591
  `;
4379
4592
 
4380
4593
  // src/web-ui-store.ts
4381
- import { createHash, randomUUID, timingSafeEqual } from "crypto";
4594
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
4382
4595
  import { mkdir, readFile, writeFile } from "fs/promises";
4383
4596
  import { basename, dirname, resolve } from "path";
4384
4597
  import { homedir } from "os";
@@ -4386,9 +4599,13 @@ var DEFAULT_OWNER = "local-owner";
4386
4599
  var SessionStore = class {
4387
4600
  sessions = /* @__PURE__ */ new Map();
4388
4601
  ttlMs;
4602
+ signingKey;
4389
4603
  constructor(ttlMs = 1e3 * 60 * 60 * 8) {
4390
4604
  this.ttlMs = ttlMs;
4391
4605
  }
4606
+ setSigningKey(key) {
4607
+ if (key) this.signingKey = key;
4608
+ }
4392
4609
  create(ownerId = DEFAULT_OWNER) {
4393
4610
  const now = Date.now();
4394
4611
  const session = {
@@ -4417,6 +4634,57 @@ var SessionStore = class {
4417
4634
  delete(sessionId) {
4418
4635
  this.sessions.delete(sessionId);
4419
4636
  }
4637
+ /**
4638
+ * Encode a session into a signed cookie value that survives serverless
4639
+ * cold starts. Format: `base64url(payload).signature`
4640
+ */
4641
+ signSession(session) {
4642
+ if (!this.signingKey) return void 0;
4643
+ const payload = Buffer.from(
4644
+ JSON.stringify({
4645
+ sid: session.sessionId,
4646
+ o: session.ownerId,
4647
+ csrf: session.csrfToken,
4648
+ exp: session.expiresAt
4649
+ })
4650
+ ).toString("base64url");
4651
+ const sig = createHmac("sha256", this.signingKey).update(payload).digest("base64url");
4652
+ return `${payload}.${sig}`;
4653
+ }
4654
+ /**
4655
+ * Restore a session from a signed cookie value. Returns the session
4656
+ * (also added to the in-memory store) or undefined if invalid/expired.
4657
+ */
4658
+ restoreFromSigned(cookieValue) {
4659
+ if (!this.signingKey) return void 0;
4660
+ const dotIdx = cookieValue.lastIndexOf(".");
4661
+ if (dotIdx <= 0) return void 0;
4662
+ const payload = cookieValue.slice(0, dotIdx);
4663
+ const sig = cookieValue.slice(dotIdx + 1);
4664
+ const expected = createHmac("sha256", this.signingKey).update(payload).digest("base64url");
4665
+ if (sig.length !== expected.length) return void 0;
4666
+ if (!timingSafeEqual(Buffer.from(sig, "utf8"), Buffer.from(expected, "utf8")))
4667
+ return void 0;
4668
+ try {
4669
+ const data = JSON.parse(
4670
+ Buffer.from(payload, "base64url").toString("utf8")
4671
+ );
4672
+ if (!data.sid || !data.o || !data.csrf || !data.exp) return void 0;
4673
+ if (Date.now() > data.exp) return void 0;
4674
+ const session = {
4675
+ sessionId: data.sid,
4676
+ ownerId: data.o,
4677
+ csrfToken: data.csrf,
4678
+ createdAt: data.exp - this.ttlMs,
4679
+ expiresAt: data.exp,
4680
+ lastSeenAt: Date.now()
4681
+ };
4682
+ this.sessions.set(session.sessionId, session);
4683
+ return session;
4684
+ } catch {
4685
+ return void 0;
4686
+ }
4687
+ }
4420
4688
  };
4421
4689
  var LoginRateLimiter = class {
4422
4690
  constructor(maxAttempts = 5, windowMs = 1e3 * 60 * 5, lockoutMs = 1e3 * 60 * 10) {
@@ -4633,9 +4901,12 @@ ${WEB_UI_STYLES}
4633
4901
 
4634
4902
  <div id="app" class="shell hidden">
4635
4903
  <aside class="sidebar">
4636
- <button id="new-chat" class="new-chat-btn">
4637
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
4638
- </button>
4904
+ <div class="sidebar-header">
4905
+ <span class="sidebar-agent-name">${agentName}</span>
4906
+ <button id="new-chat" class="new-chat-btn">
4907
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
4908
+ </button>
4909
+ </div>
4639
4910
  <div id="conversation-list" class="conversation-list"></div>
4640
4911
  <div class="sidebar-footer">
4641
4912
  <button id="logout" class="logout-btn">Log out</button>
@@ -6150,6 +6421,10 @@ Connect remote MCP servers and expose their tools to the agent:
6150
6421
  # Add remote MCP server
6151
6422
  poncho mcp add --url https://mcp.example.com/github --name github --auth-bearer-env GITHUB_TOKEN
6152
6423
 
6424
+ # Server with custom headers (e.g. Arcade)
6425
+ poncho mcp add --url https://mcp.arcade.dev --name arcade \\
6426
+ --auth-bearer-env ARCADE_API_KEY --header "Arcade-User-ID: user@example.com"
6427
+
6153
6428
  # List configured servers
6154
6429
  poncho mcp list
6155
6430
 
@@ -6231,14 +6506,22 @@ export default {
6231
6506
  url: "https://mcp.example.com/github",
6232
6507
  auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
6233
6508
  },
6509
+ // Custom headers for servers that require them (e.g. Arcade)
6510
+ // { name: "arcade", url: "https://mcp.arcade.dev", auth: { type: "bearer", tokenEnv: "ARCADE_API_KEY" }, headers: { "Arcade-User-ID": "user@example.com" } },
6234
6511
  ],
6235
6512
  // Tool access: true (available), false (disabled), 'approval' (requires human approval)
6236
6513
  tools: {
6514
+ list_directory: true,
6515
+ read_file: true,
6237
6516
  write_file: true, // gated by environment for writes
6517
+ delete_file: 'approval', // requires human approval
6518
+ delete_directory: 'approval', // requires human approval
6238
6519
  send_email: 'approval', // requires human approval
6239
6520
  byEnvironment: {
6240
6521
  production: {
6241
6522
  write_file: false,
6523
+ delete_file: false,
6524
+ delete_directory: false,
6242
6525
  },
6243
6526
  development: {
6244
6527
  send_email: true, // skip approval in dev
@@ -6321,6 +6604,7 @@ Connect your agent to email so users can interact by sending emails:
6321
6604
  RESEND_API_KEY=re_...
6322
6605
  RESEND_WEBHOOK_SECRET=whsec_...
6323
6606
  RESEND_FROM=Agent <agent@yourdomain.com>
6607
+ RESEND_REPLY_TO=support@yourdomain.com # optional
6324
6608
  \`\`\`
6325
6609
  5. Add to \`poncho.config.js\`:
6326
6610
  \`\`\`javascript
@@ -7157,71 +7441,72 @@ var createRequestHandler = async (options) => {
7157
7441
  if (event.type === "tool:approval:checkpoint") {
7158
7442
  const cpConv = await conversationStore.get(childConversationId);
7159
7443
  if (cpConv) {
7160
- const cpData = {
7161
- approvalId: event.approvalId,
7444
+ const allCpData = event.approvals.map((a) => ({
7445
+ approvalId: a.approvalId,
7162
7446
  runId: latestRunId,
7163
- tool: event.tool,
7164
- toolCallId: event.toolCallId,
7165
- input: event.input,
7447
+ tool: a.tool,
7448
+ toolCallId: a.toolCallId,
7449
+ input: a.input,
7166
7450
  checkpointMessages: [...historyMessages, ...event.checkpointMessages],
7167
7451
  baseMessageCount: 0,
7168
7452
  pendingToolCalls: event.pendingToolCalls
7169
- };
7170
- cpConv.pendingApprovals = [cpData];
7453
+ }));
7454
+ cpConv.pendingApprovals = allCpData;
7171
7455
  cpConv.updatedAt = Date.now();
7172
7456
  await conversationStore.update(cpConv);
7173
- const approved = await new Promise((resolve4) => {
7174
- pendingSubagentApprovals.set(event.approvalId, {
7175
- resolve: resolve4,
7176
- childHarness,
7177
- checkpoint: cpData,
7178
- childConversationId,
7179
- parentConversationId
7180
- });
7457
+ const decidedApprovals = await new Promise((resolve4) => {
7458
+ for (const cpData of allCpData) {
7459
+ pendingSubagentApprovals.set(cpData.approvalId, {
7460
+ resolve: resolve4,
7461
+ childHarness,
7462
+ checkpoint: cpData,
7463
+ childConversationId,
7464
+ parentConversationId
7465
+ });
7466
+ }
7181
7467
  });
7182
- let toolResults;
7183
- if (approved) {
7184
- const toolContext = {
7185
- runId: cpData.runId,
7186
- agentId: identity.id,
7187
- step: 0,
7188
- workingDir,
7189
- parameters: {},
7190
- conversationId: childConversationId
7191
- };
7192
- const execResults = await childHarness.executeTools(
7193
- [{ id: cpData.toolCallId, name: cpData.tool, input: cpData.input }],
7194
- toolContext
7195
- );
7196
- toolResults = execResults.map((r) => ({
7468
+ const checkpointRef = allCpData[0];
7469
+ const toolContext = {
7470
+ runId: checkpointRef.runId,
7471
+ agentId: identity.id,
7472
+ step: 0,
7473
+ workingDir,
7474
+ parameters: {},
7475
+ conversationId: childConversationId
7476
+ };
7477
+ const approvalToolCallIds = new Set(decidedApprovals.map((a) => a.toolCallId));
7478
+ const callsToExecute = [];
7479
+ const deniedResults = [];
7480
+ for (const a of decidedApprovals) {
7481
+ if (a.decision === "approved" && a.toolCallId) {
7482
+ callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
7483
+ const toolText = `- done \`${a.tool}\``;
7484
+ toolTimeline.push(toolText);
7485
+ currentTools.push(toolText);
7486
+ } else if (a.toolCallId) {
7487
+ deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
7488
+ const toolText = `- denied \`${a.tool}\``;
7489
+ toolTimeline.push(toolText);
7490
+ currentTools.push(toolText);
7491
+ }
7492
+ }
7493
+ const pendingToolCalls = checkpointRef.pendingToolCalls ?? [];
7494
+ for (const tc of pendingToolCalls) {
7495
+ if (!approvalToolCallIds.has(tc.id)) {
7496
+ callsToExecute.push(tc);
7497
+ }
7498
+ }
7499
+ let toolResults = [...deniedResults];
7500
+ if (callsToExecute.length > 0) {
7501
+ const execResults = await childHarness.executeTools(callsToExecute, toolContext);
7502
+ toolResults.push(...execResults.map((r) => ({
7197
7503
  callId: r.callId,
7198
7504
  toolName: r.tool,
7199
7505
  result: r.output,
7200
7506
  error: r.error
7201
- }));
7202
- const toolText = `- done \`${cpData.tool}\``;
7203
- toolTimeline.push(toolText);
7204
- currentTools.push(toolText);
7205
- } else {
7206
- toolResults = [{
7207
- callId: cpData.toolCallId,
7208
- toolName: cpData.tool,
7209
- error: "Tool execution denied by user"
7210
- }];
7211
- const toolText = `- denied \`${cpData.tool}\``;
7212
- toolTimeline.push(toolText);
7213
- currentTools.push(toolText);
7214
- }
7215
- broadcastEvent(
7216
- childConversationId,
7217
- approved ? { type: "tool:approval:granted", approvalId: event.approvalId } : { type: "tool:approval:denied", approvalId: event.approvalId }
7218
- );
7219
- const cpConv2 = await conversationStore.get(childConversationId);
7220
- if (cpConv2) {
7221
- cpConv2.pendingApprovals = [];
7222
- await conversationStore.update(cpConv2);
7507
+ })));
7223
7508
  }
7224
- const resumeMessages = [...cpData.checkpointMessages];
7509
+ const resumeMessages = [...checkpointRef.checkpointMessages];
7225
7510
  for await (const resumeEvent of childHarness.continueFromToolResult({
7226
7511
  messages: resumeMessages,
7227
7512
  toolResults,
@@ -7505,16 +7790,16 @@ var createRequestHandler = async (options) => {
7505
7790
  if (event.type === "tool:approval:checkpoint") {
7506
7791
  const conv = await conversationStore.get(conversationId);
7507
7792
  if (conv) {
7508
- conv.pendingApprovals = [{
7509
- approvalId: event.approvalId,
7793
+ conv.pendingApprovals = event.approvals.map((a) => ({
7794
+ approvalId: a.approvalId,
7510
7795
  runId: latestRunId,
7511
- tool: event.tool,
7512
- toolCallId: event.toolCallId,
7513
- input: event.input,
7796
+ tool: a.tool,
7797
+ toolCallId: a.toolCallId,
7798
+ input: a.input,
7514
7799
  checkpointMessages: [...fullCheckpointMessages, ...event.checkpointMessages],
7515
7800
  baseMessageCount: 0,
7516
7801
  pendingToolCalls: event.pendingToolCalls
7517
- }];
7802
+ }));
7518
7803
  conv.updatedAt = Date.now();
7519
7804
  await conversationStore.update(conv);
7520
7805
  }
@@ -7749,16 +8034,16 @@ var createRequestHandler = async (options) => {
7749
8034
  if (event.type === "tool:approval:checkpoint") {
7750
8035
  await updateConversation((c) => {
7751
8036
  c.messages = buildMessages();
7752
- c.pendingApprovals = [{
7753
- approvalId: event.approvalId,
8037
+ c.pendingApprovals = event.approvals.map((a) => ({
8038
+ approvalId: a.approvalId,
7754
8039
  runId: latestRunId,
7755
- tool: event.tool,
7756
- toolCallId: event.toolCallId,
7757
- input: event.input,
8040
+ tool: a.tool,
8041
+ toolCallId: a.toolCallId,
8042
+ input: a.input,
7758
8043
  checkpointMessages: event.checkpointMessages,
7759
8044
  baseMessageCount: historyMessages.length,
7760
8045
  pendingToolCalls: event.pendingToolCalls
7761
- }];
8046
+ }));
7762
8047
  });
7763
8048
  checkpointedRun = true;
7764
8049
  }
@@ -7827,9 +8112,9 @@ var createRequestHandler = async (options) => {
7827
8112
  waitUntil: waitUntilHook,
7828
8113
  ownerId: "local-owner"
7829
8114
  });
7830
- adapter.registerRoutes(messagingRouteRegistrar);
7831
8115
  try {
7832
8116
  await bridge.start();
8117
+ adapter.registerRoutes(messagingRouteRegistrar);
7833
8118
  messagingBridges.push(bridge);
7834
8119
  console.log(` Slack messaging enabled at /api/messaging/slack`);
7835
8120
  } catch (err) {
@@ -7842,6 +8127,7 @@ var createRequestHandler = async (options) => {
7842
8127
  apiKeyEnv: channelConfig.apiKeyEnv,
7843
8128
  webhookSecretEnv: channelConfig.webhookSecretEnv,
7844
8129
  fromEnv: channelConfig.fromEnv,
8130
+ replyToEnv: channelConfig.replyToEnv,
7845
8131
  allowedSenders: channelConfig.allowedSenders,
7846
8132
  mode: channelConfig.mode,
7847
8133
  allowedRecipients: channelConfig.allowedRecipients,
@@ -7853,9 +8139,9 @@ var createRequestHandler = async (options) => {
7853
8139
  waitUntil: waitUntilHook,
7854
8140
  ownerId: "local-owner"
7855
8141
  });
7856
- adapter.registerRoutes(messagingRouteRegistrar);
7857
8142
  try {
7858
8143
  await bridge.start();
8144
+ adapter.registerRoutes(messagingRouteRegistrar);
7859
8145
  messagingBridges.push(bridge);
7860
8146
  const adapterTools = adapter.getToolDefinitions?.() ?? [];
7861
8147
  if (adapterTools.length > 0) {
@@ -7877,6 +8163,9 @@ var createRequestHandler = async (options) => {
7877
8163
  const authToken = process.env[authTokenEnv] ?? "";
7878
8164
  const authRequired = config?.auth?.required ?? false;
7879
8165
  const requireAuth = authRequired && authToken.length > 0;
8166
+ if (requireAuth) {
8167
+ sessionStore.setSigningKey(authToken);
8168
+ }
7880
8169
  const webUiEnabled = config?.webUi !== false;
7881
8170
  const isProduction = resolveHarnessEnvironment() === "production";
7882
8171
  const secureCookies = isProduction;
@@ -7950,8 +8239,8 @@ var createRequestHandler = async (options) => {
7950
8239
  }
7951
8240
  }
7952
8241
  const cookies = parseCookies(request);
7953
- const sessionId = cookies.poncho_session;
7954
- const session = sessionId ? sessionStore.get(sessionId) : void 0;
8242
+ const cookieValue = cookies.poncho_session;
8243
+ const session = cookieValue ? sessionStore.get(cookieValue) ?? sessionStore.restoreFromSigned(cookieValue) : void 0;
7955
8244
  const ownerId = session?.ownerId ?? "local-owner";
7956
8245
  const requiresCsrfValidation = request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS";
7957
8246
  if (pathname === "/api/auth/session" && request.method === "GET") {
@@ -7999,7 +8288,8 @@ var createRequestHandler = async (options) => {
7999
8288
  }
8000
8289
  loginRateLimiter.registerSuccess(ip);
8001
8290
  const createdSession = sessionStore.create(ownerId);
8002
- setCookie(response, "poncho_session", createdSession.sessionId, {
8291
+ const signedValue = sessionStore.signSession(createdSession);
8292
+ setCookie(response, "poncho_session", signedValue ?? createdSession.sessionId, {
8003
8293
  httpOnly: true,
8004
8294
  secure: secureCookies,
8005
8295
  sameSite: "Lax",
@@ -8194,13 +8484,26 @@ data: ${JSON.stringify(data)}
8194
8484
  const approved = body.approved === true;
8195
8485
  const pendingSubagent = pendingSubagentApprovals.get(approvalId);
8196
8486
  if (pendingSubagent) {
8197
- pendingSubagentApprovals.delete(approvalId);
8487
+ pendingSubagent.checkpoint.decision = approved ? "approved" : "denied";
8488
+ broadcastEvent(
8489
+ pendingSubagent.childConversationId,
8490
+ approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
8491
+ );
8198
8492
  const childConv = await conversationStore.get(pendingSubagent.childConversationId);
8199
- if (childConv && Array.isArray(childConv.pendingApprovals)) {
8200
- childConv.pendingApprovals = childConv.pendingApprovals.filter((pa) => pa.approvalId !== approvalId);
8493
+ const allApprovals2 = childConv?.pendingApprovals ?? [];
8494
+ const allDecided2 = allApprovals2.length > 0 && allApprovals2.every((pa) => pa.decision != null);
8495
+ if (allDecided2) {
8496
+ for (const pa of allApprovals2) {
8497
+ pendingSubagentApprovals.delete(pa.approvalId);
8498
+ }
8499
+ if (childConv) {
8500
+ childConv.pendingApprovals = [];
8501
+ await conversationStore.update(childConv);
8502
+ }
8503
+ pendingSubagent.resolve(allApprovals2);
8504
+ } else if (childConv) {
8201
8505
  await conversationStore.update(childConv);
8202
8506
  }
8203
- pendingSubagent.resolve(approved);
8204
8507
  writeJson(response, 200, { ok: true, approvalId, approved });
8205
8508
  return;
8206
8509
  }
@@ -8233,47 +8536,63 @@ data: ${JSON.stringify(data)}
8233
8536
  });
8234
8537
  return;
8235
8538
  }
8236
- foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? []).filter((a) => a.approvalId !== approvalId);
8237
- await conversationStore.update(foundConversation);
8539
+ foundApproval.decision = approved ? "approved" : "denied";
8238
8540
  broadcastEvent(
8239
8541
  conversationId,
8240
8542
  approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
8241
8543
  );
8544
+ const allApprovals = foundConversation.pendingApprovals ?? [];
8545
+ const allDecided = allApprovals.length > 0 && allApprovals.every((a) => a.decision != null);
8546
+ if (!allDecided) {
8547
+ await conversationStore.update(foundConversation);
8548
+ writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
8549
+ return;
8550
+ }
8551
+ foundConversation.pendingApprovals = [];
8552
+ await conversationStore.update(foundConversation);
8553
+ const checkpointRef = allApprovals[0];
8242
8554
  void (async () => {
8243
- let toolResults;
8244
- if (approved) {
8245
- const toolContext = {
8246
- runId: foundApproval.runId,
8247
- agentId: identity.id,
8248
- step: 0,
8249
- workingDir,
8250
- parameters: {}
8251
- };
8252
- const execResults = await harness.executeTools(
8253
- [{ id: foundApproval.toolCallId, name: foundApproval.tool, input: foundApproval.input }],
8254
- toolContext
8255
- );
8256
- toolResults = execResults.map((r) => ({
8555
+ const toolContext = {
8556
+ runId: checkpointRef.runId,
8557
+ agentId: identity.id,
8558
+ step: 0,
8559
+ workingDir,
8560
+ parameters: {}
8561
+ };
8562
+ const approvalToolCallIds = new Set(allApprovals.map((a) => a.toolCallId));
8563
+ const callsToExecute = [];
8564
+ const deniedResults = [];
8565
+ for (const a of allApprovals) {
8566
+ if (a.decision === "approved" && a.toolCallId) {
8567
+ callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
8568
+ } else if (a.decision === "denied" && a.toolCallId) {
8569
+ deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
8570
+ }
8571
+ }
8572
+ const pendingToolCalls = checkpointRef.pendingToolCalls ?? [];
8573
+ for (const tc of pendingToolCalls) {
8574
+ if (!approvalToolCallIds.has(tc.id)) {
8575
+ callsToExecute.push(tc);
8576
+ }
8577
+ }
8578
+ let toolResults = [...deniedResults];
8579
+ if (callsToExecute.length > 0) {
8580
+ const execResults = await harness.executeTools(callsToExecute, toolContext);
8581
+ toolResults.push(...execResults.map((r) => ({
8257
8582
  callId: r.callId,
8258
8583
  toolName: r.tool,
8259
8584
  result: r.output,
8260
8585
  error: r.error
8261
- }));
8262
- } else {
8263
- toolResults = [{
8264
- callId: foundApproval.toolCallId,
8265
- toolName: foundApproval.tool,
8266
- error: "Tool execution denied by user"
8267
- }];
8586
+ })));
8268
8587
  }
8269
8588
  await resumeRunFromCheckpoint(
8270
8589
  conversationId,
8271
8590
  foundConversation,
8272
- foundApproval,
8591
+ checkpointRef,
8273
8592
  toolResults
8274
8593
  );
8275
8594
  })();
8276
- writeJson(response, 200, { ok: true, approvalId, approved });
8595
+ writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: true });
8277
8596
  return;
8278
8597
  }
8279
8598
  const conversationEventsMatch = pathname.match(
@@ -8757,16 +9076,16 @@ data: ${JSON.stringify(data)}
8757
9076
  metadata: toolTimeline.length > 0 || checkpointSections.length > 0 ? { toolActivity: [...toolTimeline], sections: checkpointSections.length > 0 ? checkpointSections : void 0 } : void 0
8758
9077
  }] : []
8759
9078
  ];
8760
- conversation.pendingApprovals = [{
8761
- approvalId: event.approvalId,
9079
+ conversation.pendingApprovals = event.approvals.map((a) => ({
9080
+ approvalId: a.approvalId,
8762
9081
  runId: latestRunId,
8763
- tool: event.tool,
8764
- toolCallId: event.toolCallId,
8765
- input: event.input,
9082
+ tool: a.tool,
9083
+ toolCallId: a.toolCallId,
9084
+ input: a.input,
8766
9085
  checkpointMessages: event.checkpointMessages,
8767
9086
  baseMessageCount: historyMessages.length,
8768
9087
  pendingToolCalls: event.pendingToolCalls
8769
- }];
9088
+ }));
8770
9089
  conversation.updatedAt = Date.now();
8771
9090
  await conversationStore.update(conversation);
8772
9091
  checkpointedRun = true;
@@ -9065,9 +9384,11 @@ data: ${JSON.stringify(data)}
9065
9384
  handler._cronJobs = cronJobs;
9066
9385
  handler._conversationStore = conversationStore;
9067
9386
  try {
9068
- const allConvs = await conversationStore.list();
9069
- for (const conv of allConvs) {
9070
- if (conv.subagentMeta?.status === "running") {
9387
+ const allSummaries = await conversationStore.listSummaries();
9388
+ const subagentSummaries = allSummaries.filter((s) => s.parentConversationId);
9389
+ for (const s of subagentSummaries) {
9390
+ const conv = await conversationStore.get(s.conversationId);
9391
+ if (conv?.subagentMeta?.status === "running") {
9071
9392
  conv.subagentMeta.status = "stopped";
9072
9393
  conv.subagentMeta.error = { code: "SERVER_RESTART", message: "Interrupted by server restart" };
9073
9394
  conv.updatedAt = Date.now();
@@ -9299,7 +9620,7 @@ var runInteractive = async (workingDir, params) => {
9299
9620
  await harness.initialize();
9300
9621
  const identity = await ensureAgentIdentity2(workingDir);
9301
9622
  try {
9302
- const { runInteractiveInk } = await import("./run-interactive-ink-B6JJ33SN.js");
9623
+ const { runInteractiveInk } = await import("./run-interactive-ink-4KVVPMR3.js");
9303
9624
  await runInteractiveInk({
9304
9625
  harness,
9305
9626
  params,
@@ -9680,6 +10001,15 @@ var mcpAdd = async (workingDir, options) => {
9680
10001
  if (!options.url.startsWith("http://") && !options.url.startsWith("https://")) {
9681
10002
  throw new Error("Invalid MCP URL. Expected http:// or https://.");
9682
10003
  }
10004
+ const parsedHeaders = options.headers && options.headers.length > 0 ? Object.fromEntries(
10005
+ options.headers.map((h) => {
10006
+ const idx = h.indexOf(":");
10007
+ if (idx < 1) {
10008
+ throw new Error(`Invalid header format "${h}". Expected "Name: value".`);
10009
+ }
10010
+ return [h.slice(0, idx).trim(), h.slice(idx + 1).trim()];
10011
+ })
10012
+ ) : void 0;
9683
10013
  const serverName = options.name ?? normalizeMcpName({ url: options.url });
9684
10014
  mcp.push({
9685
10015
  name: serverName,
@@ -9688,7 +10018,8 @@ var mcpAdd = async (workingDir, options) => {
9688
10018
  auth: options.authBearerEnv ? {
9689
10019
  type: "bearer",
9690
10020
  tokenEnv: options.authBearerEnv
9691
- } : void 0
10021
+ } : void 0,
10022
+ headers: parsedHeaders
9692
10023
  });
9693
10024
  await writeConfigFile(workingDir, { ...config, mcp });
9694
10025
  let envSeedMessage;
@@ -9732,8 +10063,10 @@ var mcpList = async (workingDir) => {
9732
10063
  process.stdout.write("Configured MCP servers:\n");
9733
10064
  for (const entry of mcp) {
9734
10065
  const auth = entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
10066
+ const headerKeys = entry.headers ? Object.keys(entry.headers) : [];
10067
+ const headerInfo = headerKeys.length > 0 ? `, headers=${headerKeys.join(",")}` : "";
9735
10068
  process.stdout.write(
9736
- `- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth})
10069
+ `- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}${headerInfo})
9737
10070
  `
9738
10071
  );
9739
10072
  }
@@ -9937,13 +10270,17 @@ var buildCli = () => {
9937
10270
  ).option("--env <name>", "env variable (repeatable)", (value, all) => {
9938
10271
  all.push(value);
9939
10272
  return all;
10273
+ }, []).option("--header <header>", "custom header as 'Name: value' (repeatable)", (value, all) => {
10274
+ all.push(value);
10275
+ return all;
9940
10276
  }, []).action(
9941
10277
  async (options) => {
9942
10278
  await mcpAdd(process.cwd(), {
9943
10279
  url: options.url,
9944
10280
  name: options.name,
9945
10281
  envVars: options.env,
9946
- authBearerEnv: options.authBearerEnv
10282
+ authBearerEnv: options.authBearerEnv,
10283
+ headers: options.header
9947
10284
  });
9948
10285
  }
9949
10286
  );