@poncho-ai/cli 0.12.0 → 0.14.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.
package/src/index.ts CHANGED
@@ -24,11 +24,20 @@ import {
24
24
  resolveStateConfig,
25
25
  type CronJobConfig,
26
26
  type PonchoConfig,
27
+ type Conversation,
27
28
  type ConversationStore,
28
29
  type UploadStore,
29
30
  } from "@poncho-ai/harness";
30
31
  import type { AgentEvent, FileInput, Message, RunInput } from "@poncho-ai/sdk";
31
32
  import { getTextContent } from "@poncho-ai/sdk";
33
+ import {
34
+ AgentBridge,
35
+ ResendAdapter,
36
+ SlackAdapter,
37
+ type AgentRunner,
38
+ type MessagingAdapter,
39
+ type RouteRegistrar,
40
+ } from "@poncho-ai/messaging";
32
41
  import Busboy from "busboy";
33
42
  import { Command } from "commander";
34
43
  import dotenv from "dotenv";
@@ -46,6 +55,7 @@ import {
46
55
  setCookie,
47
56
  verifyPassphrase,
48
57
  } from "./web-ui.js";
58
+ import { buildOpenApiSpec, renderApiDocsHtml } from "./api-docs.js";
49
59
  import { createInterface } from "node:readline/promises";
