@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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +36 -0
- package/dist/{chunk-XIFWXRUB.js → chunk-A32BXZKP.js} +1577 -196
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-64QEOUXL.js → run-interactive-ink-SLWDVTDX.js} +1 -1
- package/package.json +4 -3
- package/src/api-docs.ts +674 -0
- package/src/index.ts +413 -40
- package/src/init-feature-context.ts +5 -0
- package/src/init-onboarding.ts +23 -2
- package/src/web-ui.ts +559 -155
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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 (
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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 === "/
|
|
1419
|
-
response
|
|
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 === "/
|
|
1428
|
-
response
|
|
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 (
|
|
1434
|
-
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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");
|
package/src/init-onboarding.ts
CHANGED
|
@@ -339,12 +339,33 @@ export const buildConfigFromOnboardingAnswers = (
|
|
|
339
339
|
};
|
|
340
340
|
maybeSet(telemetry, "otlp", answers["telemetry.otlp"]);
|
|
341
341
|
|
|
342
|
-
|
|
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
|
}
|