+
+ _, err = fmt.Fprintln(out, line)
+ if err != nil {
+ return fmt.Errorf("write import plan: %w", err)
+ }
+ }
+
+ estimate := 1.3 * float64(p.messages)
+ if len(p.added) == 0 {
+ estimate = 0
+ }
+
+ lines := []string{
+ fmt.Sprintf("batches: %d (%d chat-days skipped)", len(p.added), p.skipped),
+ "dates: " + dateRange,
+ "cutoff: " + cutoff,
+ "owner: " + owner,
+ fmt.Sprintf("bytes: %d (new batch text)", p.bytes),
+ fmt.Sprintf("messages: %d (merged log)", p.messages),
+ fmt.Sprintf("estimated calls: %.0f (about 1.3 per message, limit %d)", estimate, rebuildCallLimit),
+ }
+
+ _, err = io.WriteString(out, strings.Join(lines, "\n")+"\n")
+ if err != nil {
+ return fmt.Errorf("write import plan: %w", err)
+ }
+
+ return nil
+}
+
+// whatsappImport runs the offline import command. It holds the same token lock
+// as the daemon and probe, even for a plan. The plan reads no credentials for a
+// model and makes no network call. Only a completed rebuild replaces the agent
+// directory. Unfinished subagents and pending target deliveries prevent either
+// mode.
+func whatsappImport(ctx context.Context, args []string, out io.Writer) (err error) {
+ options, err := whatsappImportParse(args)
+ if err != nil {
+ return fmt.Errorf("parse import arguments: %w", err)