@poncho-ai/cli 0.5.1 → 0.6.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.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/CHANGELOG.md +11 -0
  3. package/dist/chunk-2QHGXOCW.js +4190 -0
  4. package/dist/chunk-3BHIZKV4.js +4433 -0
  5. package/dist/chunk-5KGEWTN2.js +4451 -0
  6. package/dist/chunk-6RKPJ6M3.js +4450 -0
  7. package/dist/chunk-76TYYF2B.js +4596 -0
  8. package/dist/chunk-AKTDJV35.js +4445 -0
  9. package/dist/chunk-ANMTOB6T.js +4655 -0
  10. package/dist/chunk-AWYXKGGD.js +4429 -0
  11. package/dist/chunk-BA5FFDMU.js +4536 -0
  12. package/dist/chunk-BOYZXUX6.js +4193 -0
  13. package/dist/chunk-DRUAHCF2.js +4192 -0
  14. package/dist/chunk-ESXYQFEA.js +4420 -0
  15. package/dist/chunk-FOUP464Z.js +4450 -0
  16. package/dist/chunk-HQK5KX73.js +4443 -0
  17. package/dist/chunk-JEI3ECVY.js +4706 -0
  18. package/dist/chunk-KQJYYNEZ.js +4924 -0
  19. package/dist/chunk-L7ZTJ2D4.js +4566 -0
  20. package/dist/chunk-LLP2S2BI.js +4620 -0
  21. package/dist/chunk-PAJHE4RF.js +4193 -0
  22. package/dist/chunk-PMM3UA6S.js +4706 -0
  23. package/dist/chunk-PYSTVO25.js +4793 -0
  24. package/dist/chunk-R5Z54SY3.js +4500 -0
  25. package/dist/chunk-R7YJ44RM.js +4449 -0
  26. package/dist/chunk-SBTOGSNJ.js +4216 -0
  27. package/dist/chunk-SLMKOFSJ.js +4771 -0
  28. package/dist/chunk-TK6BUSBH.js +4467 -0
  29. package/dist/chunk-TZTINGAP.js +4702 -0
  30. package/dist/chunk-UBCCPTBC.js +4678 -0
  31. package/dist/chunk-WJVYDTUE.js +4218 -0
  32. package/dist/chunk-XFUFWOS4.js +4192 -0
  33. package/dist/chunk-XI6V7BKE.js +4761 -0
  34. package/dist/chunk-YIVONFU2.js +4706 -0
  35. package/dist/cli.js +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/run-interactive-ink-3F3GQQH2.js +494 -0
  38. package/dist/run-interactive-ink-47PEYK57.js +494 -0
  39. package/dist/run-interactive-ink-4W7B6MZM.js +494 -0
  40. package/dist/run-interactive-ink-6YSU2AFP.js +494 -0
  41. package/dist/run-interactive-ink-A7HDSDJV.js +494 -0
  42. package/dist/run-interactive-ink-BHF2CAVH.js +494 -0
  43. package/dist/run-interactive-ink-BOKRENUR.js +494 -0
  44. package/dist/run-interactive-ink-BUF3RYIK.js +494 -0
  45. package/dist/run-interactive-ink-C4H4JNBP.js +494 -0
  46. package/dist/run-interactive-ink-CEWPPPGQ.js +494 -0
  47. package/dist/run-interactive-ink-CWMASTAZ.js +494 -0
  48. package/dist/run-interactive-ink-EB5AGYLN.js +494 -0
  49. package/dist/run-interactive-ink-FWANCYNR.js +494 -0
  50. package/dist/run-interactive-ink-GDNHWAKW.js +494 -0
  51. package/dist/run-interactive-ink-HLJ2YQ5N.js +494 -0
  52. package/dist/run-interactive-ink-J2S3V2LB.js +494 -0
  53. package/dist/run-interactive-ink-JDTNFRN2.js +494 -0
  54. package/dist/run-interactive-ink-JJCOMTJ2.js +494 -0
  55. package/dist/run-interactive-ink-JOHZ23OL.js +494 -0
  56. package/dist/run-interactive-ink-KMS44EYF.js +494 -0
  57. package/dist/run-interactive-ink-L3EB5GYB.js +494 -0
  58. package/dist/run-interactive-ink-N7MMFB4H.js +494 -0
  59. package/dist/run-interactive-ink-PK4NP4JQ.js +494 -0
  60. package/dist/run-interactive-ink-QFK6ZQFM.js +494 -0
  61. package/dist/run-interactive-ink-RX2IX3CH.js +494 -0
  62. package/dist/run-interactive-ink-SXK3AQJJ.js +494 -0
  63. package/dist/run-interactive-ink-THTNJZHB.js +494 -0
  64. package/dist/run-interactive-ink-VJMRO6OF.js +494 -0
  65. package/dist/run-interactive-ink-ZAVTGCKW.js +494 -0
  66. package/dist/run-interactive-ink-ZBCX4NUD.js +494 -0
  67. package/dist/run-interactive-ink-ZRQIK4AP.js +494 -0
  68. package/dist/run-interactive-ink-ZUS5DFPZ.js +494 -0
  69. package/package.json +2 -2
  70. package/src/index.ts +337 -71
  71. package/src/web-ui.ts +637 -115
