@poncho-ai/cli 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -349,7 +349,20 @@ export const buildConfigFromOnboardingAnswers = (
349
349
  };
350
350
 
351
351
  if (messagingPlatform !== "none") {
352
- config.messaging = [{ platform: messagingPlatform as "slack" }];
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];
353
366
  }
354
367
 
355
368
  return config;
@@ -22,14 +22,6 @@ import { consumeFirstRunIntro } from "./init-feature-context.js";
22
22
  import { resolveHarnessEnvironment } from "./index.js";
23
23
  import { getMascotLines } from "./mascot.js";
24
24
 
25
- // Re-export types that index.ts references
26
- export type ApprovalRequest = {
27
- tool: string;
28
- input: Record<string, unknown>;
29
- approvalId: string;
30
- resolve: (approved: boolean) => void;
31
- };
32
-
33
25
  export type SessionSnapshot = {
34
26
  messages: Message[];
35
27
  nextTurn: number;
@@ -374,14 +366,12 @@ export const runInteractiveInk = async ({
374
366
  workingDir,
375
367
  config,
376
368
  conversationStore,
377
- onSetApprovalCallback,
378
369
  }: {
379
370
  harness: AgentHarness;
380
371
  params: Record<string, string>;
381
372
  workingDir: string;
382
373
  config?: PonchoConfig;
383
374
  conversationStore: ConversationStore;
384
- onSetApprovalCallback?: (cb: (req: ApprovalRequest) => void) => void;
385
375
  }): Promise<void> => {
386
376
  const metadata = await loadMetadata(workingDir);
387
377
 
@@ -391,32 +381,6 @@ export const runInteractiveInk = async ({
391
381
  terminal: true,
392
382
  });
393
383
 
394
- // --- Approval bridge -------------------------------------------------------
395
- // When the harness needs tool approval, it calls the approval handler in
396
- // index.ts, which creates a pending promise and fires our callback.
397
- // We use readline to prompt the user for y/n.
398
- if (onSetApprovalCallback) {
399
- onSetApprovalCallback((req: ApprovalRequest) => {
400
- // Print approval prompt — we're mid-turn so stdout might have partial text
401
- process.stdout.write("\n");
402
- const preview = compactPreview(req.input, 100);
403
- rl.question(
404
- `${C.yellow}${C.bold}Tool "${req.tool}" requires approval${C.reset}\n` +
405
- `${C.gray}input: ${preview}${C.reset}\n` +
406
- `${C.yellow}approve? (y/n): ${C.reset}`,
407
- (answer) => {
408
- const approved = answer.trim().toLowerCase() === "y";
409
- console.log(
410
- approved
411
- ? green(` approved ${req.tool}`)
412
- : magenta(` denied ${req.tool}`),
413
- );
414
- req.resolve(approved);
415
- },
416
- );
417
- });
418
- }
419
-
420
384
  // --- Print header ----------------------------------------------------------
421
385
 
422
386
  console.log("");
@@ -555,130 +519,190 @@ export const runInteractiveInk = async ({
555
519
  pendingFiles = [];
556
520
  }
557
521
 
522
+ type CheckpointEvent = Extract<AgentEvent, { type: "tool:approval:checkpoint" }>;
523
+ let eventSource: AsyncGenerator<AgentEvent> = harness.run({
524
+ task: trimmed,
525
+ parameters: params,
526
+ messages,
527
+ files: turnFiles.length > 0 ? turnFiles : undefined,
528
+ abortSignal: activeRunAbortController.signal,
529
+ });
530
+
558
531
  try {
559
- for await (const event of harness.run({
560
- task: trimmed,
561
- parameters: params,
562
- messages,
563
- files: turnFiles.length > 0 ? turnFiles : undefined,
564
- abortSignal: activeRunAbortController.signal,
565
- })) {
566
- if (event.type === "run:started") {
567
- latestRunId = event.runId;
568
- }
569
- if (event.type === "model:chunk") {
570
- sawChunk = true;
571
- // If we have tools accumulated and text starts again, push tools as a section
572
- if (currentTools.length > 0) {
573
- sections.push({ type: "tools", content: currentTools });
574
- currentTools = [];
532
+ // eslint-disable-next-line no-constant-condition
533
+ while (true) {
534
+ let checkpointEvent: CheckpointEvent | null = null;
535
+
536
+ for await (const event of eventSource) {
537
+ if (event.type === "run:started") {
538
+ latestRunId = event.runId;
575
539
  }
576
- responseText += event.content;
577
- streamedText += event.content;
578
- currentText += event.content;
540
+ if (event.type === "model:chunk") {
541
+ sawChunk = true;
542
+ if (currentTools.length > 0) {
543
+ sections.push({ type: "tools", content: currentTools });
544
+ currentTools = [];
545
+ }
546
+ responseText += event.content;
547
+ streamedText += event.content;
548
+ currentText += event.content;
579
549
 
580
- if (!thinkingCleared) {
550
+ if (!thinkingCleared) {
551
+ clearThinking();
552
+ process.stdout.write(`${C.green}assistant> ${C.reset}`);
553
+ }
554
+ process.stdout.write(event.content);
555
+ } else if (
556
+ event.type === "tool:started" ||
557
+ event.type === "tool:completed" ||
558
+ event.type === "tool:error" ||
559
+ event.type === "tool:approval:required"
560
+ ) {
561
+ if (streamedText.length > 0) {
562
+ committedText = true;
563
+ streamedText = "";
564
+ process.stdout.write("\n");
565
+ }
581
566
  clearThinking();
582
- process.stdout.write(`${C.green}assistant> ${C.reset}`);
583
- }
584
- // Stream the text directly to stdout
585
- process.stdout.write(event.content);
586
- } else if (
587
- event.type === "tool:started" ||
588
- event.type === "tool:completed" ||
589
- event.type === "tool:error" ||
590
- event.type === "tool:approval:required" ||
591
- event.type === "tool:approval:granted" ||
592
- event.type === "tool:approval:denied"
593
- ) {
594
- // Flush any streaming text before tool output
595
- if (streamedText.length > 0) {
596
- committedText = true;
597
- streamedText = "";
598
- process.stdout.write("\n");
599
- }
600
- clearThinking();
601
567
 
602
- if (event.type === "tool:started") {
603
- // If we have text accumulated, push it as a text section
604
- if (currentText.length > 0) {
605
- sections.push({ type: "text", content: currentText });
606
- currentText = "";
568
+ if (event.type === "tool:started") {
569
+ if (currentText.length > 0) {
570
+ sections.push({ type: "text", content: currentText });
571
+ currentText = "";
572
+ }
573
+ const preview = showToolPayloads
574
+ ? compactPreview(event.input, 400)
575
+ : compactPreview(event.input, 100);
576
+ console.log(yellow(`tools> start ${event.tool} input=${preview}`));
577
+ const toolText = `- start \`${event.tool}\``;
578
+ toolTimeline.push(toolText);
579
+ currentTools.push(toolText);
580
+ toolEvents += 1;
581
+ } else if (event.type === "tool:completed") {
582
+ const preview = showToolPayloads
583
+ ? compactPreview(event.output, 400)
584
+ : compactPreview(event.output, 100);
585
+ console.log(
586
+ yellow(
587
+ `tools> done ${event.tool} in ${formatDuration(event.duration)}`,
588
+ ),
589
+ );
590
+ if (showToolPayloads) {
591
+ console.log(yellow(`tools> output ${preview}`));
592
+ }
593
+ const toolText = `- done \`${event.tool}\` in ${formatDuration(event.duration)}`;
594
+ toolTimeline.push(toolText);
595
+ currentTools.push(toolText);
596
+ } else if (event.type === "tool:error") {
597
+ console.log(
598
+ red(`tools> error ${event.tool}: ${event.error}`),
599
+ );
600
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
601
+ toolTimeline.push(toolText);
602
+ currentTools.push(toolText);
603
+ } else if (event.type === "tool:approval:required") {
604
+ console.log(
605
+ magenta(`tools> approval required for ${event.tool}`),
606
+ );
607
+ const toolText = `- approval required \`${event.tool}\``;
608
+ toolTimeline.push(toolText);
609
+ currentTools.push(toolText);
607
610
  }
608
- const preview = showToolPayloads
609
- ? compactPreview(event.input, 400)
610
- : compactPreview(event.input, 100);
611
- console.log(yellow(`tools> start ${event.tool} input=${preview}`));
612
- const toolText = `- start \`${event.tool}\``;
613
- toolTimeline.push(toolText);
614
- currentTools.push(toolText);
615
- toolEvents += 1;
616
- } else if (event.type === "tool:completed") {
617
- const preview = showToolPayloads
618
- ? compactPreview(event.output, 400)
619
- : compactPreview(event.output, 100);
620
- console.log(
621
- yellow(
622
- `tools> done ${event.tool} in ${formatDuration(event.duration)}`,
623
- ),
624
- );
625
- if (showToolPayloads) {
626
- console.log(yellow(`tools> output ${preview}`));
611
+ } else if (event.type === "tool:approval:checkpoint") {
612
+ checkpointEvent = event as CheckpointEvent;
613
+ } else if (event.type === "run:error") {
614
+ clearThinking();
615
+ runFailed = true;
616
+ console.log(red(`error> ${event.error.message}`));
617
+ } else if (event.type === "run:cancelled") {
618
+ clearThinking();
619
+ runCancelled = true;
620
+ } else if (event.type === "model:response") {
621
+ usage = event.usage;
622
+ } else if (event.type === "run:completed" && !sawChunk) {
623
+ clearThinking();
624
+ responseText = event.result.response ?? "";
625
+ if (responseText.length > 0) {
626
+ process.stdout.write(
627
+ `${C.green}assistant> ${C.reset}${responseText}\n`,
628
+ );
627
629
  }
628
- const toolText = `- done \`${event.tool}\` in ${formatDuration(event.duration)}`;
629
- toolTimeline.push(toolText);
630
- currentTools.push(toolText);
631
- } else if (event.type === "tool:error") {
632
- console.log(
633
- red(`tools> error ${event.tool}: ${event.error}`),
634
- );
635
- const toolText = `- error \`${event.tool}\`: ${event.error}`;
636
- toolTimeline.push(toolText);
637
- currentTools.push(toolText);
638
- } else if (event.type === "tool:approval:required") {
639
- console.log(
640
- magenta(`tools> approval required for ${event.tool}`),
641
- );
642
- const toolText = `- approval required \`${event.tool}\``;
643
- toolTimeline.push(toolText);
644
- currentTools.push(toolText);
645
- } else if (event.type === "tool:approval:granted") {
646
- console.log(
647
- gray(`tools> approval granted (${event.approvalId})`),
648
- );
649
- const toolText = `- approval granted (${event.approvalId})`;
650
- toolTimeline.push(toolText);
651
- currentTools.push(toolText);
652
- } else if (event.type === "tool:approval:denied") {
653
- console.log(
654
- magenta(`tools> approval denied (${event.approvalId})`),
655
- );
656
- const toolText = `- approval denied (${event.approvalId})`;
657
- toolTimeline.push(toolText);
658
- currentTools.push(toolText);
659
630
  }
660
- } else if (event.type === "run:error") {
661
- clearThinking();
662
- runFailed = true;
663
- console.log(red(`error> ${event.error.message}`));
664
- } else if (event.type === "run:cancelled") {
665
- clearThinking();
666
- runCancelled = true;
667
- } else if (event.type === "model:response") {
668
- usage = event.usage;
669
- } else if (event.type === "run:completed" && !sawChunk) {
670
- clearThinking();
671
- responseText = event.result.response ?? "";
672
- if (responseText.length > 0) {
673
- process.stdout.write(
674
- `${C.green}assistant> ${C.reset}${responseText}\n`,
675
- );
631
+ }
632
+
633
+ if (!checkpointEvent) break;
634
+
635
+ // Prompt user for approval
636
+ if (streamedText.length > 0) {
637
+ process.stdout.write("\n");
638
+ streamedText = "";
639
+ }
640
+ clearThinking();
641
+ const preview = compactPreview(checkpointEvent.input, 100);
642
+ const answer = await ask(rl,
643
+ `${C.yellow}${C.bold}Tool "${checkpointEvent.tool}" requires approval${C.reset}\n` +
644
+ `${C.gray}input: ${preview}${C.reset}\n` +
645
+ `${C.yellow}approve? (y/n): ${C.reset}`,
646
+ );
647
+ const approved = answer.trim().toLowerCase() === "y";
648
+ console.log(
649
+ approved
650
+ ? green(` approved ${checkpointEvent.tool}`)
651
+ : magenta(` denied ${checkpointEvent.tool}`),
652
+ );
653
+
654
+ const approvalText = approved
655
+ ? `- approval granted (${checkpointEvent.approvalId})`
656
+ : `- approval denied (${checkpointEvent.approvalId})`;
657
+ toolTimeline.push(approvalText);
658
+ currentTools.push(approvalText);
659
+
660
+ let toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>;
661
+ if (approved) {
662
+ const execResults = await harness.executeTools(
663
+ [{ id: checkpointEvent.toolCallId, name: checkpointEvent.tool, input: checkpointEvent.input }],
664
+ { runId: latestRunId, agentId: "interactive", step: 0, workingDir, parameters: params },
665
+ );
666
+ toolResults = execResults.map(r => ({
667
+ callId: r.callId,
668
+ toolName: r.tool,
669
+ result: r.output,
670
+ error: r.error,
671
+ }));
672
+ for (const r of execResults) {
673
+ if (r.error) {
674
+ console.log(red(`tools> error ${r.tool}: ${r.error}`));
675
+ const toolText = `- error \`${r.tool}\`: ${r.error}`;
676
+ toolTimeline.push(toolText);
677
+ currentTools.push(toolText);
678
+ } else {
679
+ console.log(yellow(`tools> done ${r.tool}`));
680
+ const toolText = `- done \`${r.tool}\``;
681
+ toolTimeline.push(toolText);
682
+ currentTools.push(toolText);
683
+ }
676
684
  }
685
+ } else {
686
+ toolResults = [{
687
+ callId: checkpointEvent.toolCallId,
688
+ toolName: checkpointEvent.tool,
689
+ error: "Tool execution denied by user",
690
+ }];
677
691
  }
692
+
693
+ const fullMessages = [...messages, ...checkpointEvent.checkpointMessages];
694
+ eventSource = harness.continueFromToolResult({
695
+ messages: fullMessages,
696
+ toolResults,
697
+ abortSignal: activeRunAbortController!.signal,
698
+ });
699
+
700
+ process.stdout.write(gray("thinking..."));
701
+ thinkingCleared = false;
678
702
  }
679
703
  } catch (error) {
680
704
  clearThinking();
681
- if (activeRunAbortController.signal.aborted) {
705
+ if (activeRunAbortController?.signal.aborted) {
682
706
  runCancelled = true;
683
707
  } else {
684
708
  runFailed = true;