50
60
  import {
51
61
  runInitOnboarding,
@@ -367,7 +377,7 @@ cp .env.example .env
367
377
  poncho dev
368
378
  \`\`\`
369
379
 
370
- Open \`http://localhost:3000\` for the web UI.
380
+ Open \`http://localhost:3000\` for the web UI, or \`http://localhost:3000/api/docs\` for interactive API documentation.
371
381
 
372
382
  On your first interactive session, the agent introduces its configurable capabilities.
373
383
  While a response is streaming, you can stop it:
@@ -489,7 +499,7 @@ Core files:
489
499
 
490
500
  - \`AGENT.md\`: behavior, model selection, runtime guidance
491
501
  - \`poncho.config.js\`: runtime config (storage, auth, telemetry, MCP, tools)
492
- - \`.env\`: secrets and environment variables
502
+ - \`.env\`: secrets and environment variables (loaded before the harness starts, so \`process.env\` is available in skill scripts)
493
503
 
494
504
  Example \`poncho.config.js\`:
495
505
 
@@ -515,18 +525,20 @@ export default {
515
525
  auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
516
526
  },
517
527
  ],
528
+ // Tool access: true (available), false (disabled), 'approval' (requires human approval)
518
529
  tools: {
519
- defaults: {
520
- list_directory: true,
521
- read_file: true,
522
- write_file: true, // still gated by environment/policy
523
- },
530
+ write_file: true, // gated by environment for writes
531
+ send_email: 'approval', // requires human approval
524
532
  byEnvironment: {
525
533
  production: {
526
- read_file: false, // example override
534
+ write_file: false,
535
+ },
536
+ development: {
537
+ send_email: true, // skip approval in dev
527
538
  },
528
539
  },
529
540
  },
541
+ // webUi: false, // Disable built-in UI for API-only deployments
530
542
  };
531
543
  \`\`\`
532
544
 
@@ -567,6 +579,44 @@ cron:
567
579
  - Docker/Fly.io: scheduler runs automatically.
568
580
  - Trigger manually: \`curl http://localhost:3000/api/cron/daily-report\`
569
581
 
582
+ ## Messaging (Slack)
583
+
584
+ Connect your agent to Slack so it responds to @mentions:
585
+
586
+ 1. Create a Slack App at [api.slack.com/apps](https://api.slack.com/apps)
587
+ 2. Add Bot Token Scopes: \`app_mentions:read\`, \`chat:write\`, \`reactions:write\`
588
+ 3. Enable Event Subscriptions, set Request URL to \`https://<your-url>/api/messaging/slack\`, subscribe to \`app_mention\`
589
+ 4. Install to workspace, copy Bot Token and Signing Secret
590
+ 5. Set env vars:
591
+ \`\`\`
592
+ SLACK_BOT_TOKEN=xoxb-...
593
+ SLACK_SIGNING_SECRET=...
594
+ \`\`\`
595
+ 6. Add to \`poncho.config.js\`:
596
+ \`\`\`javascript
597
+ messaging: [{ platform: 'slack' }]
598
+ \`\`\`
599
+
600
+ ## Messaging (Email via Resend)
601
+
602
+ Connect your agent to email so users can interact by sending emails:
603
+
604
+ 1. Set up a domain and enable Inbound at [resend.com](https://resend.com)
605
+ 2. Create a webhook for \`email.received\` pointing to \`https://<your-url>/api/messaging/resend\`
606
+ 3. Install the Resend SDK: \`npm install resend\`
607
+ 4. Set env vars:
608
+ \`\`\`
609
+ RESEND_API_KEY=re_...
610
+ RESEND_WEBHOOK_SECRET=whsec_...
611
+ RESEND_FROM=Agent <agent@yourdomain.com>
612
+ \`\`\`
613
+ 5. Add to \`poncho.config.js\`:
614
+ \`\`\`javascript
615
+ messaging: [{ platform: 'resend' }]
616
+ \`\`\`
617
+
618
+ For full control over outbound emails, use **tool mode** (\`mode: 'tool'\`) — the agent gets a \`send_email\` tool instead of auto-replying. See the repo README for details.
619
+
570
620
  ## Deployment
571
621
 
572
622
  \`\`\`bash
@@ -1371,6 +1421,299 @@ export const createRequestHandler = async (options?: {
1371
1421
  workingDir,
1372
1422
  agentId: identity.id,
1373
1423
  });
1424
+ // ---------------------------------------------------------------------------
1425
+ // Messaging adapters (Slack, etc.) — routes bypass Poncho auth; each
1426
+ // adapter handles its own request verification (e.g. Slack signing secret).
1427
+ // ---------------------------------------------------------------------------
1428
+ const messagingRoutes = new Map<string, Map<string, (req: IncomingMessage, res: ServerResponse) => Promise<void>>>();
1429
+ const messagingRouteRegistrar: RouteRegistrar = (method, path, routeHandler) => {
1430
+ let byMethod = messagingRoutes.get(path);
1431
+ if (!byMethod) {
1432
+ byMethod = new Map();
1433
+ messagingRoutes.set(path, byMethod);
1434
+ }
1435
+ byMethod.set(method, routeHandler);
1436
+ };
1437
+
1438
+ const messagingRunner: AgentRunner = {
1439
+ async getOrCreateConversation(conversationId, meta) {
1440
+ const existing = await conversationStore.get(conversationId);
1441
+ if (existing) {
1442
+ return { messages: existing.messages };
1443
+ }
1444
+ const now = Date.now();
1445
+ const conversation = {
1446
+ conversationId,
1447
+ title: meta.title ?? `${meta.platform} thread`,
1448
+ messages: [] as Message[],
1449
+ ownerId: meta.ownerId,
1450
+ tenantId: null,
1451
+ createdAt: now,
1452
+ updatedAt: now,
1453
+ };
1454
+ await conversationStore.update(conversation);
1455
+ return { messages: [] };
1456
+ },
1457
+ async run(conversationId, input) {
1458
+ console.log("[messaging-runner] starting run for", conversationId, "task:", input.task.slice(0, 80));
1459
+
1460
+ const historyMessages = [...input.messages];
1461
+ const userContent = input.task;
1462
+
1463
+ // Read-modify-write helper: always fetches the latest version from
1464
+ // the store before writing, so concurrent writers (e.g. the approval
1465
+ // handler's persistConversationPendingApprovals) don't get clobbered.
1466
+ const updateConversation = async (
1467
+ patch: (conv: Conversation) => void,
1468
+ ): Promise<void> => {
1469
+ const fresh = await conversationStore.get(conversationId);
1470
+ if (!fresh) return;
1471
+ patch(fresh);
1472
+ fresh.updatedAt = Date.now();
1473
+ await conversationStore.update(fresh);
1474
+ };
1475
+
1476
+ // Persist user turn immediately so the web UI shows it while the agent runs.
1477
+ await updateConversation((c) => {
1478
+ c.messages = [...historyMessages, { role: "user" as const, content: userContent }];
1479
+ });
1480
+
1481
+ let latestRunId = "";
1482
+ let assistantResponse = "";
1483
+ const toolTimeline: string[] = [];
1484
+ const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
1485
+ let currentTools: string[] = [];
1486
+ let currentText = "";
1487
+
1488
+ const buildMessages = (): Message[] => {
1489
+ const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
1490
+ ...sections.map((s) => ({
1491
+ type: s.type,
1492
+ content: Array.isArray(s.content) ? [...s.content] : s.content,
1493
+ })),
1494
+ ];
1495
+ if (currentTools.length > 0) {
1496
+ draftSections.push({ type: "tools", content: [...currentTools] });
1497
+ }
1498
+ if (currentText.length > 0) {
1499
+ draftSections.push({ type: "text", content: currentText });
1500
+ }
1501
+ const hasDraftContent =
1502
+ assistantResponse.length > 0 || toolTimeline.length > 0 || draftSections.length > 0;
1503
+ if (!hasDraftContent) {
1504
+ return [...historyMessages, { role: "user" as const, content: userContent }];
1505
+ }
1506
+ return [
1507
+ ...historyMessages,
1508
+ { role: "user" as const, content: userContent },
1509
+ {
1510
+ role: "assistant" as const,
1511
+ content: assistantResponse,
1512
+ metadata:
1513
+ toolTimeline.length > 0 || draftSections.length > 0
1514
+ ? ({
1515
+ toolActivity: [...toolTimeline],
1516
+ sections: draftSections.length > 0 ? draftSections : undefined,
1517
+ } as Message["metadata"])
1518
+ : undefined,
1519
+ },
1520
+ ];
1521
+ };
1522
+
1523
+ const persistDraftAssistantTurn = async (): Promise<void> => {
1524
+ if (assistantResponse.length === 0 && toolTimeline.length === 0) return;
1525
+ await updateConversation((c) => {
1526
+ c.messages = buildMessages();
1527
+ });
1528
+ };
1529
+
1530
+ const runInput = {
1531
+ task: input.task,
1532
+ conversationId,
1533
+ messages: input.messages,
1534
+ files: input.files,
1535
+ parameters: input.metadata ? {
1536
+ __messaging_platform: input.metadata.platform,
1537
+ __messaging_sender_id: input.metadata.sender.id,
1538
+ __messaging_sender_name: input.metadata.sender.name ?? "",
1539
+ __messaging_thread_id: input.metadata.threadId,
1540
+ } : undefined,
1541
+ };
1542
+
1543
+ try {
1544
+ for await (const event of harness.runWithTelemetry(runInput)) {
1545
+ if (event.type === "run:started") {
1546
+ latestRunId = event.runId;
1547
+ runOwners.set(event.runId, "local-owner");
1548
+ runConversations.set(event.runId, conversationId);
1549
+ }
1550
+ if (event.type === "model:chunk") {
1551
+ if (currentTools.length > 0) {
1552
+ sections.push({ type: "tools", content: currentTools });
1553
+ currentTools = [];
1554
+ }
1555
+ assistantResponse += event.content;
1556
+ currentText += event.content;
1557
+ }
1558
+ if (event.type === "tool:started") {
1559
+ if (currentText.length > 0) {
1560
+ sections.push({ type: "text", content: currentText });
1561
+ currentText = "";
1562
+ }
1563
+ const toolText = `- start \`${event.tool}\``;
1564
+ toolTimeline.push(toolText);
1565
+ currentTools.push(toolText);
1566
+ }
1567
+ if (event.type === "tool:completed") {
1568
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
1569
+ toolTimeline.push(toolText);
1570
+ currentTools.push(toolText);
1571
+ }
1572
+ if (event.type === "tool:error") {
1573
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
1574
+ toolTimeline.push(toolText);
1575
+ currentTools.push(toolText);
1576
+ }
1577
+ if (event.type === "step:completed") {
1578
+ await persistDraftAssistantTurn();
1579
+ }
1580
+ if (event.type === "tool:approval:required") {
1581
+ const toolText = `- approval required \`${event.tool}\``;
1582
+ toolTimeline.push(toolText);
1583
+ currentTools.push(toolText);
1584
+ await persistDraftAssistantTurn();
1585
+ await persistConversationPendingApprovals(conversationId);
1586
+ }
1587
+ if (event.type === "tool:approval:granted") {
1588
+ const toolText = `- approval granted (${event.approvalId})`;
1589
+ toolTimeline.push(toolText);
1590
+ currentTools.push(toolText);
1591
+ await persistDraftAssistantTurn();
1592
+ }
1593
+ if (event.type === "tool:approval:denied") {
1594
+ const toolText = `- approval denied (${event.approvalId})`;
1595
+ toolTimeline.push(toolText);
1596
+ currentTools.push(toolText);
1597
+ await persistDraftAssistantTurn();
1598
+ }
1599
+ if (
1600
+ event.type === "run:completed" &&
1601
+ assistantResponse.length === 0 &&
1602
+ event.result.response
1603
+ ) {
1604
+ assistantResponse = event.result.response;
1605
+ }
1606
+ if (event.type === "run:error") {
1607
+ assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
1608
+ }
1609
+ broadcastEvent(conversationId, event);
1610
+ }
1611
+ } catch (err) {
1612
+ console.error("[messaging-runner] run failed:", err instanceof Error ? err.message : err);
1613
+ assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
1614
+ }
1615
+
1616
+ // Finalize sections (clear after pushing so buildMessages doesn't re-add)
1617
+ if (currentTools.length > 0) {
1618
+ sections.push({ type: "tools", content: currentTools });
1619
+ currentTools = [];
1620
+ }
1621
+ if (currentText.length > 0) {
1622
+ sections.push({ type: "text", content: currentText });
1623
+ currentText = "";
1624
+ }
1625
+
1626
+ await updateConversation((c) => {
1627
+ c.messages = buildMessages();
1628
+ c.runtimeRunId = latestRunId || c.runtimeRunId;
1629
+ c.pendingApprovals = [];
1630
+ });
1631
+ finishConversationStream(conversationId);
1632
+ await persistConversationPendingApprovals(conversationId);
1633
+ if (latestRunId) {
1634
+ runOwners.delete(latestRunId);
1635
+ runConversations.delete(latestRunId);
1636
+ }
1637
+
1638
+ console.log("[messaging-runner] run complete, response length:", assistantResponse.length);
1639
+ const response = assistantResponse;
1640
+
1641
+ return { response };
1642
+ },
1643
+ };
1644
+
1645
+ const messagingBridges: AgentBridge[] = [];
1646
+ if (config?.messaging && config.messaging.length > 0) {
1647
+ let waitUntilHook: ((promise: Promise<unknown>) => void) | undefined;
1648
+ if (process.env.VERCEL) {
1649
+ try {
1650
+ // Dynamic require via variable so TypeScript doesn't attempt static
1651
+ // resolution of @vercel/functions (only present in Vercel deployments).
1652
+ const modName = "@vercel/functions";
1653
+ const mod = await import(/* webpackIgnore: true */ modName);
1654
+ waitUntilHook = mod.waitUntil;
1655
+ } catch {
1656
+ // @vercel/functions not installed -- fall through to no-op.
1657
+ }
1658
+ }
1659
+
1660
+ for (const channelConfig of config.messaging) {
1661
+ if (channelConfig.platform === "slack") {
1662
+ const adapter = new SlackAdapter({
1663
+ botTokenEnv: channelConfig.botTokenEnv,
1664
+ signingSecretEnv: channelConfig.signingSecretEnv,
1665
+ });
1666
+ const bridge = new AgentBridge({
1667
+ adapter,
1668
+ runner: messagingRunner,
1669
+ waitUntil: waitUntilHook,
1670
+ ownerId: "local-owner",
1671
+ });
1672
+ adapter.registerRoutes(messagingRouteRegistrar);
1673
+ try {
1674
+ await bridge.start();
1675
+ messagingBridges.push(bridge);
1676
+ console.log(` Slack messaging enabled at /api/messaging/slack`);
1677
+ } catch (err) {
1678
+ console.warn(
1679
+ ` Slack messaging disabled: ${err instanceof Error ? err.message : String(err)}`,
1680
+ );
1681
+ }
1682
+ } else if (channelConfig.platform === "resend") {
1683
+ const adapter = new ResendAdapter({
1684
+ apiKeyEnv: channelConfig.apiKeyEnv,
1685
+ webhookSecretEnv: channelConfig.webhookSecretEnv,
1686
+ fromEnv: channelConfig.fromEnv,
1687
+ allowedSenders: channelConfig.allowedSenders,
1688
+ mode: channelConfig.mode,
1689
+ allowedRecipients: channelConfig.allowedRecipients,
1690
+ maxSendsPerRun: channelConfig.maxSendsPerRun,
1691
+ });
1692
+ const bridge = new AgentBridge({
1693
+ adapter,
1694
+ runner: messagingRunner,
1695
+ waitUntil: waitUntilHook,
1696
+ ownerId: "local-owner",
1697
+ });
1698
+ adapter.registerRoutes(messagingRouteRegistrar);
1699
+ try {
1700
+ await bridge.start();
1701
+ messagingBridges.push(bridge);
1702
+ const adapterTools = adapter.getToolDefinitions?.() ?? [];
1703
+ if (adapterTools.length > 0) {
1704
+ harness.registerTools(adapterTools);
1705
+ }
1706
+ const modeLabel = channelConfig.mode === "tool" ? "tool" : "auto-reply";
1707
+ console.log(` Resend email messaging enabled at /api/messaging/resend (mode: ${modeLabel})`);
1708
+ } catch (err) {
1709
+ console.warn(
1710
+ ` Resend email messaging disabled: ${err instanceof Error ? err.message : String(err)}`,
1711
+ );
1712
+ }
1713
+ }
1714
+ }
1715
+ }
1716
+
1374
1717
  const sessionStore = new SessionStore();