package/src/index.ts CHANGED
@@ -348,7 +348,7 @@ poncho mcp add --url https://mcp.example.com/github --name github --auth-bearer-
348
348
  # List configured servers
349
349
  poncho mcp list
350
350
 
351
- # Discover and select MCP tools into config allowlist
351
+ # Discover MCP tools and print frontmatter intent snippets
352
352
  poncho mcp tools list github
353
353
  poncho mcp tools select github
354
354
 
@@ -358,32 +358,32 @@ poncho mcp remove github
358
358
 
359
359
  Set required secrets in \`.env\` (for example, \`GITHUB_TOKEN=...\`).
360
360
 
361
- ## Tool Intent in Frontmatter
361
+ ## Tool Intent and Approvals in Frontmatter
362
362
 
363
363
  Declare tool intent directly in \`AGENT.md\` and \`SKILL.md\` frontmatter:
364
364
 
365
365
  \`\`\`yaml
366
- tools:
367
- mcp:
368
- - github/list_issues
369
- - github/*
370
- scripts:
371
- - starter/scripts/*
366
+ allowed-tools:
367
+ - mcp:github/list_issues
368
+ - mcp:github/*
369
+ approval-required:
370
+ - mcp:github/create_issue
371
+ - ./scripts/deploy.ts
372
372
  \`\`\`
373
373
 
374
374
  How it works:
375
375
 
376
376
  - \`AGENT.md\` provides fallback MCP intent when no skill is active.
377
377
  - \`SKILL.md\` intent applies when you activate that skill (\`activate_skill\`).
378
- - Skill scripts are accessible by default from each skill's \`scripts/\` directory.
379
- - \`AGENT.md\` \`tools.scripts\` can still be used to narrow script access when active skills do not set script intent.
380
- - Active skills are unioned, then filtered by policy in \`poncho.config.js\`.
378
+ - Scripts in a sibling \`scripts/\` directory are available by convention.
379
+ - For non-standard script folders (for example \`tools/\`), add explicit relative entries in \`allowed-tools\`.
380
+ - Use \`approval-required\` to require human approval for specific MCP calls or script files.
381
381
  - Deactivating a skill (\`deactivate_skill\`) removes its MCP tools from runtime registration.
382
382
 
383
383
  Pattern format is strict slash-only:
384
384
 
385
385
  - MCP: \`server/tool\`, \`server/*\`
386
- - Scripts: \`skill/scripts/file.ts\`, \`skill/scripts/*\`
386
+ - Scripts: relative paths such as \`./scripts/file.ts\`, \`./scripts/*\`, \`./tools/deploy.ts\`
387
387
 
388
388
  ## Configuration
389
389
 
@@ -415,16 +415,8 @@ export default {
415
415
  name: "github",
416
416
  url: "https://mcp.example.com/github",
417
417
  auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
418
- tools: {
419
- mode: "allowlist",
420
- include: ["github/list_issues", "github/get_issue"],
421
- },
422
418
  },
423
419
  ],
424
- scripts: {
425
- mode: "allowlist",
426
- include: ["starter/scripts/*"],
427
- },
428
420
  tools: {
429
421
  defaults: {
430
422
  list_directory: true,
@@ -824,7 +816,95 @@ export const createRequestHandler = async (options?: {
824
816
  agentModelName = modelMatch[1].trim().replace(/^["']|["']$/g, "");
825
817
  }
826
818
  } catch {}
827
- const harness = new AgentHarness({ workingDir, environment: resolveHarnessEnvironment() });
819
+ const runOwners = new Map<string, string>();
820
+ const runConversations = new Map<string, string>();
821
+ type PendingApproval = {
822
+ ownerId: string;
823
+ runId: string;
824
+ conversationId: string | null;
825
+ tool: string;
826
+ input: Record<string, unknown>;
827
+ resolve: (approved: boolean) => void;
828
+ };
829
+ const pendingApprovals = new Map<string, PendingApproval>();
830
+
831
+ // Per-conversation event streaming: buffer events and allow SSE subscribers
832
+ type ConversationEventStream = {
833
+ buffer: AgentEvent[];
834
+ subscribers: Set<ServerResponse>;
835
+ finished: boolean;
836
+ };
837
+ const conversationEventStreams = new Map<string, ConversationEventStream>();
838
+ const broadcastEvent = (conversationId: string, event: AgentEvent): void => {
839
+ let stream = conversationEventStreams.get(conversationId);
840
+ if (!stream) {
841
+ stream = { buffer: [], subscribers: new Set(), finished: false };
842
+ conversationEventStreams.set(conversationId, stream);
843
+ }
844
+ stream.buffer.push(event);
845
+ for (const subscriber of stream.subscribers) {
846
+ try {
847
+ subscriber.write(formatSseEvent(event));
848
+ } catch {
849
+ stream.subscribers.delete(subscriber);
850
+ }
851
+ }
852
+ };
853
+ const finishConversationStream = (conversationId: string): void => {
854
+ const stream = conversationEventStreams.get(conversationId);
855
+ if (stream) {
856
+ stream.finished = true;
857
+ for (const subscriber of stream.subscribers) {
858
+ try {
859
+ subscriber.write("event: stream:end\ndata: {}\n\n");
860
+ subscriber.end();
861
+ } catch {
862
+ // Already closed.
863
+ }
864
+ }
865
+ stream.subscribers.clear();
866
+ // Keep buffer for a short time so late-joining clients get replay
867
+ setTimeout(() => conversationEventStreams.delete(conversationId), 30_000);
868
+ }
869
+ };
870
+ const persistConversationPendingApprovals = async (conversationId: string): Promise<void> => {
871
+ const conversation = await conversationStore.get(conversationId);
872
+ if (!conversation) {
873
+ return;
874
+ }
875
+ conversation.pendingApprovals = Array.from(pendingApprovals.entries())
876
+ .filter(
877
+ ([, pending]) =>
878
+ pending.ownerId === conversation.ownerId && pending.conversationId === conversationId,
879
+ )
880
+ .map(([approvalId, pending]) => ({
881
+ approvalId,
882
+ runId: pending.runId,
883
+ tool: pending.tool,
884
+ input: pending.input,
885
+ }));
886
+ await conversationStore.update(conversation);
887
+ };
888
+ const harness = new AgentHarness({
889
+ workingDir,
890
+ environment: resolveHarnessEnvironment(),
891
+ approvalHandler: async (request) =>
892
+ new Promise<boolean>((resolveApproval) => {
893
+ const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
894
+ const conversationIdForRun = runConversations.get(request.runId) ?? null;
895
+ pendingApprovals.set(request.approvalId, {
896
+ ownerId: ownerIdForRun,
897
+ runId: request.runId,
898
+ conversationId: conversationIdForRun,
899
+ tool: request.tool,
900
+ input: request.input,
901
+ resolve: resolveApproval,
902
+ });
903
+ if (conversationIdForRun) {
904
+ void persistConversationPendingApprovals(conversationIdForRun);
905
+ }
906
+ }),
907
+ });
828
908
  await harness.initialize();
829
909
  const telemetry = new TelemetryEmitter(config?.telemetry);
830
910
  const conversationStore = createConversationStore(resolveStateConfig(config), { workingDir });
@@ -1045,6 +1125,94 @@ export const createRequestHandler = async (options?: {
1045
1125
  return;
1046
1126
  }
1047
1127
 
1128
+ const approvalMatch = pathname.match(/^\/api\/approvals\/([^/]+)$/);
1129
+ if (approvalMatch && request.method === "POST") {
1130
+ const approvalId = decodeURIComponent(approvalMatch[1] ?? "");
1131
+ const pending = pendingApprovals.get(approvalId);
1132
+ if (!pending || pending.ownerId !== ownerId) {
1133
+ // If the server restarted, an old pending approval can remain in
1134
+ // conversation history without an active resolver. Prune stale entries.
1135
+ const conversations = await conversationStore.list(ownerId);
1136
+ let prunedStale = false;
1137
+ for (const conversation of conversations) {
1138
+ if (!Array.isArray(conversation.pendingApprovals)) {
1139
+ continue;
1140
+ }
1141
+ const next = conversation.pendingApprovals.filter(
1142
+ (approval) => approval.approvalId !== approvalId,
1143
+ );
1144
+ if (next.length !== conversation.pendingApprovals.length) {
1145
+ conversation.pendingApprovals = next;
1146
+ await conversationStore.update(conversation);
1147
+ prunedStale = true;
1148
+ }
1149
+ }
1150
+ writeJson(response, 404, {
1151
+ code: "APPROVAL_NOT_FOUND",
1152
+ message: prunedStale
1153
+ ? "Approval request is no longer active"
1154
+ : "Approval request not found",
1155
+ });
1156
+ return;
1157
+ }
1158
+ const body = (await readRequestBody(request)) as { approved?: boolean };
1159
+ const approved = body.approved === true;
1160
+ pendingApprovals.delete(approvalId);
1161
+ if (pending.conversationId) {
1162
+ await persistConversationPendingApprovals(pending.conversationId);
1163
+ }
1164
+ pending.resolve(approved);
1165
+ writeJson(response, 200, { ok: true, approvalId, approved });
1166
+ return;
1167
+ }
1168
+
1169
+ const conversationEventsMatch = pathname.match(
1170
+ /^\/api\/conversations\/([^/]+)\/events$/,
1171
+ );
1172
+ if (conversationEventsMatch && request.method === "GET") {
1173
+ const conversationId = decodeURIComponent(conversationEventsMatch[1] ?? "");
1174
+ const conversation = await conversationStore.get(conversationId);
1175
+ if (!conversation || conversation.ownerId !== ownerId) {
1176
+ writeJson(response, 404, {
1177
+ code: "CONVERSATION_NOT_FOUND",
1178
+ message: "Conversation not found",
1179
+ });
1180
+ return;
1181
+ }
1182
+ response.writeHead(200, {
1183
+ "Content-Type": "text/event-stream",
1184
+ "Cache-Control": "no-cache",
1185
+ Connection: "keep-alive",
1186
+ });
1187
+ const stream = conversationEventStreams.get(conversationId);
1188
+ if (!stream) {
1189
+ // No active run — close immediately
1190
+ response.write("event: stream:end\ndata: {}\n\n");
1191
+ response.end();
1192
+ return;
1193
+ }
1194
+ // Replay buffered events
1195
+ for (const bufferedEvent of stream.buffer) {
1196
+ try {
1197
+ response.write(formatSseEvent(bufferedEvent));
1198
+ } catch {
1199
+ response.end();
1200
+ return;
1201
+ }
1202
+ }
1203
+ if (stream.finished) {
1204
+ response.write("event: stream:end\ndata: {}\n\n");
1205
+ response.end();
1206
+ return;
1207
+ }
1208
+ // Subscribe to live events
1209
+ stream.subscribers.add(response);
1210
+ request.on("close", () => {
1211
+ stream.subscribers.delete(response);
1212
+ });
1213
+ return;
1214
+ }
1215
+
1048
1216
  const conversationPathMatch = pathname.match(/^\/api\/conversations\/([^/]+)$/);
1049
1217
  if (conversationPathMatch) {
1050
1218
  const conversationId = decodeURIComponent(conversationPathMatch[1] ?? "");
@@ -1057,7 +1225,35 @@ export const createRequestHandler = async (options?: {
1057
1225
  return;
1058
1226
  }
1059
1227
  if (request.method === "GET") {
1060
- writeJson(response, 200, { conversation });
1228
+ const storedPending = Array.isArray(conversation.pendingApprovals)
1229
+ ? conversation.pendingApprovals
1230
+ : [];
1231
+ const livePending = Array.from(pendingApprovals.entries())
1232
+ .filter(
1233
+ ([, pending]) =>
1234
+ pending.ownerId === ownerId && pending.conversationId === conversationId,
1235
+ )
1236
+ .map(([approvalId, pending]) => ({
1237
+ approvalId,
1238
+ runId: pending.runId,
1239
+ tool: pending.tool,
1240
+ input: pending.input,
1241
+ }));
1242
+ const mergedPendingById = new Map<string, (typeof livePending)[number]>();
1243
+ for (const approval of storedPending) {
1244
+ if (approval && typeof approval.approvalId === "string") {
1245
+ mergedPendingById.set(approval.approvalId, approval);
1246
+ }
1247
+ }
1248
+ for (const approval of livePending) {
1249
+ mergedPendingById.set(approval.approvalId, approval);
1250
+ }
1251
+ writeJson(response, 200, {
1252
+ conversation: {
1253
+ ...conversation,
1254
+ pendingApprovals: Array.from(mergedPendingById.values()),
1255
+ },
1256
+ });
1061
1257
  return;
1062
1258
  }
1063
1259
  if (request.method === "PATCH") {
@@ -1114,6 +1310,7 @@ export const createRequestHandler = async (options?: {
1114
1310
  "Cache-Control": "no-cache",
1115
1311
  Connection: "keep-alive",
1116
1312
  });
1313
+ const historyMessages = [...conversation.messages];
1117
1314
  let latestRunId = conversation.runtimeRunId ?? "";
1118
1315
  let assistantResponse = "";
1119
1316
  const toolTimeline: string[] = [];
@@ -1121,6 +1318,48 @@ export const createRequestHandler = async (options?: {
1121
1318
  let currentText = "";
1122
1319
  let currentTools: string[] = [];
1123
1320
  try {
1321
+ // Persist the user turn immediately so refreshing mid-run keeps chat context.
1322
+ conversation.messages = [...historyMessages, { role: "user", content: messageText }];
1323
+ conversation.updatedAt = Date.now();
1324
+ await conversationStore.update(conversation);
1325
+
1326
+ const persistDraftAssistantTurn = async (): Promise<void> => {
1327
+ const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
1328
+ ...sections.map((section) => ({
1329
+ type: section.type,
1330
+ content: Array.isArray(section.content) ? [...section.content] : section.content,
1331
+ })),
1332
+ ];
1333
+ if (currentTools.length > 0) {
1334
+ draftSections.push({ type: "tools", content: [...currentTools] });
1335
+ }
1336
+ if (currentText.length > 0) {
1337
+ draftSections.push({ type: "text", content: currentText });
1338
+ }
1339
+ const hasDraftContent =
1340
+ assistantResponse.length > 0 || toolTimeline.length > 0 || draftSections.length > 0;
1341
+ if (!hasDraftContent) {
1342
+ return;
1343
+ }
1344
+ conversation.messages = [
1345
+ ...historyMessages,
1346
+ { role: "user", content: messageText },
1347
+ {
1348
+ role: "assistant",
1349
+ content: assistantResponse,
1350
+ metadata:
1351
+ toolTimeline.length > 0 || draftSections.length > 0
1352
+ ? ({
1353
+ toolActivity: [...toolTimeline],
1354
+ sections: draftSections.length > 0 ? draftSections : undefined,
1355
+ } as Message["metadata"])
1356
+ : undefined,
1357
+ },
1358
+ ];
1359
+ conversation.updatedAt = Date.now();
1360
+ await conversationStore.update(conversation);
1361
+ };
1362
+
1124
1363
  const recallCorpus = (await conversationStore.list(ownerId))
1125
1364
  .filter((item) => item.conversationId !== conversationId)
1126
1365
  .slice(0, 20)
@@ -1143,10 +1382,12 @@ export const createRequestHandler = async (options?: {
1143
1382
  __conversationRecallCorpus: recallCorpus,
1144
1383
  __activeConversationId: conversationId,
1145
1384
  },
1146
- messages: conversation.messages,
1385
+ messages: historyMessages,
1147
1386
  })) {
1148
1387
  if (event.type === "run:started") {
1149
1388
  latestRunId = event.runId;
1389
+ runOwners.set(event.runId, ownerId);
1390
+ runConversations.set(event.runId, conversationId);
1150
1391
  }
1151
1392
  if (event.type === "model:chunk") {
1152
1393
  // If we have tools accumulated and text starts again, push tools as a section
@@ -1181,16 +1422,19 @@ export const createRequestHandler = async (options?: {
1181
1422
  const toolText = `- approval required \`${event.tool}\``;
1182
1423
  toolTimeline.push(toolText);
1183
1424
  currentTools.push(toolText);
1425
+ await persistDraftAssistantTurn();
1184
1426
  }
1185
1427
  if (event.type === "tool:approval:granted") {
1186
1428
  const toolText = `- approval granted (${event.approvalId})`;
1187
1429
  toolTimeline.push(toolText);
1188
1430
  currentTools.push(toolText);
1431
+ await persistDraftAssistantTurn();
1189
1432
  }
1190
1433
  if (event.type === "tool:approval:denied") {
1191
1434
  const toolText = `- approval denied (${event.approvalId})`;
1192
1435
  toolTimeline.push(toolText);
1193
1436
  currentTools.push(toolText);
1437
+ await persistDraftAssistantTurn();
1194
1438
  }
1195
1439
  if (
1196
1440
  event.type === "run:completed" &&
@@ -1200,7 +1444,13 @@ export const createRequestHandler = async (options?: {
1200
1444
  assistantResponse = event.result.response;
1201
1445
  }
1202
1446
  await telemetry.emit(event);
1203
- response.write(formatSseEvent(event));
1447
+ broadcastEvent(conversationId, event);
1448
+ try {
1449
+ response.write(formatSseEvent(event));
1450
+ } catch {
1451
+ // Client disconnected (e.g. browser refresh). Continue processing
1452
+ // so the run completes and conversation is persisted.
1453
+ }
1204
1454
  }
1205
1455
  // Finalize sections
1206
1456
  if (currentTools.length > 0) {
@@ -1210,7 +1460,7 @@ export const createRequestHandler = async (options?: {
1210
1460
  sections.push({ type: "text", content: currentText });
1211
1461
  }
1212
1462
  conversation.messages = [
1213
- ...conversation.messages,
1463
+ ...historyMessages,
1214
1464
  { role: "user", content: messageText },
1215
1465
  {
1216
1466
  role: "assistant",
@@ -1225,21 +1475,62 @@ export const createRequestHandler = async (options?: {
1225
1475
  },
1226
1476
  ];
1227
1477
  conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
1478
+ conversation.pendingApprovals = [];
1228
1479
  conversation.updatedAt = Date.now();
1229
1480
  await conversationStore.update(conversation);
1230
1481
  } catch (error) {
1231
- response.write(
1232
- formatSseEvent({
1233
- type: "run:error",
1234
- runId: latestRunId || "run_unknown",
1235
- error: {
1236
- code: "RUN_ERROR",
1237
- message: error instanceof Error ? error.message : "Unknown error",
1238
- },
1239
- }),
1240
- );
1482
+ try {
1483
+ response.write(
1484
+ formatSseEvent({
1485
+ type: "run:error",
1486
+ runId: latestRunId || "run_unknown",
1487
+ error: {
1488
+ code: "RUN_ERROR",
1489
+ message: error instanceof Error ? error.message : "Unknown error",
1490
+ },
1491
+ }),
1492
+ );
1493
+ } catch {
1494
+ // Client already disconnected; persist whatever we accumulated.
1495
+ const fallbackSections = [...sections];
1496
+ if (currentTools.length > 0) {
1497
+ fallbackSections.push({ type: "tools", content: [...currentTools] });
1498
+ }
1499
+ if (currentText.length > 0) {
1500
+ fallbackSections.push({ type: "text", content: currentText });
1501
+ }
1502
+ if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
1503
+ conversation.messages = [
1504
+ ...historyMessages,
1505
+ { role: "user", content: messageText },
1506
+ {
1507
+ role: "assistant",
1508
+ content: assistantResponse,
1509
+ metadata:
1510
+ toolTimeline.length > 0 || fallbackSections.length > 0
1511
+ ? ({
1512
+ toolActivity: [...toolTimeline],
1513
+ sections: fallbackSections.length > 0 ? fallbackSections : undefined,
1514
+ } as Message["metadata"])
1515
+ : undefined,
1516
+ },
1517
+ ];
1518
+ conversation.updatedAt = Date.now();
1519
+ await conversationStore.update(conversation);
1520
+ }
1521
+ }
1241
1522
  } finally {
1242
- response.end();
1523
+ finishConversationStream(conversationId);
1524
+ await persistConversationPendingApprovals(conversationId);
1525
+ if (latestRunId) {
1526
+ runOwners.delete(latestRunId);
1527
+ runConversations.delete(latestRunId);
1528
+ }
1529
+ try {
1530
+ response.end();
1531
+ } catch {
1532
+ // Already closed.
1533
+ }
1243
1534
  }
1244
1535
  return;
1245
1536
  }
@@ -1851,25 +2142,14 @@ export const mcpList = async (workingDir: string): Promise<void> => {
1851
2142
  const mcp = config?.mcp ?? [];
1852
2143
  if (mcp.length === 0) {
1853
2144
  process.stdout.write("No MCP servers configured.\n");
1854
- if (config?.scripts) {
1855
- process.stdout.write(
1856
- `Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}\n`,
1857
- );
1858
- }
1859
2145
  return;
1860
2146
  }
1861
2147
  process.stdout.write("Configured MCP servers:\n");
1862
2148
  for (const entry of mcp) {
1863
2149
  const auth =
1864
2150
  entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
1865
- const mode = entry.tools?.mode ?? "all";
1866
- process.stdout.write(
1867
- `- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}, mode=${mode})\n`,
1868
- );
1869
- }
1870
- if (config?.scripts) {
1871
2151
  process.stdout.write(
1872
- `Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}\n`,
2152
+ `- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth})\n`,
1873
2153
  );
1874
2154
  }
1875
2155
  };
@@ -2013,40 +2293,26 @@ export const mcpToolsSelect = async (
2013
2293
  selected.length === discovered.length
2014
2294
  ? [`${serverName}/*`]
2015
2295
  : selected.sort();
2016
- const { config, index } = await resolveMcpEntry(workingDir, serverName);
2017
- const mcp = [...(config.mcp ?? [])];
2018
- const existing = mcp[index];
2019
- mcp[index] = {
2020
- ...existing,
2021
- tools: {
2022
- ...(existing.tools ?? {}),
2023
- mode: "allowlist",
2024
- include: includePatterns,
2025
- },
2026
- };
2027
- await writeConfigFile(workingDir, { ...config, mcp });
2028
- process.stdout.write(
2029
- `Updated ${serverName} to allowlist ${includePatterns.join(", ")} in poncho.config.js.\n`,
2030
- );
2296
+ process.stdout.write(`Selected MCP tools: ${includePatterns.join(", ")}\n`);
2031
2297
  process.stdout.write(
2032
- "\nRequired next step: add MCP intent in AGENT.md or SKILL.md. Without this, these MCP tools will not be registered for the model.\n",
2298
+ "\nRequired next step: add MCP intent in AGENT.md or SKILL.md allowed-tools. Without this, these MCP tools will not be registered for the model.\n",
2033
2299
  );
2034
2300
  process.stdout.write(
2035
2301
  "\nOption A: AGENT.md (global fallback intent)\n" +
2036
2302
  "Paste this into AGENT.md frontmatter:\n" +
2037
2303
  "---\n" +
2038
- "tools:\n" +
2039
- " mcp:\n" +
2040
- includePatterns.map((tool) => ` - ${tool}`).join("\n") +
2304
+ "allowed-tools:\n" +
2305
+ includePatterns.map((tool) => ` - mcp:${tool}`).join("\n") +
2041
2306
  "\n---\n",
2042
2307
  );
2043
2308
  process.stdout.write(
2044
2309
  "\nOption B: SKILL.md (only when that skill is activated)\n" +
2045
2310
  "Paste this into SKILL.md frontmatter:\n" +
2046
2311
  "---\n" +
2047
- "tools:\n" +
2048
- " mcp:\n" +
2049
- includePatterns.map((tool) => ` - ${tool}`).join("\n") +
2312
+ "allowed-tools:\n" +
2313
+ includePatterns.map((tool) => ` - mcp:${tool}`).join("\n") +
2314
+ "\napproval-required:\n" +
2315
+ includePatterns.map((tool) => ` - mcp:${tool}`).join("\n") +
2050
2316
  "\n---\n",
2051
2317
  );
2052
2318
  };
@@ -2220,7 +2486,7 @@ export const buildCli = (): Command => {
2220
2486
  mcpToolsCommand
2221
2487
  .command("select")
2222
2488
  .argument("<name>", "server name")
2223
- .description("Select MCP tools and store as config allowlist")
2489
+ .description("Select MCP tools and print frontmatter allowed-tools entries")
2224
2490
  .option("--all", "select all discovered tools", false)
2225
2491
  .option("--tools <csv>", "comma-separated discovered tool names")
2226
2492
  .action(