1375
1718
  const loginRateLimiter = new LoginRateLimiter();
1376
1719
 
@@ -1379,6 +1722,7 @@ export const createRequestHandler = async (options?: {
1379
1722
  const authRequired = config?.auth?.required ?? false;
1380
1723
  const requireAuth = authRequired && authToken.length > 0;
1381
1724
 
1725
+ const webUiEnabled = config?.webUi !== false;
1382
1726
  const isProduction = resolveHarnessEnvironment() === "production";
1383
1727
  const secureCookies = isProduction;
1384
1728
 
@@ -1404,42 +1748,64 @@ export const createRequestHandler = async (options?: {
1404
1748
  }
1405
1749
  const [pathname] = request.url.split("?");
1406
1750
 
1407
- if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
1408
- writeHtml(response, 200, renderWebUiHtml({ agentName }));
1409
- return;
1410
- }
1751
+ if (webUiEnabled) {
1752
+ if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
1753
+ writeHtml(response, 200, renderWebUiHtml({ agentName }));
1754
+ return;
1755
+ }
1411
1756
 
1412
- if (pathname === "/manifest.json" && request.method === "GET") {
1413
- response.writeHead(200, { "Content-Type": "application/manifest+json" });
1414
- response.end(renderManifest({ agentName }));
1415
- return;
1757
+ if (pathname === "/manifest.json" && request.method === "GET") {
1758
+ response.writeHead(200, { "Content-Type": "application/manifest+json" });
1759
+ response.end(renderManifest({ agentName }));
1760
+ return;
1761
+ }
1762
+
1763
+ if (pathname === "/sw.js" && request.method === "GET") {
1764
+ response.writeHead(200, {
1765
+ "Content-Type": "application/javascript",
1766
+ "Service-Worker-Allowed": "/",
1767
+ });
1768
+ response.end(renderServiceWorker());
1769
+ return;
1770
+ }
1771
+
1772
+ if (pathname === "/icon.svg" && request.method === "GET") {
1773
+ response.writeHead(200, { "Content-Type": "image/svg+xml" });
1774
+ response.end(renderIconSvg({ agentName }));
1775
+ return;
1776
+ }
1777
+
1778
+ if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
1779
+ response.writeHead(302, { Location: "/icon.svg" });
1780
+ response.end();
1781
+ return;
1782
+ }
1416
1783
  }
1417
1784
 
1418
- if (pathname === "/sw.js" && request.method === "GET") {
1419
- response.writeHead(200, {
1420
- "Content-Type": "application/javascript",
1421
- "Service-Worker-Allowed": "/",
1422
- });
1423
- response.end(renderServiceWorker());
1785
+ if (pathname === "/health" && request.method === "GET") {
1786
+ writeJson(response, 200, { status: "ok" });
1424
1787
  return;
1425
1788
  }
1426
1789
 
1427
- if (pathname === "/icon.svg" && request.method === "GET") {
1428
- response.writeHead(200, { "Content-Type": "image/svg+xml" });
1429
- response.end(renderIconSvg({ agentName }));
1790
+ if (pathname === "/api/openapi.json" && request.method === "GET") {
1791
+ writeJson(response, 200, buildOpenApiSpec({ agentName }));
1430
1792
  return;
1431
1793
  }
1432
1794
 
1433
- if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
1434
- // Redirect to SVG — browsers that support PWA icons will use the SVG
1435
- response.writeHead(302, { Location: "/icon.svg" });
1436
- response.end();
1795
+ if (pathname === "/api/docs" && request.method === "GET") {
1796
+ writeHtml(response, 200, renderApiDocsHtml("/api/openapi.json"));
1437
1797
  return;
1438
1798
  }
1439
1799
 
1440
- if (pathname === "/health" && request.method === "GET") {
1441
- writeJson(response, 200, { status: "ok" });
1442
- return;
1800
+ // Messaging adapter routes bypass Poncho auth (they verify requests
1801
+ // using platform-specific mechanisms, e.g. Slack signing secret).
1802
+ const messagingByMethod = messagingRoutes.get(pathname ?? "");
1803
+ if (messagingByMethod) {
1804
+ const routeHandler = messagingByMethod.get(request.method ?? "");
1805
+ if (routeHandler) {
1806
+ await routeHandler(request, response);
1807
+ return;
1808
+ }
1443
1809
  }
1444
1810
 
1445
1811
  const cookies = parseCookies(request);
@@ -1649,18 +2015,19 @@ export const createRequestHandler = async (options?: {
1649
2015
  });
1650
2016
  const stream = conversationEventStreams.get(conversationId);
1651
2017
  if (!stream) {
1652
- // No active run — close immediately
1653
2018
  response.write("event: stream:end\ndata: {}\n\n");
1654
2019
  response.end();
1655
2020
  return;
1656
2021
  }
1657
- // Replay buffered events
1658
- for (const bufferedEvent of stream.buffer) {
1659
- try {
1660
- response.write(formatSseEvent(bufferedEvent));
1661
- } catch {
1662
- response.end();
1663
- return;
2022
+ const liveOnly = (request.url ?? "").includes("live_only=true");
2023
+ if (!liveOnly) {
2024
+ for (const bufferedEvent of stream.buffer) {
2025
+ try {
2026
+ response.write(formatSseEvent(bufferedEvent));
2027
+ } catch {
2028
+ response.end();
2029
+ return;
2030
+ }
1664
2031
  }
1665
2032
  }
1666
2033
  if (stream.finished) {
@@ -1711,11 +2078,14 @@ export const createRequestHandler = async (options?: {
1711
2078
  for (const approval of livePending) {
1712
2079
  mergedPendingById.set(approval.approvalId, approval);
1713
2080
  }
2081
+ const activeStream = conversationEventStreams.get(conversationId);
2082
+ const hasActiveRun = !!activeStream && !activeStream.finished;
1714
2083
  writeJson(response, 200, {
1715
2084
  conversation: {
1716
2085
  ...conversation,
1717
2086
  pendingApprovals: Array.from(mergedPendingById.values()),
1718
2087
  },
2088
+ hasActiveRun,
1719
2089
  });
1720
2090
  return;
1721
2091
  }
@@ -1994,6 +2364,7 @@ export const createRequestHandler = async (options?: {
1994
2364
 
1995
2365
  for await (const event of harness.runWithTelemetry({
1996
2366
  task: messageText,
2367
+ conversationId,
1997
2368
  parameters: {
1998
2369
  ...(bodyParameters ?? {}),
1999
2370
  __conversationRecallCorpus: recallCorpus,
@@ -2273,6 +2644,7 @@ export const createRequestHandler = async (options?: {
2273
2644
 
2274
2645
  for await (const event of harness.runWithTelemetry({
2275
2646
  task: cronJob.task,
2647
+ conversationId: conversation.conversationId,
2276
2648
  parameters: { __activeConversationId: conversation.conversationId },
2277
2649
  messages: historyMessages,
2278
2650
  abortSignal: abortController.signal,
@@ -2459,6 +2831,7 @@ export const startDevServer = async (
2459
2831
  let currentText = "";
2460
2832
  for await (const event of harness.runWithTelemetry({
2461
2833
  task: config.task,
2834
+ conversationId: conversation.conversationId,
2462
2835
  parameters: { __activeConversationId: conversation.conversationId },
2463
2836
  messages: [],
2464
2837
  })) {
@@ -29,11 +29,15 @@ const summarizeConfig = (config: PonchoConfig | undefined): string[] => {
29
29
  const memoryEnabled = config?.storage?.memory?.enabled ?? config?.memory?.enabled ?? false;
30
30
  const authRequired = config?.auth?.required ?? false;
31
31
  const telemetryEnabled = config?.telemetry?.enabled ?? true;
32
+ const messagingPlatforms = (config?.messaging ?? []).map((m) => m.platform);
32
33
  return [
33
34
  `storage: ${provider}`,
34
35
  `memory tools: ${memoryEnabled ? "enabled" : "disabled"}`,
35
36
  `auth: ${authRequired ? "required" : "not required"}`,
36
37
  `telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`,
38
+ ...(messagingPlatforms.length > 0
39
+ ? [`messaging: ${messagingPlatforms.join(", ")}`]
40
+ : []),
37
41
  ];
38
42
  };
39
43
 
@@ -127,6 +131,7 @@ export const consumeFirstRunIntro = async (
127
131
  "- **Turn on telemetry**: Track usage with OpenTelemetry/OTLP",
128
132
  "- **Add MCP servers**: Connect external tool servers",
129
133
  "- **Schedule cron jobs**: Set up recurring tasks in AGENT.md frontmatter",
134
+ "- **Connect to Slack**: Set up messaging so users can @mention this agent in Slack",
130
135
  "",
131
136
  "Just let me know what you'd like to work on!\n",
132
137
  ].join("\n");
@@ -339,12 +339,33 @@ export const buildConfigFromOnboardingAnswers = (
339
339
  };
340
340
  maybeSet(telemetry, "otlp", answers["telemetry.otlp"]);
341
341
 
342
- return {
342
+ const messagingPlatform = String(answers["messaging.platform"] ?? "none");
343
+
344
+ const config: PonchoConfig = {
343
345
  mcp: [],
344
346
  auth,
345
347
  storage,
346
348
  telemetry,
347
349
  };
350
+
351
+ if (messagingPlatform !== "none") {
352
+ const channelConfig: NonNullable<PonchoConfig["messaging"]>[number] = {
353
+ platform: messagingPlatform as "slack" | "resend",
354
+ };
355
+ if (messagingPlatform === "resend") {
356
+ const mode = String(answers["messaging.resend.mode"] ?? "auto-reply");
357
+ if (mode === "tool") {
358
+ channelConfig.mode = "tool";
359
+ }
360
+ const recipientsRaw = String(answers["messaging.resend.allowedRecipients"] ?? "");
361
+ if (recipientsRaw.trim().length > 0) {
362
+ channelConfig.allowedRecipients = recipientsRaw.split(",").map((s) => s.trim()).filter(Boolean);
363
+ }
364
+ }
365
+ config.messaging = [channelConfig];
366
+ }
367
+
368
+ return config;
348
369
  };
349
370
 
350
371
  export const isDefaultOnboardingConfig = (
@@ -354,7 +375,7 @@ export const isDefaultOnboardingConfig = (
354
375
  return true;
355
376
  }
356
377
  const topLevelKeys = Object.keys(config);
357
- const allowedTopLevel = new Set(["mcp", "auth", "storage", "telemetry"]);
378
+ const allowedTopLevel = new Set(["mcp", "auth", "storage", "telemetry", "messaging"]);
358
379
  if (topLevelKeys.some((key) => !allowedTopLevel.has(key))) {
359
380
  return false;
360
381
  }